#!/usr/bin/env bash
# test-gates.sh — zero-dep regression tests for gates.sh
# Follows pattern from test-run-plan-lib.sh with both success + fail-closed branches
# Tests: gate0 strict, gate0 baseline-ratchet, risk subcommand

set -uo pipefail
# Gate mechanics only: throwaway fixture repos must not be shipped to a buildbox.
# Remote dispatch has its own suite (workstation/claude/tests/gate0-remote-dispatch).
export GATE0_REMOTE=0
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GATES="$LIB_DIR/gates.sh"
RISCER="$LIB_DIR/risk-router.sh"
PASS=0
FAIL=0
out=""
TEST_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/gates-test-output-XXXXXX")
trap 'rm -rf "$TEST_TMPDIR"' EXIT

ok()   { PASS=$((PASS+1)); printf '  ok   %s\n' "$1"; }
bad()  { FAIL=$((FAIL+1)); printf '  FAIL %s\n     %s\n' "$1" "$2"; }

# mkrepo: new throwaway repo; echoes its path.
mkrepo() {
  local d; d=$(mktemp -d "/tmp/gates-test-${1:-}XXXX")
  git -C "$d" init -q
  git -C "$d" config user.email t@t.t; git -C "$d" config user.name t
  git -C "$d" config commit.gpgsign false
  mkdir -p "$d/.git/hooks-empty"
  git -C "$d" config core.hooksPath "$d/.git/hooks-empty"
  printf '%s' "$d"
}

use_repo_hooks() {
  local d=$1
  mkdir -p "$d/.git/hooks"
  git -C "$d" config core.hooksPath "$d/.git/hooks"
}

# commitf: $1 repo $2 path $3 content $4 msg
commitf() {
  local r=$1 p=$2; mkdir -p "$r/$(dirname "$p")"
  printf '%s\n' "$3" > "$r/$p"
  git -C "$r" add "$p"
  git -C "$r" commit -q -m "$4"
  git -C "$r" rev-parse HEAD
}

echo "gates.sh tests:"

# ==== gate0 strict mode tests ===

echo

# 1. gate0 strict mode success (package.json discovered)
r=$(mkrepo g0strict); commitf "$r" package.json '{"scripts": {"test": "node --version"}}' "seed"
base=$(commitf "$r" src/a.js "export const a=1" "seed")
head=$(commitf "$r" src/a.js "export const a=2" "feat: bump a")

# Source a test that finds the package.json
(
  cd "$r"
  "$GATES" gate0 strict "$r" "$r" "test" 2>/dev/null
) > "$TEST_TMPDIR/gates_test_output" 2>&1
if [[ $? -eq 0 ]]; then
  ok "gate0 strict mode with package.json (success)"
else
  bad "gate0 strict mode with package.json (should succeed)" "$out"
fi
rm -rf "$r"

# 1aa. gate0 emits semantic lifecycle events for a successful check
r=$(mkrepo g0semantic)
printf '%s\n' '{"scripts": {"test": "node --version"}}' > "$r/package.json"
git -C "$r" add package.json
git -C "$r" commit -q -m "seed"
out=$("$GATES" gate0 strict "$r" "$r" "semantic" 2>&1) && rc=$? || rc=$?
events=$(printf '%s\n' "$out" | grep '^{' || true)
if [[ $rc -eq 0 ]] \
  && jq -e 'select(.protocol == "runplan.gate/v1" and .event == "check.start")' <<<"$events" >/dev/null \
  && jq -e 'select(.protocol == "runplan.gate/v1" and .event == "item.start" and (.pid > 0))' <<<"$events" >/dev/null \
  && jq -e 'select(.protocol == "runplan.gate/v1" and .event == "gate.complete")' <<<"$events" >/dev/null; then
  ok "gate0 emits semantic lifecycle events"
else
  bad "gate0 emits semantic lifecycle events" "$out"
fi
rm -rf "$r"

# 1a. gate0 fails when a nested pnpm filter matches no workspace package
pnpm_real=""
while IFS= read -r candidate; do
  [[ -n "$candidate" ]] || continue
  [[ "$candidate" == "$HOME/.claude/bin/"* ]] && continue
  if "$candidate" --version >/dev/null 2>&1; then
    pnpm_real="$candidate"
    break
  fi
