#!/bin/bash
# Sandboxed AC verification for act:04bdcc6b.
# Everything runs under a mktemp dir with HOME overridden — never touches
# the real ~/.mux/worktrees, ~/.config/mux, or any live worktree.
set -uo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
SANDBOX=$(mktemp -d /tmp/cc-dirty-check-test.XXXXXX)
FAKE_HOME="$SANDBOX/home"
PROJ="$SANDBOX/proj"
WT_DIR="$FAKE_HOME/.mux/worktrees"

pass=0; fail=0
ok()  { echo "  PASS: $1"; pass=$((pass+1)); }
bad() { echo "  FAIL: $1"; fail=$((fail+1)); }
assert() { # assert <desc> <cmd...>
  local desc="$1"; shift
  if "$@" >/dev/null 2>&1; then ok "$desc"; else bad "$desc"; fi
}

# --- sandbox home -----------------------------------------------------------
mkdir -p "$FAKE_HOME/.config/mux" "$WT_DIR" "$FAKE_HOME/.local/share/mux/wt-health" \
         "$FAKE_HOME/.local/bin" "$FAKE_HOME/.claude/projects"
cp "$REPO_ROOT/templates/mux/config/worktree-dirty-check.sh" "$FAKE_HOME/.config/mux/"
cp "$REPO_ROOT/templates/mux/config/worktree-cleanup.sh" "$FAKE_HOME/.config/mux/"
cp "$REPO_ROOT/templates/mux/config/worktree-session-health.sh" "$FAKE_HOME/.config/mux/"
chmod +x "$FAKE_HOME/.config/mux/"*.sh

# Stub muxlib.py — answers only what the worktree code paths ask.
cat > "$FAKE_HOME/.config/mux/muxlib.py" <<'PY'
import sys, os
cmd = sys.argv[1] if len(sys.argv) > 1 else ""
if cmd == "worktree-is-active":
    print("yes")
elif cmd == "project-path":
    print(os.environ.get("STUB_PROJ_PATH", ""))
elif cmd == "project-names":
    print("sess")
elif cmd == "narrate-check":
    print("yes")
PY
# Stub manage-notes.py — log invocations.
cat > "$FAKE_HOME/.config/mux/manage-notes.py" <<'PY'
import sys, os
with open(os.path.join(os.environ["HOME"], "notes-log.txt"), "a") as f:
    f.write(" ".join(sys.argv[1:]) + "\n")
PY
echo '{"active":[]}' > "$FAKE_HOME/.config/mux/worktrees.json"
echo '{}' > "$FAKE_HOME/.config/mux/projects.json"

# --- fixture main repo ------------------------------------------------------
# .claude/skills/ is gitignored INFRA (refreshed by file copy); plans/,
# methodology/, and rules/ are TRACKED = authored record (sync via git only).
git init -q "$PROJ"
git -C "$PROJ" config user.email t@t.t; git -C "$PROJ" config user.name t
mkdir -p "$PROJ/.claude/plans" "$PROJ/.claude/methodology" "$PROJ/.claude/rules" "$PROJ/.claude/skills"
echo '.claude/skills/' > "$PROJ/.gitignore"
echo plan > "$PROJ/.claude/plans/existing.md"
echo meth > "$PROJ/.claude/methodology/m.md"
echo rule > "$PROJ/.claude/rules/r.md"
echo skill-v1 > "$PROJ/.claude/skills/skill.md"
echo '{"mcpServers":{}}' > "$PROJ/.mcp.json"
echo lock-v1 > "$PROJ/package-lock.json"
echo code > "$PROJ/file.txt"
git -C "$PROJ" add -A && git -C "$PROJ" commit -qm init
MAIN_SLUG=$(echo "$PROJ" | sed 's|[/.]|-|g')
mkdir -p "$FAKE_HOME/.claude/projects/$MAIN_SLUG"

mkwt() { git -C "$PROJ" worktree add -q "$WT_DIR/sess-$1" -b "mux/$1" HEAD; }
RUN_ENV=(env HOME="$FAKE_HOME" STUB_PROJ_PATH="$PROJ")
MUX="$REPO_ROOT/templates/mux/bin/mux"
HELPER="$FAKE_HOME/.config/mux/worktree-dirty-check.sh"
CLEANUP_HOOK="$FAKE_HOME/.config/mux/worktree-cleanup.sh"

