#!/usr/bin/env bash
#
# The repo's own guards, before a commit rather than after it.
#
# ## Why this lives in the repository and not in .git/hooks
#
# `.git/hooks` is not versioned, so a hook living there protects exactly one
# checkout and silently protects nothing anywhere else. This directory is
# committed, and `core.hooksPath` points at it:
#
#   git config core.hooksPath .githooks
#
# That one line is the only setup, and it is per clone. See the README.
#
# ## Adapted from the template's version
#
# The template's pre-commit runs `npx tsc --noEmit` and `npx vitest`. Period is
# a Kotlin/Android project with no npm anywhere, so the two guard commands are
# Gradle instead. Everything else — the ordering, the secret scan, the
# unstaged-changes warning, SKIP_GUARDS — is unchanged, because the arguments
# for those are not language-specific.
#
# ## What it checks, and why in this order
#
# 1. The secret scan, first and cheapest: it reads the staged diff only. A
#    keystore caught here costs a `git reset`; the same keystore caught after a
#    push cannot be undone at all — an upload key cannot be rotated once the app
#    is published.
# 2. The JVM unit tests, when Kotlin is staged. They are the prediction
#    acceptance cases from PRODUCT_PLAN.md §51 and they run on the JVM in about
#    a second, which is the whole reason :domain:* is not an Android module.
#
# Compilation of the Android modules is deliberately NOT run here. It needs the
# SDK and takes half a minute, and a pre-commit hook people disable is worse
# than one that checks less. `./gradlew assembleRelease` belongs to the release
# checklist — docs/security/SECURITY_CHECKLIST.md — which is where it is.
#
# ## It warns about unstaged changes rather than failing on them
#
# Both commands run against the **working tree**, not the index. A clean run
# proves the working tree is good, which is only the same thing as the commit
# being good when nothing is left unstaged. Saying so is more honest than
# implying a guarantee that was not made.
#
# ## Escape hatch
#
#   SKIP_GUARDS=1 git commit ...     skips both, loudly
#   git commit --no-verify ...       skips the hook entirely, silently
#
# The first is preferred: it leaves a line in the terminal saying the guards did
# not run, which is the difference between a deliberate exception and a habit.
#
# ## Exit codes
#
#   0  the guards that applied ran and passed
#   1  a guard failed, or refused — the commit is stopped

set -uo pipefail

cd "$(git rev-parse --show-toplevel)" || exit 1

say() { printf '\033[1mpre-commit:\033[0m %s\n' "$*" >&2; }

if [ -n "${SKIP_GUARDS:-}" ]; then
  say "SKIP_GUARDS set — the secret scan and tests did NOT run for this commit."
  exit 0
fi

# Nothing staged is not this hook's problem; git will refuse on its own.
if git diff --cached --quiet; then
  exit 0
fi

# Credentials, before the commit exists.
if [ -f scripts/secrets.sh ]; then
  if ! bash scripts/secrets.sh; then
    say "possible credential in the staged changes — commit refused."
    say "If it is real, ROTATE IT. Deleting the line does not remove it from a"
    say "commit that already exists. If it is not, --allow the path or adjust"
    say "the patterns; do not silence the check."
    exit 1
  fi
else
  # Said out loud rather than passed over: a missing scanner reads exactly like
  # a scanner that found nothing.
  say "note: scripts/secrets.sh is not here, so nothing scanned the staged diff."
fi

# Only worth running when Kotlin or the build changed. A docs-only commit does
# not need the suite; a change to a build file does, because that is how a
# module boundary gets widened without anyone reading a Kotlin line.
staged=$(git diff --cached --name-only)
touches_code=$(printf '%s\n' "$staged" \
  | grep -cE '\.(kt|kts)$|^gradle/libs\.versions\.toml$|^gradle\.properties$' || true)

if [ "$touches_code" -gt 0 ]; then
  say "JVM unit tests…"

  # :domain:* only. These are pure-JVM modules, so this needs no Android SDK and
  # no emulator, and it is the suite that guards the product's core claim.
  if ! ./gradlew --quiet --console=plain :domain:cycle:test :domain:prediction:test; then
    say "tests failed — commit refused."
    exit 1
  fi
else
  say "no .kt/.kts or build file staged — skipping the suite."
fi

# Said last so it is the thing still on screen when the editor opens.
if ! git diff --quiet; then
  say "NOTE: unstaged changes are present. The guards ran against the working"
  say "      tree, so they did not verify this commit in isolation."
fi

exit 0
