#!/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).
# Section I sabotages the pin, which on a regressed shim forks without bound, so the
# whole file is lab-only: bash tests/run-in-lab.sh git-guard-shim.test.sh
set -uo pipefail
. "$(dirname "$0")/lib/danger-guard.sh" 2>/dev/null || true
command -v require_danger_lab >/dev/null || {
  printf 'REFUSED: %s is exhaustion-class and lib/danger-guard.sh did not load, so it cannot verify its host.\n' \
    "$(basename -- "$0")" >&2
  exit 70
}
require_danger_lab "$0"
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. `-C <deploy_root>` from anywhere is blocked by the shim's deploy-root check.
DEPLOY="$TMP/deploy"; mkdir -p "$DEPLOY/.git/hooks"; git -C "$DEPLOY" init -q
export OVERDECK_DEPLOY_DIR="$DEPLOY"

run "$WT" 1 -C "$DEPLOY" clean -fd
rc=$?
[[ $rc -eq 77 && "$LAST_ERR" == *"LIVE deploy clone"* ]] \
  && ok "destructive -C deploy-root command blocked" \
  || bad "destructive -C deploy-root blocked" "rc=$rc err=$LAST_ERR"

# G. `-C <symlink-to-deploy>` is resolved before comparison; symlinks are no longer bypass.
mkdir -p "$DEPLOY/modules/workstation/claude/hooks" "$FAKE_HOME/.claude"
ln -s "$DEPLOY/modules/workstation/claude/hooks" "$FAKE_HOME/.claude/hooks-link"
run "$WT" 1 --git-dir="$DEPLOY/.git" --work-tree="$DEPLOY" clean -fd
rc=$?
[[ $rc -eq 77 && "$LAST_ERR" == *"LIVE deploy clone"* ]] \
  && ok "destructive --work-tree= deploy-root command blocked" \
  || bad "destructive --work-tree= deploy-root blocked" "rc=$rc err=$LAST_ERR"

run "$WT" 1 --work-tree "$REPO" reset --hard
rc=$?
[[ $rc -eq 77 && "$LAST_ERR" == *"SHARED main"* ]] \
  && ok "destructive --work-tree main-checkout command blocked" \
  || bad "destructive --work-tree main-checkout blocked" "rc=$rc err=$LAST_ERR"

run "$WT" 1 -C "$FAKE_HOME/.claude/hooks-link" clean -fd
rc=$?
[[ $rc -eq 77 && "$LAST_ERR" == *"LIVE deploy clone"* ]] \
  && ok "destructive -C symlinked deploy path blocked" \
  || bad "destructive -C symlinked deploy path blocked" "rc=$rc err=$LAST_ERR"

# H. 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"

# I. 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"

# J. 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" \
  OD_SHIM_PIN_FILE="$TMP/nohome/.local/state/overdeck/shim-real/git" 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")"

# K. Sabotaged resolution — the pin points at a copy of the shim, the exact shape that
#    forked without bound before. Resolution must reject it and refuse like any
#    unusable pin. Reached only after require_danger_lab has proved this process is
#    inside the disposable guest.
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 78 && "$(cat "$TMP/err")" == *install-git-guard-real* && $((AFTER - BEFORE)) -lt 20 ]] \
  && ok "sabotaged pin rejected: exits 78 without forking or exec-looping" \
  || bad "sabotaged pin rejected: exits 78 without forking or exec-looping" "rc=$rc procs=$BEFORE->$AFTER err=$(cat "$TMP/err")"

# L. The re-entry bound itself, exercised directly: it is the backstop for a target that
#    passes resolution and is a shim anyway. Builtins only, so it is safe anywhere.
BOUND_ERR=$( _OD_SHIM_DEPTH_git=2 bash -c '. "$1"; shim_guard_enter git' _ "$ROOT/lib/shim-guard.sh" 2>&1 )
rc=$?
[[ $rc -eq 79 && "$BOUND_ERR" == *"re-entered at depth"* ]] \
  && ok "re-entry bound exits 79 past depth 2" \
  || bad "re-entry bound exits 79 past depth 2" "rc=$rc err=$BOUND_ERR"

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