#!/usr/bin/env bash
# Regression tests for bin/_git-guard-shim.sh (installed as bin/git) — the PATH shim
# that gives codex (`cdx exec`) and cursor-agent the same main-checkout protection
# Claude Code gets from hooks/main-checkout-guard.sh. Neither runtime reads Claude's
# PreToolUse hooks; both resolve `git` via PATH, which is how this is enforced for
# every runtime at one choke point instead of duplicating the check per-runtime.
#
# The shim only acts when AGENT_BUILD_SCOPE_ACTIVE=1 (set unconditionally by
# _agent-build-scope for every claude/codex/cursor-agent launch) — a human typing
# `git restore` in their own shell never has it set, so manual use is untouched.
#
# The "real" git in these tests logs every invocation it receives to GIT_CALL_LOG
# before delegating to the actual system git, so a test can assert whether the
# command that was TYPED actually reached git (allowed) or never did (blocked).
# Run: bash git-guard-shim.test.sh (exit 0 = all pass).
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
GIT_SHIM="$ROOT/bin/git"
PASS=0; FAIL=0
ok()  { PASS=$((PASS+1)); printf 'PASS %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf 'FAIL %s\n     %s\n' "$1" "$2"; }

TMP=$(mktemp -d "${TMPDIR:-/tmp}/ggs-test-XXXX")
trap 'rm -rf "$TMP"' EXIT

# Resolved with a PATH that excludes the shim dirs: a bare `command -v git` in an
# agent shell returns ~/.claude/bin/git (the shim), and a stub that execs a shim
# makes the shim under test fork between the two copies without bound.
REALGIT="$(PATH=/usr/bin:/bin command -v git)"
if [[ -z "$REALGIT" ]]; then
  echo "no system git in /usr/bin:/bin — cannot build test fixture" >&2
  exit 1
fi
mkdir -p "$TMP/realbin"
cat > "$TMP/realbin/git" <<EOF
#!/usr/bin/env bash
echo "CALL \$*" >> "\$GIT_CALL_LOG"
exec "$REALGIT" "\$@"
EOF
chmod +x "$TMP/realbin/git"

# The shim resolves git ONLY from the pin install-git-guard-real writes under $HOME —
# it never searches PATH — so every case below runs against a temp HOME whose pin
# names the logging stub.
FAKE_HOME="$TMP/home"
mkdir -p "$FAKE_HOME/.local/state/overdeck/shim-real"
printf '%s\n' "$TMP/realbin/git" > "$FAKE_HOME/.local/state/overdeck/shim-real/git"

REPO="$TMP/repo"
mkdir -p "$REPO"
git -C "$REPO" init -q
git -C "$REPO" config user.email t@t; git -C "$REPO" config user.name t; git -C "$REPO" config commit.gpgsign false
echo x > "$REPO/f"; git -C "$REPO" add -A; git -C "$REPO" commit -qm init
WT="$REPO/.worktrees/wt1"; mkdir -p "$WT"

LOG="$TMP/calls.log"

# run <dir> <agent_active:0|1> <git-args...> -> exit code; sets $LAST_LOG (last
# logged invocation) and $LAST_ERR (stderr) as globals.
run() {
  local dir="$1" active="$2"; shift 2
  : > "$LOG"
  if [[ "$active" == 1 ]]; then
    ( cd "$dir" && env PATH="/usr/bin:/bin" HOME="$FAKE_HOME" GIT_CALL_LOG="$LOG" AGENT_BUILD_SCOPE_ACTIVE=1 bash "$GIT_SHIM" "$@" )
  else
    ( cd "$dir" && env PATH="/usr/bin:/bin" HOME="$FAKE_HOME" GIT_CALL_LOG="$LOG" bash "$GIT_SHIM" "$@" )
  fi >"$TMP/out" 2>"$TMP/err"
  local rc=$?
  LAST_ERR="$(cat "$TMP/err")"
  LAST_LOG="$(tail -1 "$LOG" 2>/dev/null || true)"
  return $rc
}

# A. Destructive verb inside the main checkout, agent-scoped: BLOCKED — the real
#    git binary is never invoked with the destructive command (only the shim's own
#    internal rev-parse classification call is logged).
run "$REPO" 1 restore .
rc=$?
if [[ $rc -eq 77 && "$LAST_LOG" != *"CALL restore ."* && "$LAST_ERR" == *"git-guard"* ]]; then
  ok "main-checkout restore blocked (agent-scoped)"
else
  bad "main-checkout restore blocked" "rc=$rc log=$LAST_LOG err=$LAST_ERR"
fi

# B. Same command, NOT agent-scoped (simulates a human typing it by hand): ALLOWED.
run "$REPO" 0 status
[[ $? -eq 0 && "$LAST_LOG" == "CALL status" ]] \
  && ok "human-invoked (no AGENT_BUILD_SCOPE_ACTIVE) passes through" \
  || bad "human-invoked passthrough" "rc=$? log=$LAST_LOG"

# C. Destructive verb inside a WORKTREE, agent-scoped: ALLOWED (git-guard only
#    protects the main checkout; per-worktree ownership is worktree-lock-gate's job).
run "$WT" 1 reset --hard
[[ $? -eq 0 && "$LAST_LOG" == "CALL reset --hard" ]] \
  && ok "destructive verb in worktree allowed" \
  || bad "destructive in worktree allowed" "rc=$? log=$LAST_LOG"

# D. Read-only git inside the main checkout, agent-scoped: ALLOWED.
run "$REPO" 1 status
[[ $? -eq 0 && "$LAST_LOG" == "CALL status" ]] \
  && ok "read-only git in main checkout allowed" \
  || bad "read-only allowed" "rc=$? log=$LAST_LOG"

# E. `-C <main_root>` from inside a worktree still resolves to the main checkout —
#    an agent cannot dodge the guard by redirecting via -C instead of `cd`.
run "$WT" 1 -C "$REPO" restore .
rc=$?
[[ $rc -eq 77 && "$LAST_ERR" == *"git-guard"* ]] \
  && ok "-C redirect into main checkout blocked" \
  || bad "-C redirect blocked" "rc=$rc err=$LAST_ERR"

# F. A repo that never opted into worktree isolation (no .worktrees dir) is untouched.
REPO2="$TMP/repo2"; mkdir -p "$REPO2"; git -C "$REPO2" init -q
git -C "$REPO2" config user.email t@t; git -C "$REPO2" config user.name t; git -C "$REPO2" config commit.gpgsign false
echo x > "$REPO2/f"; git -C "$REPO2" add -A; git -C "$REPO2" commit -qm init
run "$REPO2" 1 restore .
[[ $? -eq 0 && "$LAST_LOG" == "CALL restore ." ]] \
  && ok "non-opted-in repo left alone" \
  || bad "non-opted-in repo left alone" "rc=$? log=$LAST_LOG"

# G. Static half — safe anywhere: the shim carries the marker its installer greps for,
#    and it iterates no PATH looking for its own name.
grep -qF 'OD_PATH_SHIM_MARKER' "$GIT_SHIM" && grep -qF 'GIT_GUARD_SHIM_MARKER' "$GIT_SHIM" \
  && ! grep -qE 'read -ra .*<<<.*PATH' "$GIT_SHIM" \
  && ok "shim carries the install-time marker and searches no PATH" \
  || bad "shim carries the install-time marker and searches no PATH" "marker missing or PATH iteration present in $GIT_SHIM"

# H. Missing pin: refuse, name the installer. No resolution is attempted, so this is
#    non-forking and safe anywhere.
( cd "$REPO" && env PATH="/usr/bin:/bin" HOME="$TMP/nohome" AGENT_BUILD_SCOPE_ACTIVE=1 \
    bash "$GIT_SHIM" status ) >"$TMP/out" 2>"$TMP/err"
rc=$?
[[ $rc -eq 78 && "$(cat "$TMP/err")" == *install-git-guard-real* ]] \
  && ok "no pin: exits 78 naming the installer" \
  || bad "no pin: exits 78 naming the installer" "rc=$rc err=$(cat "$TMP/err")"

# I. Sabotaged resolution — the pin points at a copy of the shim, the exact shape that
#    forked without bound before. The re-entry bound must stop it. Forks by design, so
#    it runs ONLY in the disposable dangerlab guest. See "Projects/0 DOCS/dangerlab.md":
#      ssh debian1 '/home/user/dangerlab/dangerlab-run --payload <dir> --timeout 300 \
#                     -- bash claude/tests/git-guard-shim.test.sh'
if [[ "$(hostname 2>/dev/null)" != dangerlab ]]; then
  printf 'SKIP sabotaged-pin re-entry regression (dangerlab only — never on a workstation)\n'
else
  SAB_HOME="$TMP/sabhome"
  mkdir -p "$SAB_HOME/.local/state/overdeck/shim-real" "$TMP/fakeshim/bin" "$TMP/fakeshim/lib"
  cp "$GIT_SHIM" "$TMP/fakeshim/bin/git"; chmod +x "$TMP/fakeshim/bin/git"
  cp "$ROOT/lib/shim-guard.sh" "$TMP/fakeshim/lib/shim-guard.sh"
  printf '%s\n' "$TMP/fakeshim/bin/git" > "$SAB_HOME/.local/state/overdeck/shim-real/git"
  BEFORE=$(ps -u "$(id -u)" --no-headers 2>/dev/null | wc -l)
  NPROC_CAP=$(( BEFORE + 150 ))
  (
    ulimit -u "$NPROC_CAP" 2>/dev/null
    cd "$REPO" && exec timeout 20 env PATH="/usr/bin:/bin" HOME="$SAB_HOME" \
      AGENT_BUILD_SCOPE_ACTIVE=1 bash "$GIT_SHIM" status
  ) >"$TMP/out" 2>"$TMP/err"
  rc=$?
  AFTER=$(ps -u "$(id -u)" --no-headers 2>/dev/null | wc -l)
  [[ $rc -eq 79 && "$(cat "$TMP/err")" == *"re-entered at depth"* && $((AFTER - BEFORE)) -lt 20 ]] \
    && ok "sabotaged pin exits 79 at the re-entry bound (no fork loop)" \
    || bad "sabotaged pin exits 79 at the re-entry bound" "rc=$rc procs=$BEFORE->$AFTER err=$(cat "$TMP/err")"
fi

echo
echo "PASS=$PASS FAIL=$FAIL"
[[ "$FAIL" -eq 0 ]]