done < <(
  {
    printf '%s\n' "$HOME/.npm-global/bin/pnpm"
    type -P -a pnpm 2>/dev/null
  } | awk '!seen[$0]++'
)
npm_real="$(type -P -a npm 2>/dev/null | grep -v "^$HOME/.claude/bin/" | head -n1)"
if [[ -n "$pnpm_real" && -n "$npm_real" ]]; then
  # npm's dir included explicitly: toolchain managers (mise) install npm and pnpm
  # in separate per-tool dirs, so npm is not implied by pnpm's dirname.
  clean_path="$(dirname "$pnpm_real"):$(dirname "$npm_real"):/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
  pnpm_version="$(PATH="$clean_path" "$pnpm_real" --version)"
  r=$(mkrepo g0pnpmmissing)
  mkdir -p "$r/packages/present" "$r/src"
  printf '%s\n' "{\"private\":true,\"packageManager\":\"pnpm@$pnpm_version\",\"scripts\":{\"test\":\"pnpm --filter @platform-modules/missing test\"}}" > "$r/package.json"
  printf 'packages:\n  - packages/*\n' > "$r/pnpm-workspace.yaml"
  printf '%s\n' '{"name":"@platform-modules/present","version":"1.0.0","scripts":{"test":"node --version"}}' > "$r/packages/present/package.json"
  printf '%s\n' 'export const value = 1;' > "$r/src/a.js"
  git -C "$r" add package.json pnpm-workspace.yaml packages/present/package.json src/a.js
  git -C "$r" commit -q -m "seed"
  printf '%s\n' 'export const value = 2;' > "$r/src/a.js"
  git -C "$r" add src/a.js
  git -C "$r" commit -q -m "feat: change source"

  out=$(PATH="$clean_path" "$GATES" gate0 strict "$r" "$r" "test" 2>&1) && rc=$? || rc=$?
  if [[ $rc -ne 0 ]]; then
    ok "gate0 fails when nested pnpm filter matches no package"
  else
    bad "gate0 fails when nested pnpm filter matches no package" "$out"
  fi
  rm -rf "$r"

  # 1b. the same filtered root script passes when the package exists
  r=$(mkrepo g0pnpmpresent)
  mkdir -p "$r/packages/present" "$r/src"
  printf '%s\n' "{\"private\":true,\"packageManager\":\"pnpm@$pnpm_version\",\"scripts\":{\"test\":\"pnpm --filter @platform-modules/present test\"}}" > "$r/package.json"
  printf 'packages:\n  - packages/*\n' > "$r/pnpm-workspace.yaml"
  printf '%s\n' '{"name":"@platform-modules/present","version":"1.0.0","scripts":{"test":"node --version"}}' > "$r/packages/present/package.json"
  printf '%s\n' 'export const value = 1;' > "$r/src/a.js"
  git -C "$r" add package.json pnpm-workspace.yaml packages/present/package.json src/a.js
  git -C "$r" commit -q -m "seed"
  printf '%s\n' 'export const value = 2;' > "$r/src/a.js"
  git -C "$r" add src/a.js
  git -C "$r" commit -q -m "feat: change source"

  out=$(PATH="$clean_path" "$GATES" gate0 strict "$r" "$r" "test" 2>&1) && rc=$? || rc=$?
  if [[ $rc -eq 0 ]]; then
    ok "gate0 passes when nested pnpm filter matches a package"
  else
    bad "gate0 passes when nested pnpm filter matches a package" "$out"
  fi
  rm -rf "$r"

  # 1c. fail-if-no-match hardening does not break a filterless recursive pnpm script
  r=$(mkrepo g0pnpmrecursive)
  mkdir -p "$r/packages/present" "$r/src"
  printf '%s\n' "{\"private\":true,\"packageManager\":\"pnpm@$pnpm_version\",\"scripts\":{\"test\":\"pnpm -r test\"}}" > "$r/package.json"
  printf 'packages:\n  - packages/*\n' > "$r/pnpm-workspace.yaml"
  printf '%s\n' '{"name":"@platform-modules/present","version":"1.0.0","scripts":{"test":"node --version"}}' > "$r/packages/present/package.json"
  printf '%s\n' 'export const value = 1;' > "$r/src/a.js"
  git -C "$r" add package.json pnpm-workspace.yaml packages/present/package.json src/a.js
  git -C "$r" commit -q -m "seed"
  printf '%s\n' 'export const value = 2;' > "$r/src/a.js"
  git -C "$r" add src/a.js
  git -C "$r" commit -q -m "feat: change source"

  out=$(PATH="$clean_path" "$GATES" gate0 strict "$r" "$r" "test" 2>&1) && rc=$? || rc=$?
  if [[ $rc -eq 0 ]]; then
    ok "gate0 passes for filterless recursive pnpm script"
  else
    bad "gate0 passes for filterless recursive pnpm script" "$out"
  fi
  rm -rf "$r"
else
  bad "pnpm filter gate tests require pnpm" "pnpm unavailable"
fi

# 2. gate0 strict mode fail-closed (no check commands)
r=$(mkrepo g0strictnocmd); commitf "$r" src/a.js "export const a=1" "seed"
base=$(commitf "$r" src/a.js "export const a=2" "feat: bump a")

out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) || true
if grep -q "fail-closed" <<<"$out"; then
  ok "gate0 strict mode fail-closed (no checks discovered)"
else
  # Check that it exited with error code (should fail-closed)
  (
    cd "$r"
    "$GATES" gate0 strict "$r" "$r" "test" >/dev/null 2>&1; echo "exit=$?"
  ) > "$TEST_TMPDIR/gates_exit_test" 2>&1
  if grep -q "exit=1" "$TEST_TMPDIR/gates_exit_test" 2>/dev/null; then
    ok "gate0 strict mode fail-closed (exited with error)"
  else
    bad "gate0 strict mode fail-closed (should fail)" "exit code check failed"
  fi
fi
rm -rf "$r"

# 2b. gate0 strict mode fail-closed (failing check via Makefile)
r=$(mkrepo g0strictfail)
printf '%s\n' 'check:' $'\tfalse' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]]; then
  ok "gate0 strict mode fail-closed (failing check)"
else
  bad "gate0 strict mode fail-closed (failing check)" "$out"
fi
rm -rf "$r"

# 2c. failing check emits a machine-readable trailer (check/rc/tail) and persists the tail file
r=$(mkrepo g0difftrailer)
printf '%s\n' "{\"scripts\": {\"test\": \"printf 'CANARY_FAIL_MARKER_9f3\\\\n'; exit 7\"}}" > "$r/package.json"
git -C "$r" add package.json
git -C "$r" commit -q -m "seed"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "myslug") 2>&1 ) && rc=$? || rc=$?
tail_path="$(printf '%s\n' "$out" | sed -n 's/.*check failed: test rc=7 tail=\(\S*\) FAILCLASS=check-failed.*/\1/p')"
if [[ $rc -ne 0 && -n "$tail_path" && -f "$tail_path" ]] && grep -q "CANARY_FAIL_MARKER_9f3" "$tail_path"; then
  ok "gate0 failing check emits check/rc/tail trailer and persists output tail"