echo "== T1: mux worktree refresh preserves uncommitted authored records (AC1)"
mkwt task1
mkdir -p "$WT_DIR/sess-task1/.claude/plans"
echo precious > "$WT_DIR/sess-task1/.claude/plans/test-file.md"   # new, uncommitted
echo edited   > "$WT_DIR/sess-task1/.claude/plans/existing.md"    # modified, uncommitted
echo skill-v2 > "$PROJ/.claude/skills/skill.md"                   # infra changed in main
"${RUN_ENV[@]}" bash "$MUX" worktree refresh sess task1 >"$SANDBOX/t1.out" 2>&1
assert "uncommitted new plans file survives refresh" test -f "$WT_DIR/sess-task1/.claude/plans/test-file.md"
assert "uncommitted plans edit survives refresh" grep -qx edited "$WT_DIR/sess-task1/.claude/plans/existing.md"
assert "infra (skills) actually refreshed from main" grep -qx skill-v2 "$WT_DIR/sess-task1/.claude/skills/skill.md"
assert "methodology untouched" grep -qx meth "$WT_DIR/sess-task1/.claude/methodology/m.md"

echo "== T2: helper verdicts (single implementation)"
mkwt task2   # modified tracked .mcp.json -> dirty (AC2)
echo '{"mcpServers":{"x":{}}}' > "$WT_DIR/sess-task2/.mcp.json"
out=$("$HELPER" "$WT_DIR/sess-task2" "$PROJ")
assert "modified .mcp.json => dirty ($out)" grep -q 'uncommitted=1 verdict=dirty' <<<"$out"

mkwt task3   # lockfile churn + .mcp.json typechange + untracked .claude infra
             # + node_modules symlink (gitignore `node_modules/` matches only
             # dirs, so the mux-made symlink shows `?? node_modules` — found
             # live 2026-06-11: it made every mux worktree immortal) -> clean (AC3)
echo lock-rewritten > "$WT_DIR/sess-task3/package-lock.json"
rm "$WT_DIR/sess-task3/.mcp.json"; ln -s "$PROJ/.mcp.json" "$WT_DIR/sess-task3/.mcp.json"
mkdir -p "$WT_DIR/sess-task3/.claude/agents"; echo x > "$WT_DIR/sess-task3/.claude/agents/a.md"
ln -s "$PROJ/node_modules" "$WT_DIR/sess-task3/node_modules"
out=$("$HELPER" "$WT_DIR/sess-task3" "$PROJ")
assert "lockfile + infra + node_modules symlink churn => clean ($out)" grep -q 'verdict=clean' <<<"$out"

mkwt task4   # untracked authored record -> dirty
mkdir -p "$WT_DIR/sess-task4/.claude/plans"; echo new > "$WT_DIR/sess-task4/.claude/plans/new-doc.md"
out=$("$HELPER" "$WT_DIR/sess-task4" "$PROJ")
assert "untracked .claude/plans/ doc => dirty ($out)" grep -q 'verdict=dirty' <<<"$out"

# task4c/d: the authored set is ANY top-level .claude/ entry with tracked
# files, not a static plans/methodology list (act:14305d16 — the
# assume-unchanged trap fired on .claude/rules/enforcement-pipeline.md).
mkwt task4c  # MODIFIED tracked .claude/rules/ file -> dirty
echo edited-rule > "$WT_DIR/sess-task4c/.claude/rules/r.md"
out=$("$HELPER" "$WT_DIR/sess-task4c" "$PROJ")
assert "modified tracked .claude/rules/ file => dirty ($out)" grep -q 'verdict=dirty' <<<"$out"

mkwt task4d  # NEW untracked doc beside tracked .claude/rules/ files -> dirty
echo new-rule > "$WT_DIR/sess-task4d/.claude/rules/new-rule.md"
out=$("$HELPER" "$WT_DIR/sess-task4d" "$PROJ")
assert "untracked doc in tracked .claude/rules/ => dirty ($out)" grep -q 'verdict=dirty' <<<"$out"

