#!/usr/bin/env bash
# Regression tests for hooks/worktree-lock-gate.sh — one owner per worktree, with
# automatic reclaim of a lock whose owning session's `claude` process is gone.
# `claude` process liveness is faked via WT_LOCK_PROC_DIR (see worktree-guard-lib.sh's
# wt_session_alive), so this is hermetic — no real Claude Code session is needed.
# Run: bash worktree-lock-gate.test.sh (exit 0 = all pass).
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HOOK="$ROOT/hooks/worktree-lock-gate.sh"
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}/wlg-test-XXXX")
trap 'rm -rf "$TMP"' EXIT

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"
echo x > "$WT/f"
LOCK="$REPO/.worktrees/.locks/wt1.json"

FAKEPROC="$TMP/proc"
proc_alive() { # proc_alive <pid> <session_id>
  mkdir -p "$FAKEPROC/$1/fd"
  echo claude > "$FAKEPROC/$1/comm"
  ln -sf "/fake/projects/proj/$2.jsonl" "$FAKEPROC/$1/fd/10"
}
proc_kill() { rm -rf "${FAKEPROC:?}/$1"; } # simulate the owning claude process exiting

edit_json() { jq -n --arg cwd "$1" --arg fp "$2" --arg sid "$3" '{tool_name:"Edit", cwd:$cwd, session_id:$sid, tool_input:{file_path:$fp}}'; }
bash_json()  { jq -n --arg cwd "$1" --arg cmd "$2" --arg sid "$3" '{tool_name:"Bash", cwd:$cwd, session_id:$sid, tool_input:{command:$cmd}}'; }

decision() {
  local out
  out=$(printf '%s' "$1" | WT_LOCK_PROC_DIR="$FAKEPROC" bash "$HOOK")
  if [[ -z "$out" ]]; then
    echo allow
  elif printf '%s' "$out" | jq -e '.hookSpecificOutput.permissionDecision == "deny"' >/dev/null 2>&1; then
    echo deny
  else
    echo "unexpected:$out"
  fi
}

rm -rf "$FAKEPROC"; mkdir -p "$FAKEPROC"

# A. First mutation by sess-A claims the (previously unowned) worktree.
proc_alive 100 sess-A
got=$(decision "$(edit_json "$WT" "$WT/f" sess-A)")
[[ "$got" == allow ]] && ok "first claim by sess-A allowed" || bad "first claim allowed" "$got"
owner=$(jq -r .session_id "$LOCK" 2>/dev/null)
[[ "$owner" == sess-A ]] && ok "lock records sess-A as owner" || bad "lock owner" "$owner"

# B. sess-A mutating its OWN worktree again is always allowed.
got=$(decision "$(edit_json "$WT" "$WT/f" sess-A)")
[[ "$got" == allow ]] && ok "owner sess-A repeat mutation allowed" || bad "owner repeat allowed" "$got"

# C. sess-A running a destructive git verb in its own worktree is allowed.
got=$(decision "$(bash_json "$WT" "git reset --hard" sess-A)")
[[ "$got" == allow ]] && ok "destructive git by owner sess-A allowed" || bad "destructive by owner allowed" "$got"

# D. A DIFFERENT session (sess-B) attempting to mutate while sess-A is still running
#    (its claude process is alive) is refused, and the message names the owner.
out=$(printf '%s' "$(edit_json "$WT" "$WT/f" sess-B)" | WT_LOCK_PROC_DIR="$FAKEPROC" bash "$HOOK")
reason=$(printf '%s' "$out" | jq -r '.hookSpecificOutput.permissionDecisionReason // empty')
if printf '%s' "$out" | jq -e '.hookSpecificOutput.permissionDecision == "deny"' >/dev/null 2>&1 \
  && [[ "$reason" == *sess-A* ]]; then
  ok "second-session (sess-B) attach refused, names sess-A"
else
  bad "second-session attach refused" "$out"
fi

# E. Read-only Bash by sess-B is never subject to the lock (not mutating).
got=$(decision "$(bash_json "$WT" "git status" sess-B)")
[[ "$got" == allow ]] && ok "read-only bash bypasses the lock" || bad "read-only bypasses lock" "$got"
owner=$(jq -r .session_id "$LOCK" 2>/dev/null)
[[ "$owner" == sess-A ]] && ok "read-only bash does not steal the lock" || bad "read-only does not steal lock" "$owner"

# F. sess-A's claude process exits -> the lock is stale -> sess-B's mutation reclaims
#    it automatically instead of being permanently blocked.
proc_kill 100
got=$(decision "$(edit_json "$WT" "$WT/f" sess-B)")
[[ "$got" == allow ]] && ok "stale lock reclaimed by sess-B" || bad "stale lock reclaimed" "$got"
owner=$(jq -r .session_id "$LOCK" 2>/dev/null)
[[ "$owner" == sess-B ]] && ok "lock now records sess-B as owner" || bad "lock owner after reclaim" "$owner"

# G. A third session (sess-C) is refused again now that sess-B (alive) owns it.
proc_alive 200 sess-B
got=$(decision "$(edit_json "$WT" "$WT/f" sess-C)")
[[ "$got" == deny ]] && ok "third session refused while sess-B alive" || bad "third session refused" "$got"

# H. Edit into the main checkout (not under .worktrees) is out of this gate's scope
#    (main-checkout-guard.sh's job) — no slug, no lock claimed.
got=$(decision "$(edit_json "$REPO" "$REPO/f" sess-D)")
[[ "$got" == allow ]] && ok "main-checkout path out of lock-gate scope" || bad "main-checkout out of scope" "$got"

# I. Missing session_id -> cannot arbitrate ownership -> fail-open (allow).
out=$(jq -n --arg cwd "$WT" --arg fp "$WT/f" '{tool_name:"Edit", cwd:$cwd, tool_input:{file_path:$fp}}')
got=$(decision "$out")
[[ "$got" == allow ]] && ok "missing session_id fails open" || bad "missing session_id fails open" "$got"

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