else
  bad "gate0 failing check emits check/rc/tail trailer and persists output tail" "rc=$rc tail_path=${tail_path:-<missing>} out=$out"
fi
rm -rf "$r"

# 2c1. local parse-config timeout is infrastructure failure, not a code failure
r=$(mkrepo g0parseconfigtimeout)
printf '%s\n' '{"scripts": {"test": "node -e \"console.error('"'"'local-gate: parse-config timed out'"'"'); process.exit(1)\""}}' > "$r/package.json"
git -C "$r" add package.json
git -C "$r" commit -q -m "seed"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "myslug") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -Fq 'FAILCLASS=infra'; then
  ok "gate0 classifies local parse-config timeout as infrastructure"
else
  bad "gate0 classifies local parse-config timeout as infrastructure" "$out"
fi
rm -rf "$r"

# 2c. gate0 strict mode fail-closed (empty package.json scripts)
r=$(mkrepo g0strictempty); commitf "$r" package.json '{}' "seed"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -qE "no allowlisted scripts|fail-closed"; then
  ok "gate0 strict mode fail-closed (empty scripts)"
else
  bad "gate0 strict mode fail-closed (empty scripts)" "$out"
fi
rm -rf "$r"

# 2d. gate0 strict accepts git worktree (.git as file)
r=$(mkrepo gitfile)
commitf "$r" package.json '{"scripts":{"test":"node --version"}}' "seed"
wt=$(mktemp -d /tmp/gates-wt-XXXX)
rm -rf "$wt"
git -C "$r" worktree add "$wt" HEAD -q
out=$("$GATES" gate0 strict "$wt" "$r" "test" 2>&1); rc=$?
if [[ $rc -eq 0 ]]; then
  ok "gate0 strict accepts worktree (.git as file)"
else
  bad "gate0 strict accepts worktree (.git as file)" "$out"
fi
git -C "$r" worktree remove "$wt" 2>/dev/null || true
rm -rf "$r" "$wt"

# 2da. gate0 restores tracked VERSION emptied by a prior failed attempt
r=$(mkrepo g0version)
printf '%s\n' '0.1.0' > "$r/VERSION"
printf '%s\n' '{"scripts":{"test":"node --version"}}' > "$r/package.json"
git -C "$r" add VERSION package.json
git -C "$r" commit -q -m "seed"
: > "$r/VERSION"
out=$("$GATES" gate0 strict "$r" "$r" "test" 2>&1); rc=$?
if [[ $rc -eq 0 ]] && [[ "$(cat "$r/VERSION")" == "0.1.0" ]]; then
  ok "gate0 restores empty tracked VERSION before checks"
else
  bad "gate0 restores empty tracked VERSION before checks" "$out"
fi
rm -rf "$r"

# 2e. gate0 strict lockfile-sync rejects manifest drift
# Lockfile generation needs a real npm; not implied by /usr/bin (mise/nvm installs).
npm_lockgen="$(type -P -a npm 2>/dev/null | grep -v "^$HOME/.claude/bin/" | head -n1)"
# The fixture lockfile must be generated the same way gate0 regenerates it. Under the
# ~/.claude/bin shims a node-backed install is re-routed through the local-gate and reshaped
# into a `ci`, which leaves the fixture lockfile stale and the assertion measuring the wrong thing.
lockgen_path="$(printf '%s' "$PATH" | tr ':' '\n' | grep -vx "$HOME/.claude/bin" | paste -sd:)"
r=$(mkrepo g0lockdrift)
mkdir -p "$r/vendor/dep"
printf '%s\n' '{"name":"dep","version":"1.0.0"}' > "$r/vendor/dep/package.json"
printf '%s\n' '{"scripts":{"test":"node --version"},"dependencies":{"dep":"file:vendor/dep"}}' > "$r/package.json"
[[ -n "$npm_lockgen" ]] && (cd "$r" && PATH="$lockgen_path" "$npm_lockgen" install --package-lock-only --offline --ignore-scripts --no-audit --no-fund >/dev/null 2>&1)
if [[ ! -f "$r/package-lock.json" ]]; then
  ok "gate0 strict lockfile-sync rejects manifest drift (skipped: no lockfile-capable npm in PATH)"
  rm -rf "$r"
else
git -C "$r" add package.json package-lock.json vendor/dep/package.json
git -C "$r" commit -q -m "seed"
printf '%s\n' '{"scripts":{"test":"node --version"},"dependencies":{"dep":"file:vendor/dep","dep2":"file:vendor/dep2"}}' > "$r/package.json"
mkdir -p "$r/vendor/dep2"
printf '%s\n' '{"name":"dep2","version":"1.0.0"}' > "$r/vendor/dep2/package.json"
git -C "$r" add package.json vendor/dep2/package.json
git -C "$r" commit -q -m "manifest drift"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && [[ "$(printf '%s\n' "$out" | head -n1)" == "gate0: lockfile-out-of-sync: package.json" ]]; then
  ok "gate0 strict lockfile-sync rejects manifest drift"
else
  bad "gate0 strict lockfile-sync rejects manifest drift" "$out"
fi
rm -rf "$r"
fi