# task4b: repo whose .claude/ has NO tracked files — without
# --untracked-files=all, porcelain collapses everything to one '?? .claude/'
# line and the authored record inside is invisible (deletable). CP3 finding,
# group 2026-06-10-1.
BARE_PROJ="$SANDBOX/bare-proj"
git init -q "$BARE_PROJ"
git -C "$BARE_PROJ" config user.email t@t.t; git -C "$BARE_PROJ" config user.name t
git -C "$BARE_PROJ" commit -q --allow-empty -m init
mkdir -p "$BARE_PROJ/.claude/plans"; echo new > "$BARE_PROJ/.claude/plans/hidden-doc.md"
out=$("$HELPER" "$BARE_PROJ" "$BARE_PROJ")
assert "authored record inside fully-untracked .claude/ => dirty ($out)" grep -q 'verdict=dirty' <<<"$out"
# ...while pure infra inside a fully-untracked .claude/ stays clean churn
BARE_PROJ2="$SANDBOX/bare-proj2"
git init -q "$BARE_PROJ2"
git -C "$BARE_PROJ2" config user.email t@t.t; git -C "$BARE_PROJ2" config user.name t
git -C "$BARE_PROJ2" commit -q --allow-empty -m init
mkdir -p "$BARE_PROJ2/.claude/agents"; echo x > "$BARE_PROJ2/.claude/agents/a.md"
out=$("$HELPER" "$BARE_PROJ2" "$BARE_PROJ2")
assert "pure infra inside fully-untracked .claude/ => clean ($out)" grep -q 'verdict=clean' <<<"$out"

out=$("$HELPER" "$SANDBOX/nonexistent" "$PROJ")
assert "broken worktree => fail-DIRTY ($out)" grep -q 'commits=? uncommitted=? verdict=dirty' <<<"$out"

echo "== T3: cleanup hook keeps dirty worktree (modified .mcp.json) (AC2)"
"${RUN_ENV[@]}" bash "$CLEANUP_HOOK" sess task2 >"$SANDBOX/t3.out" 2>&1
assert "worktree with real .mcp.json edit NOT deleted" test -d "$WT_DIR/sess-task2"
assert "sticky note left for dirty worktree" grep -q "task2" "$FAKE_HOME/notes-log.txt"

echo "== T4: cleanup hook silently removes lockfile-only worktree (AC3)"
"${RUN_ENV[@]}" bash "$CLEANUP_HOOK" sess task3 >"$SANDBOX/t4.out" 2>&1
assert "lockfile-churn-only worktree removed as clean" test ! -d "$WT_DIR/sess-task3"

echo "== T5: interactive mux worktree cleanup agrees (AC2 + AC3, same verdict)"
mkwt task5   # lockfile-only -> clean, auto-removed without prompt
echo lock-rewritten > "$WT_DIR/sess-task5/package-lock.json"
out=$("${RUN_ENV[@]}" bash "$MUX" worktree cleanup sess task5 </dev/null 2>&1)
assert "bin/mux: lockfile-only cleaned up as clean" grep -q "cleaned up (no changes)" <<<"$out"
assert "bin/mux: lockfile-only worktree removed" test ! -d "$WT_DIR/sess-task5"

mkwt task6   # modified .mcp.json -> dirty, prompts instead of deleting
echo '{"changed":true}' > "$WT_DIR/sess-task6/.mcp.json"
out=$(printf 'x\n' | "${RUN_ENV[@]}" bash "$MUX" worktree cleanup sess task6 2>&1) || true
assert "bin/mux: modified .mcp.json reported dirty" grep -q "has work to merge" <<<"$out"
assert "bin/mux: dirty worktree not deleted" test -d "$WT_DIR/sess-task6"

echo "== T6: missing helper degrades to fail-DIRTY (never deletes)"
mkwt task7   # truly clean worktree, but helper missing
mv "$HELPER" "$HELPER.disabled"
"${RUN_ENV[@]}" bash "$CLEANUP_HOOK" sess task7 >"$SANDBOX/t6.out" 2>&1
assert "hook without helper does NOT delete (fail-DIRTY)" test -d "$WT_DIR/sess-task7"
out=$(printf 'x\n' | "${RUN_ENV[@]}" bash "$MUX" worktree cleanup sess task7 2>&1) || true
assert "bin/mux without helper treats as dirty" grep -q "has work to merge" <<<"$out"
mv "$HELPER.disabled" "$HELPER"

echo "== T7: truly clean worktree still removed (baseline)"
mkwt task8
"${RUN_ENV[@]}" bash "$CLEANUP_HOOK" sess task8 >"$SANDBOX/t7.out" 2>&1
assert "clean worktree silently removed by hook" test ! -d "$WT_DIR/sess-task8"

echo ""
echo "RESULT: $pass passed, $fail failed (sandbox: $SANDBOX)"
[[ $fail -eq 0 ]] && rm -rf "$SANDBOX"
exit $fail