# 2f. gate0 strict lockfile-sync accepts consistent manifest and lockfile
r=$(mkrepo g0locksync)
mkdir -p "$r/vendor/dep" "$r/vendor/dep2"
printf '%s\n' '{"name":"dep","version":"1.0.0"}' > "$r/vendor/dep/package.json"
printf '%s\n' '{"name":"dep2","version":"1.0.0"}' > "$r/vendor/dep2/package.json"
printf '%s\n' '{"scripts":{"test":"node --version"},"dependencies":{"dep":"file:vendor/dep"}}' > "$r/package.json"
[[ -n "$npm_lockgen" ]] && (cd "$r" && PATH="$lockgen_path" "$npm_lockgen" install --package-lock-only --offline --ignore-scripts --no-audit --no-fund >/dev/null 2>&1)
if [[ ! -f "$r/package-lock.json" ]]; then
  ok "gate0 strict lockfile-sync accepts consistent manifest and lockfile (skipped: no lockfile-capable npm in PATH)"
  rm -rf "$r"
else
git -C "$r" add package.json package-lock.json vendor/dep/package.json vendor/dep2/package.json
git -C "$r" commit -q -m "seed"
printf '%s\n' '{"scripts":{"test":"node --version"},"dependencies":{"dep":"file:vendor/dep","dep2":"file:vendor/dep2"}}' > "$r/package.json"
(cd "$r" && PATH="$lockgen_path" "$npm_lockgen" install --package-lock-only --offline --ignore-scripts --no-audit --no-fund >/dev/null 2>&1)
git -C "$r" add package.json package-lock.json
git -C "$r" commit -q -m "manifest and lockfile"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -eq 0 ]]; then
  ok "gate0 strict lockfile-sync accepts consistent manifest and lockfile"
else
  bad "gate0 strict lockfile-sync accepts consistent manifest and lockfile" "$out"
fi
rm -rf "$r"
fi

# 2g. gate0 strict commit-hook rejects staged task diff
r=$(mkrepo g0hookfail); commitf "$r" package.json '{"scripts":{"test":"node --version"}}' "seed"
use_repo_hooks "$r"
printf '%s\n' '#!/usr/bin/env bash' 'echo hook says no >&2' 'exit 7' > "$r/.git/hooks/pre-commit"
chmod +x "$r/.git/hooks/pre-commit"
printf '%s\n' 'changed' > "$r/a.txt"
git -C "$r" add a.txt
git -C "$r" commit -q --no-verify -m "task"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && [[ "$(printf '%s\n' "$out" | head -n1)" == "gate0: pre-commit-hook-rejected: exit 7" ]] && printf '%s\n' "$out" | grep -q "hook says no"; then
  ok "gate0 strict commit-hook rejects staged task diff"
else
  bad "gate0 strict commit-hook rejects staged task diff" "$out"
fi
rm -rf "$r"

# 2h. gate0 strict commit-hook accepts passing hook
r=$(mkrepo g0hookpass); commitf "$r" package.json '{"scripts":{"test":"node --version"}}' "seed"
use_repo_hooks "$r"
printf '%s\n' '#!/usr/bin/env bash' 'git diff --cached --name-only | grep -q a.txt' > "$r/.git/hooks/pre-commit"
chmod +x "$r/.git/hooks/pre-commit"
printf '%s\n' 'changed' > "$r/a.txt"
git -C "$r" add a.txt
git -C "$r" commit -q --no-verify -m "task"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -eq 0 ]]; then
  ok "gate0 strict commit-hook accepts passing hook"
else
  bad "gate0 strict commit-hook accepts passing hook" "$out"
fi
rm -rf "$r"

# 2h2. gate0 strict commit-hook restores worktree after rejecting mutating hook
r=$(mkrepo g0hookrestore); commitf "$r" package.json '{"scripts":{"test":"node --version"}}' "seed"
commitf "$r" tracked-file "original" "tracked seed" >/dev/null
use_repo_hooks "$r"
printf '%s\n' '#!/usr/bin/env bash' 'echo mutated >> tracked-file' 'echo created > hook-created-file' 'exit 1' > "$r/.git/hooks/pre-commit"
chmod +x "$r/.git/hooks/pre-commit"
printf '%s\n' 'changed' > "$r/tracked-file"
git -C "$r" add tracked-file
git -C "$r" commit -q --no-verify -m "task"
before=$(cat "$r/tracked-file")
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
after=$(cat "$r/tracked-file")
if [[ $rc -ne 0 ]] \
  && [[ "$(printf '%s\n' "$out" | head -n1)" == "gate0: pre-commit-hook-rejected: exit 1" ]] \
  && [[ "$after" == "$before" ]] \
  && [[ ! -e "$r/hook-created-file" ]]; then
  ok "gate0 strict commit-hook restores worktree after rejecting mutating hook"
else
  bad "gate0 strict commit-hook restores worktree after rejecting mutating hook" "$out"
fi
rm -rf "$r"

# 2h3. gate0 strict commit-hook preserves unstaged tracked edits while reverting hook mutations
r=$(mkrepo g0hookdirty); commitf "$r" package.json '{"scripts":{"test":"node --version"}}' "seed"
commitf "$r" tracked-file "original" "tracked seed" >/dev/null
commitf "$r" hook-target "target original" "hook target seed" >/dev/null
use_repo_hooks "$r"
printf '%s\n' '#!/usr/bin/env bash' 'echo hook mutation > hook-target' 'echo created > hook-created-file' 'exit 1' > "$r/.git/hooks/pre-commit"
chmod +x "$r/.git/hooks/pre-commit"
printf '%s\n' 'committed task change' > "$r/tracked-file"
git -C "$r" add tracked-file
git -C "$r" commit -q --no-verify -m "task"
printf '%s\n' 'legitimate unstaged edit' > "$r/tracked-file"
before=$(cat "$r/tracked-file")
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
after=$(cat "$r/tracked-file")
hook_target_after=$(cat "$r/hook-target")
if [[ $rc -ne 0 ]] \
  && [[ "$(printf '%s\n' "$out" | head -n1)" == "gate0: pre-commit-hook-rejected: exit 1" ]] \
  && [[ "$after" == "$before" ]] \
  && [[ "$hook_target_after" == "target original" ]] \
  && [[ ! -e "$r/hook-created-file" ]]; then
  ok "gate0 strict commit-hook preserves unstaged tracked edits while reverting hook mutations"
else
  bad "gate0 strict commit-hook preserves unstaged tracked edits while reverting hook mutations" "$out"
fi
rm -rf "$r"

# 2i. gate0 strict no-hook repo output unchanged
r=$(mkrepo g0nohook); commitf "$r" package.json '{"scripts":{"test":"node --version"}}' "seed"
printf '%s\n' 'changed' > "$r/a.txt"
git -C "$r" add a.txt
git -C "$r" commit -q -m "task"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -eq 0 ]] && ! printf '%s\n' "$out" | grep -q "lockfile-out-of-sync\|pre-commit-hook"; then
  ok "gate0 strict no-hook repo output unchanged"
else
  bad "gate0 strict no-hook repo output unchanged" "$out"
fi
rm -rf "$r"

# 3. gate0 strict mode fail-closed (not a git repo)
{
  notgit=$(mktemp -d /tmp/gates-notgit-XXXX)
  (
    cd "$notgit"
    echo "not a repo" > README.md
    "$GATES" gate0 strict "$notgit" "$notgit" "test" 2>&1
  ) > "$TEST_TMPDIR/gates_notgit_test" 2>&1
  if [[ $? -ne 0 ]]; then
    ok "gate0 strict mode fail-closed (not a git repo)"
  else
    bad "gate0 strict mode fail-closed (not a git repo)" "should have failed"
  fi
} 2>&1

# ==== gate0 baseline-ratchet mode tests ===

echo

# 4. gate0 baseline-ratchet mode success (Makefile discovered)
r=$(mkrepo g0baseratchet); commitf "$r" Makefile 'test:
	node --version' "seed"
base=$(commitf "$r" src/a.js "export const a=1" "seed")
head=$(commitf "$r" src/a.js "export const a=2" "feat: bump a")

(
  cd "$r"
  "$GATES" gate0 baseline-ratchet-init "$r" "$r" "test" 2>/dev/null
) > "$TEST_TMPDIR/gates_baselines_init_output" 2>&1
if [[ $? -eq 0 ]]; then
  (
    cd "$r"
    "$GATES" gate0 baseline-ratchet "$r" "$r" "test" 2>/dev/null
  ) > "$TEST_TMPDIR/gates_baselines_test_output" 2>&1
  if [[ $? -eq 0 ]]; then
    ok "gate0 baseline-ratchet mode with Makefile (success)"
  else
    bad "gate0 baseline-ratchet mode with Makefile (should succeed after init)" "$out"
  fi
else
  bad "gate0 baseline-ratchet-init mode with Makefile (should succeed)" "$out"
fi
rm -rf "$r"

# 4b. gate0 baseline-ratchet mode fail-closed (missing baseline file)
r=$(mkrepo g0basemissing); commitf "$r" Makefile 'test:
	node --version' "seed"
out=$( (cd "$r" && "$GATES" gate0 baseline-ratchet "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q "fail-closed.*baseline file not found"; then
  ok "gate0 baseline-ratchet mode fail-closed (missing baseline)"
else
  bad "gate0 baseline-ratchet mode fail-closed (missing baseline)" "$out"
fi
rm -rf "$r"

# 5. gate0 baseline-ratchet mode fail-closed (unknown mode)
r=$(mkrepo g0baserad); commitf "$r" package.json '{}' "seed"

out=$( (cd "$r" && "$GATES" gate0 invalidmode "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q "unknown mode"; then
  ok "gate0 baseline-ratchet mode fail-closed (unknown mode)"
else
  bad "gate0 baseline-ratchet mode fail-closed (unknown mode)" "should fail with message"
fi
rm -rf "$r"

# 6. gate0 baseline-ratchet mode fail-closed (minimum args)
out=$( ("$GATES" gate0 strict) 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q "Usage:"; then
  ok "gate0 strict mode fail-closed (minimum args)"
else
  bad "gate0 strict mode fail-closed (minimum args)" "should show usage"
fi

# ==== gate0 risk mode tests ===

echo

# 7. gate0 risk mode success (risk-router.sh found)
r=$(mkrepo g0risk); commitf "$r" src/a.js "export const a=1" "seed"
base=$(git -C "$r" rev-parse HEAD)
# Make a commit that should result in low risk
commitf "$r" src/a.js "export const a=2" "feat: low risk change"
head=$(git -C "$r" rev-parse HEAD)

(
  cd "$r"
  "$GATES" risk "$r" "$base..$head" 2>/dev/null
) > "$TEST_TMPDIR/gates_risk_test_output" 2>&1
if [[ $? -eq 0 ]]; then
  ok "gate0 risk mode (success)"
else
  bad "gate0 risk mode (should succeed)" "$out"
fi
rm -rf "$r"

# 8. gate0 risk mode fail-closed (worktree not a git repo)
{
  notgit=$(mktemp -d /tmp/gates-risk-notgit-XXXX)
  (
    cd "$notgit"
    echo "not a repo" > README.md
    "$GATES" risk "$notgit" "head..head" 2>&1
  ) > "$TEST_TMPDIR/gates_risk_notgit_test" 2>&1
  if [[ $? -ne 0 ]]; then
    ok "gate0 risk mode fail-closed (not a git repo)"
  else
    bad "gate0 risk mode fail-closed (not a git repo)" "should have failed"
  fi
} 2>&1

# 9. gate0 risk mode fail-closed (invalid range)
r=$(mkrepo g0riskrange); commitf "$r" src/a.js "export const a=1" "seed"
valid_sha=$(git -C "$r" rev-parse HEAD)

out=$( (cd "$r" && "$GATES" risk "$r" "invalid..range") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q "REASON: bad base"; then
  ok "gate0 risk mode fail-closed (invalid range)"
else
  bad "gate0 risk mode fail-closed (invalid range)" "should fail with message"
fi
rm -rf "$r"

# 9b. gate0 risk mode fail-closed (bare SHA, no ..)
r=$(mkrepo g0riskbare); commitf "$r" src/a.js "export const a=1" "seed"
bare_sha=$(git -C "$r" rev-parse HEAD)

out=$( (cd "$r" && "$GATES" risk "$r" "$bare_sha") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -eq 2 ]] && printf '%s\n' "$out" | grep -q "RISK=ERROR"; then
  ok "gate0 risk mode fail-closed (bare SHA range)"
else
  bad "gate0 risk mode fail-closed (bare SHA range)" "$out"
fi
rm -rf "$r"

# 10. gate0 risk mode fail-closed (missing command)
out=$( ("$GATES" invalidcommand /tmp /tmp test) 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q "unknown command"; then
  ok "gate0 risk mode fail-closed (invalid command)"
else
  bad "gate0 risk mode fail-closed (invalid command)" "should fail with message"
fi

# ==== lib/risk-router.sh validation ===

echo

# 11. risk-router.sh help/usage validation (should show usage for missing args)
r=$(mkrepo riskrouter); commitf "$r" src/a.js "export const a=1" "seed"

out=$( (cd "$r" && "$RISCER") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -qi "usage:"; then
  ok "risk-router.sh help/usage validation"
else
  bad "risk-router.sh help/usage validation" "should show usage for missing args"
fi
rm -rf "$r"

# 12. risk-router.sh success path (basic risk scan)
r=$(mkrepo riskbasic); commitf "$r" src/a.js "export const a=1" "seed"
base=$(git -C "$r" rev-parse HEAD)
commitf "$r" src/a.js "export const a=2" "feat: safe change"
head=$(git -C "$r" rev-parse HEAD)

(
  cd "$r"
  "$RISCER" "$r" "$base..$head"
) > "$TEST_TMPDIR/risk_router_test" 2>&1
if [[ $? -eq 0 ]]; then
  ok "risk-router.sh basic risk scan"
else
  bad "risk-router.sh basic risk scan" "exit code: $?"
fi
rm -rf "$r"

# ==== gates.sh risk subcommand integration ===

echo

# 13. gates.sh risk subcommand with empty diff (should output LOW)
r=$(mkrepo risksempty); commitf "$r" src/a.js "export const a=1" "seed"
head=$(git -C "$r" rev-parse HEAD)

out=$( (cd "$r" && "$GATES" risk "$r" "$head..$head") 2>&1 ) || true
if grep -q "RISK=LOW" <<<"$out"; then
  ok "gates.sh risk subcommand LOW on empty diff"
else
  bad "gates.sh risk subcommand LOW on empty diff" "should output LOW"
fi
rm -rf "$r"

# 14. gates.sh risk subcommand with money sink (HIGH risk)
r=$(mkrepo riskmoney); commitf "$r" src/credits.js "export const price=99" "seed"
base=$(git -C "$r" rev-parse HEAD)
echo "const total = paymentAmount + 'charge'" > "$r/payment.js"
git -C "$r" add payment.js
git -C "$r" commit -q -m "feat: payment processing"
head=$(git -C "$r" rev-parse HEAD)

out=$( (cd "$r" && "$GATES" risk "$r" "$base..$head") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -eq 0 ]] && printf '%s\n' "$out" | grep -q "RISK=HIGH"; then
  ok "gates.sh risk subcommand HIGH on money sink"
else
  bad "gates.sh risk subcommand HIGH on money sink" "should detect HIGH risk"
fi
rm -rf "$r"

# ==== end-to-end gates.sh integration ===
echo

# 2e. gate0 strict mode: non-string script body is rejected (JSON schema, not security)
r=$(mkrepo g0strictnonstring); commitf "$r" package.json '{"scripts": {"test": 42}}' "seed"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q "fail-closed"; then
  ok "gate0 strict mode fail-closed (non-string script body)"
else
  bad "gate0 strict mode fail-closed (non-string script body)" "$out"
fi
rm -rf "$r"

# 2f. gate0 strict mode: Makefile target with allowlisted name is discovered and run
r=$(mkrepo g0makeok)
printf '%s\n' 'test:' $'\tnode --version' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -eq 0 ]]; then
  ok "gate0 strict mode Makefile target discovered and run"
else
  bad "gate0 strict mode Makefile target discovered and run" "$out"
fi
rm -rf "$r"

# 2g. gate0 strict mode: Makefile failing recipe fails gate
r=$(mkrepo g0makefail)
printf '%s\n' 'test:' $'\tfalse' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]]; then
  ok "gate0 strict mode fail-closed (failing Makefile recipe)"
else
  bad "gate0 strict mode fail-closed (failing Makefile recipe)" "$out"
fi
rm -rf "$r"

# 2h intentionally removed (was: unsafe Makefile recipe — wrong threat model)
# gate0 runs the repo's own test suite; body/recipe content is not validated.
if true; then
  ok "gate0 scope: body/recipe validation out of scope (gate0 runs repo own tests)"
else
  bad "gate0 strict mode fail-closed (unsafe Makefile recipe)" "$out"
fi
rm -rf "$r"

# 2j. gate0 strict: Makefile target with prerequisites runs (make handles prerequisites natively)
r=$(mkrepo g0makeprereq)
printf '%s\n' 'test: build' $'\tnode --version' 'build:' $'\techo build' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -eq 0 ]]; then
  ok "gate0 strict Makefile target with prerequisites runs (make handles prereqs)"
else
  bad "gate0 strict Makefile target with prerequisites" "$out"
fi
rm -rf "$r"

# 2k. gate0 strict: Makefile failing recipe fails gate
r=$(mkrepo g0makefailrec)
printf '%s\n' 'test:' $'\tfalse' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]]; then
  ok "gate0 strict fail-closed (Makefile recipe returns nonzero)"
else
  bad "gate0 strict fail-closed (Makefile recipe returns nonzero)" "$out"
fi
rm -rf "$r"

# 2l. gate0 strict: non-allowlisted Makefile target not discovered
r=$(mkrepo g0makenonallow)
printf '%s\n' 'publish:' $'\techo pub' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed"
out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q "fail-closed\|no allowlisted"; then
  ok "gate0 strict fail-closed (non-allowlisted Makefile target not run)"
else
  bad "gate0 strict fail-closed (non-allowlisted Makefile target)" "$out"
fi
rm -rf "$r"

# 15. Full gates.sh flow validation (all subcommand arguments properly processed)
r=$(mkrepo gate0flow); commitf "$r" package.json '{"scripts": {"test": "node --version"}}' "seed"
base=$(git -C "$r" rev-parse HEAD)

out=$( (cd "$r" && "$GATES" gate0 strict "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -eq 0 ]]; then
  ok "full gates.sh flow: proper argument processing and success"
else
  bad "full gates.sh flow validation" "$out"
fi
rm -rf "$r"

# ==== baseline-ratchet fail-closed regression tests ===

echo

# Test A: empty normalized output when baseline had lines is detected as regression
r=$(mkrepo g0baseemptyout)
printf '%s\n' 'test:' $'\techo "ERROR: something broke" >&2; false' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed with error output"
(cd "$r" && "$GATES" gate0 baseline-ratchet-init "$r" "$r" "test" 2>/dev/null)
printf '%s\n' 'test:' $'\tfalse' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "silent failure"
out=$( (cd "$r" && "$GATES" gate0 baseline-ratchet "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]]; then
  ok "baseline-ratchet: regression on empty output when baseline had lines"
else
  bad "baseline-ratchet: regression on empty output when baseline had lines" "$out"
fi
rm -rf "$r"

# Test B: new output lines when baseline was empty is detected as regression
r=$(mkrepo g0basenewout)
printf '%s\n' 'test:' $'\tfalse' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed silent failure"
(cd "$r" && "$GATES" gate0 baseline-ratchet-init "$r" "$r" "test" 2>/dev/null)
printf '%s\n' 'test:' $'\techo "NEW ERROR: appeared" >&2; false' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "failure with new output"
out=$( (cd "$r" && "$GATES" gate0 baseline-ratchet "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]]; then
  ok "baseline-ratchet: regression on new output lines when baseline was empty"
else
  bad "baseline-ratchet: regression on new output lines when baseline was empty" "$out"
fi
rm -rf "$r"

# Test C: same failure lines still pass (ratchet semantics preserved)
r=$(mkrepo g0basesamefail)
printf '%s\n' 'test:' $'\techo "FIXED ERROR: same" >&2; false' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed fixed error"
(cd "$r" && "$GATES" gate0 baseline-ratchet-init "$r" "$r" "test" 2>/dev/null)
out=$( (cd "$r" && "$GATES" gate0 baseline-ratchet "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -eq 0 ]]; then
  ok "baseline-ratchet: same failure lines pass (no regression)"
else
  bad "baseline-ratchet: same failure lines pass (no regression)" "$out"
fi
rm -rf "$r"

# Test D: disappeared baseline check (removed target, other passing check remains)
r=$(mkrepo g0basedisappeared)
printf '%s\n' 'test:' $'\tfalse' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed failing test"
(cd "$r" && "$GATES" gate0 baseline-ratchet-init "$r" "$r" "test" 2>/dev/null)
printf '%s\n' 'lint:' $'\ttrue' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "remove test, add passing lint"
out=$( (cd "$r" && "$GATES" gate0 baseline-ratchet "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -qiE 'disappeared|regression'; then
  ok "baseline-ratchet: fail-closed when baseline check disappears"
else
  bad "baseline-ratchet: fail-closed when baseline check disappears" "$out"
fi
rm -rf "$r"

# Test E: renamed baseline check (test renamed to lint, baseline still has test)
r=$(mkrepo g0baserenamed)
printf '%s\n' 'test:' $'\tfalse' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "seed failing test"
(cd "$r" && "$GATES" gate0 baseline-ratchet-init "$r" "$r" "test" 2>/dev/null)
printf '%s\n' 'lint:' $'\ttrue' > "$r/Makefile"
git -C "$r" add Makefile
git -C "$r" commit -q -m "rename test to lint"
out=$( (cd "$r" && "$GATES" gate0 baseline-ratchet "$r" "$r" "test") 2>&1 ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -qiE 'disappeared|regression'; then
  ok "baseline-ratchet: fail-closed when baseline check is renamed away"
else
  bad "baseline-ratchet: fail-closed when baseline check is renamed away" "$out"
fi
rm -rf "$r"

eval "$(sed -n '/^failing_test_signature() {/,/^}/p' "$GATES")"

bun_output=$'f.test.ts:\nerror: expect(received).toBe(expected)\n(fail) Grp > boom case [0.08ms]\n(fail) Hooked > hooktimeout case [5001.17ms]\n  ^ a beforeEach/afterEach hook timed out for this test.\n\n 0 pass\n 2 fail'
out=$(failing_test_signature "$bun_output")
if [[ "$out" == $'Grp > boom case\nHooked > hooktimeout case' ]]; then
  ok "failing_test_signature: bun (fail) lines attribute without their timings"
else
  bad "failing_test_signature: bun (fail) lines attribute without their timings" "$out"
fi

pnpm_output=$'Scope: 5 of 6 workspace projects\ncontroller test$ bun test\nspine test:  25 pass\ncontroller test: (fail) ClusterScheduler > spills only when all builders are overloaded [5648.16ms]\npackages/deck-ui test: FAIL src/Chip.test.tsx > renders\n[ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL] overdeck-controller@0.1.0 test: `bun test`'
out=$(failing_test_signature "$pnpm_output")
if [[ "$out" == $'ClusterScheduler > spills only when all builders are overloaded\nsrc/Chip.test.tsx > renders' ]]; then
  ok "failing_test_signature: pnpm -r package prefix does not hide the failure"
else
  bad "failing_test_signature: pnpm -r package prefix does not hide the failure" "$out"
fi

eval "$(sed -n '/^retry_check_isolated() {/,/^}/p' "$GATES")"
capture_check_record() { printf '%s' "${PNPM_CONFIG_WORKSPACE_CONCURRENCY:-unset}" > "$TEST_TMPDIR/retry-env"; return 0; }
retry_check_isolated $'NPM_SCRIPT\ttest' /nonexistent "$bun_output" >/dev/null 2>&1
out=$(<"$TEST_TMPDIR/retry-env")
if [[ "$out" == "1" && -z "${PNPM_CONFIG_WORKSPACE_CONCURRENCY:-}" ]]; then
  ok "retry_check_isolated: pins workspace fan-out to one package, then unsets it"
else
  bad "retry_check_isolated: pins workspace fan-out to one package, then unsets it" "$out"
fi
unset -f capture_check_record

# ==== docs-only scoping ====
# Fixture test script is `false`: any run of the discovered checks fails, so a
# zero exit proves the checks were skipped rather than passing by luck.
echo

mkscoperepo() {
  local r; r=$(mkrepo g0docscope)
  printf '%s\n' '{"scripts": {"test": "false"}}' > "$r/package.json"
  mkdir -p "$r/src"
  printf '%s\n' 'export const a=1' > "$r/src/a.js"
  git -C "$r" add -A; git -C "$r" commit -q -m seed
  git -C "$r" branch "plan/scope"
  printf '%s' "$r"
}

r=$(mkscoperepo)
commitf "$r" docs/note.md "hello" "docs: note" >/dev/null
out=$("$GATES" gate0 strict "$r" "$r" "scope" 2>&1); rc=$?
if [[ $rc -eq 0 && "$out" == *"documentation-only"* ]]; then
  ok "gate0 skips code checks for a docs-only diff"
else
  bad "gate0 skips code checks for a docs-only diff" "rc=$rc $out"
fi
rm -rf "$r"

r=$(mkscoperepo)
commitf "$r" src/a.js "export const a=2" "feat: bump" >/dev/null
out=$("$GATES" gate0 strict "$r" "$r" "scope" 2>&1); rc=$?
if [[ $rc -ne 0 && "$out" != *"documentation-only"* ]]; then
  ok "gate0 runs checks for a code-only diff"
else
  bad "gate0 runs checks for a code-only diff" "rc=$rc $out"
fi
rm -rf "$r"

r=$(mkscoperepo)
printf '%s\n' 'export const a=2' > "$r/src/a.js"
printf '%s\n' 'hello' > "$r/README.md"
git -C "$r" add -A; git -C "$r" commit -q -m "mixed"
out=$("$GATES" gate0 strict "$r" "$r" "scope" 2>&1); rc=$?
if [[ $rc -ne 0 && "$out" != *"documentation-only"* ]]; then
  ok "gate0 runs checks for a mixed docs+code diff"
else
  bad "gate0 runs checks for a mixed docs+code diff" "rc=$rc $out"
fi
rm -rf "$r"

# Code in an earlier commit of the branch: classification must span the whole
# branch, never just HEAD^.
r=$(mkscoperepo)
commitf "$r" src/a.js "export const a=3" "feat: bump" >/dev/null
commitf "$r" docs/note.md "hello" "docs: note" >/dev/null
out=$("$GATES" gate0 strict "$r" "$r" "scope" 2>&1); rc=$?
if [[ $rc -ne 0 && "$out" != *"documentation-only"* ]]; then
  ok "gate0 runs checks when code changed in an earlier branch commit"
else
  bad "gate0 runs checks when code changed in an earlier branch commit" "rc=$rc $out"
fi
rm -rf "$r"

echo
echo "PASS=$PASS FAIL=$FAIL"
if [[ "$FAIL" -eq 0 ]]; then
  echo "All tests passed!"
  exit 0
else
  echo "Some tests failed!"
  exit 1
fi
