#!/bin/bash
# Sandboxed AC verification for act:fe6aab6f (mux fail-loud sweep).
#
# Everything runs under a mktemp dir with HOME overridden AND a private
# throwaway tmux server (tmux -L mux-test-$$ -f /dev/null) — it never touches
# the real ~/.mux/worktrees, ~/.config/mux, or the operator's live tmux
# server (the test runner itself may be running inside one). The test server
# is killed in cleanup even on assertion failure.
set -uo pipefail

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

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
}

# --- private throwaway tmux server -------------------------------------------
# Never the operator's live server: dedicated socket, no config, killed on exit.
unset TMUX TMUX_PANE
SOCK="mux-test-$$"
TMX() { tmux -L "$SOCK" -f /dev/null "$@"; }
cleanup() { TMX kill-server 2>/dev/null || true; rm -rf "$SANDBOX"; }
trap cleanup EXIT

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

# Stub muxlib.py — answers only what cmd_new/cmd_resume/create_worktree ask.
# project-path mimics the real muxlib: prints the path, or exits 1 when the
# project isn't registered (STUB_PROJ_PATH empty/unset).
cat > "$FAKE_HOME/.config/mux/muxlib.py" <<'PY'
import sys, os
cmd = sys.argv[1] if len(sys.argv) > 1 else ""
if cmd == "project-path":
    p = os.environ.get("STUB_PROJ_PATH", "")
    if p:
        print(p)
    else:
        sys.exit(1)
elif cmd == "project-names":
    print("sess")
elif cmd in ("worktree-add", "worktree-remove"):
    pass
elif cmd == "worktree-is-active":
    print("yes")
elif cmd == "narrate-check":
    print("yes")
PY
echo '{"active":[]}' > "$FAKE_HOME/.config/mux/worktrees.json"
echo '{}' > "$FAKE_HOME/.config/mux/projects.json"

# --- fixture projects ----------------------------------------------------------
# .claude/rules/ is tracked (= authored record), .claude/skills/ is gitignored
# (= disposable infra) — lets T3 assert the creation-path carve-out.
git init -q "$PROJ"
git -C "$PROJ" config user.email t@t.t; git -C "$PROJ" config user.name t
echo code > "$PROJ/file.txt"
mkdir -p "$PROJ/.claude/rules" "$PROJ/.claude/skills"
echo '.claude/skills/' > "$PROJ/.gitignore"
echo rule > "$PROJ/.claude/rules/r.md"
echo skill > "$PROJ/.claude/skills/s.md"
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"

EMPTY_PROJ="$SANDBOX/empty-proj"   # git repo, NO commits → worktree add must fail
git init -q "$EMPTY_PROJ"

PLAIN_DIR="$SANDBOX/plain-dir"     # registered project that is not a git repo
mkdir -p "$PLAIN_DIR"

# --- test desk on the private server -------------------------------------------
HOME="$FAKE_HOME" TMX new-session -d -s sess -x 200 -y 50 -c "$PROJ" \
  || { echo "FATAL: could not start private tmux server"; exit 1; }
SOCKET_PATH=$(TMX display-message -p -t sess '#{socket_path}')
SERVER_PID=$(TMX display-message -p -t sess '#{pid}')
PANE_ID=$(TMX display-message -p -t sess:0 '#{pane_id}')

# Run mux "inside" the private server: TMUX points at the test socket so
# in_tmux passes and every bare `tmux` call lands on the throwaway server.
run_mux() { # run_mux <stub-proj-path> <args...>
  local stub="$1"; shift
  env HOME="$FAKE_HOME" STUB_PROJ_PATH="$stub" \
      TMUX="${SOCKET_PATH},${SERVER_PID},0" TMUX_PANE="$PANE_ID" \
      bash "$MUX" "$@"
}
win_count() { TMX list-windows -t sess 2>/dev/null | wc -l | tr -d ' '; }
win_opt()   { TMX show-options -wv -t "sess:$1" "$2" 2>/dev/null; }

ID_A="aaaaaaaa-1111-2222-3333-444444444444"
ID_B="bbbbbbbb-1111-2222-3333-444444444444"
ID_C="cccccccc-1111-2222-3333-444444444444"
ID_D="dddddddd-1111-2222-3333-444444444444"
ID_E="eeeeeeee-1111-2222-3333-444444444444"
ID_F="ffffffff-1111-2222-3333-444444444444"

echo "== T0: no surviving silent fallbacks in bin/mux (AC1)"
assert "zero '|| win_path=\"\$path\"' fallbacks remain" \
  bash -c "! grep -q '|| win_path=\"\\\$path\"' '$MUX'"

echo "== T1: cmd_resume input guard (id flows into branch/path/send-keys)"
before=$(win_count)
out=$(run_mux "$PROJ" resume abc 2>&1); rc=$?
assert "id shorter than 8 chars dies ($rc)" test "$rc" -ne 0
out=$(run_mux "$PROJ" resume 'not$a-valid;id!!' 2>&1); rc=$?
assert "non-hex id dies ($rc)" test "$rc" -ne 0
assert "guard failures open no window" test "$(win_count)" -eq "$before"

echo "== T2: no active Claude => by-design main landing (stays working)"
before=$(win_count)
out=$(run_mux "$PROJ" resume "$ID_A" 2>&1); rc=$?
assert "resume with no active Claude exits 0 ($rc)" test "$rc" -eq 0
assert "window resume-aaaaaaaa opened" test "$(win_count)" -eq $((before + 1))
assert "main landing carries NO @mux_worktree flag" \
  test -z "$(win_opt resume-aaaaaaaa @mux_worktree)"

# Activate the desk: a non-shell pane command makes has_active_claude true.
TMX new-window -t sess -d -n fakeclaude 'sleep 300'

echo "== T3: active Claude + git project => worktree + window options (AC3)"
out=$(run_mux "$PROJ" resume "$ID_B" 2>&1); rc=$?
assert "resume exits 0 ($rc)" test "$rc" -eq 0
assert "worktree created" test -d "$WT_DIR/sess-resume-bbbbbbbb"
assert "@mux_worktree returns 1 (AC3)" \
  test "$(win_opt resume-bbbbbbbb @mux_worktree)" = "1"
assert "@wt_healthy set alongside (·wt indicator not red)" \
  test "$(win_opt resume-bbbbbbbb @wt_healthy)" = "1"

echo "== T3b: creation-path carve-out — tracked .claude/ never frozen (act:14305d16)"
WT_B="$WT_DIR/sess-resume-bbbbbbbb"
assert "no tracked .claude/ file carries the assume-unchanged bit" \
  bash -c "! git -C '$WT_B' ls-files -v .claude/ | grep -q '^h '"
assert "gitignored .claude/skills/ infra copied into the worktree" \
  test -f "$WT_B/.claude/skills/s.md"
assert "tracked .claude/rules/ file checked out, not clobbered" \
  bash -c "grep -qx rule '$WT_B/.claude/rules/r.md'"

echo "== T4: re-resume of the same id REUSES the existing worktree"
TMX kill-window -t sess:resume-bbbbbbbb 2>/dev/null
out=$(run_mux "$PROJ" resume "$ID_B" 2>&1); rc=$?
assert "second resume of same id exits 0 ($rc)" test "$rc" -eq 0
assert "existing worktree reused (no uniquified sibling)" \
  bash -c "! ls -d '$WT_DIR'/sess-resume-bbbbbbbb-* 2>/dev/null | grep -q ."
assert "reused window flagged @mux_worktree=1" \
  test "$(win_opt resume-bbbbbbbb @mux_worktree)" = "1"
assert "reused window flagged @wt_healthy=1" \
  test "$(win_opt resume-bbbbbbbb @wt_healthy)" = "1"

echo "== T5: slug path occupied by an INVALID dir => die naming the path"
mkdir -p "$WT_DIR/sess-resume-cccccccc"
before=$(win_count)
out=$(run_mux "$PROJ" resume "$ID_C" 2>&1); rc=$?
assert "invalid leftover dies ($rc)" test "$rc" -ne 0
assert "die message names the colliding path" \
  grep -q "sess-resume-cccccccc" <<<"$out"
assert "die message offers a recovery command" grep -q "rm -rf" <<<"$out"
assert "nothing opened in main" test "$(win_count)" -eq "$before"

echo "== T6: real worktree-creation failure => die, nothing opens (AC2)"
before=$(win_count)
out=$(run_mux "$EMPTY_PROJ" resume "$ID_D" 2>&1); rc=$?
assert "cmd_resume: forced creation failure dies ($rc)" test "$rc" -ne 0
assert "cmd_resume: die message refuses main checkout" \
  grep -q "refusing to run work on the main checkout" <<<"$out"
assert "cmd_resume: nothing opened in main" test "$(win_count)" -eq "$before"
out=$(run_mux "$EMPTY_PROJ" new 2>&1); rc=$?
assert "cmd_new no-prompt: forced creation failure dies ($rc)" test "$rc" -ne 0
assert "cmd_new no-prompt: die message refuses main checkout" \
  grep -q "refusing to run work on the main checkout" <<<"$out"
assert "cmd_new no-prompt: nothing opened in main" test "$(win_count)" -eq "$before"

echo "== T7: isolation impossible BY DESIGN => loud fall-through, not die"
before=$(win_count)
out=$(run_mux "$PLAIN_DIR" resume "$ID_E" 2>&1); rc=$?
assert "non-git project dir: resume still works ($rc)" test "$rc" -eq 0
assert "non-git project dir: visible warning names why" \
  grep -q "isn't a git repository" <<<"$out"
assert "non-git project dir: window opened on main" test "$(win_count)" -eq $((before + 1))
assert "non-git landing carries NO @mux_worktree flag" \
  test -z "$(win_opt resume-eeeeeeee @mux_worktree)"
before=$(win_count)
out=$(run_mux "" resume "$ID_F" 2>&1); rc=$?
assert "unregistered desk: resume still works ($rc)" test "$rc" -eq 0
assert "unregistered desk: visible warning names why" \
  grep -q "isn't a registered mux project" <<<"$out"
assert "unregistered desk: window opened" test "$(win_count)" -eq $((before + 1))

echo "== T8: cmd_new no-prompt success path still isolates + flags"
before=$(win_count)
out=$(run_mux "$PROJ" new 2>&1); rc=$?
assert "cmd_new no-prompt exits 0 ($rc)" test "$rc" -eq 0
assert "window opened" test "$(win_count)" -eq $((before + 1))
new_wt=$(ls -d "$WT_DIR"/sess-window-* 2>/dev/null | head -1)
assert "worktree created for the new window" test -n "$new_wt"
if [[ -n "$new_wt" ]]; then
  wname=$(basename "$new_wt"); wname="${wname#sess-}"
  assert "new window flagged @mux_worktree=1" \
    test "$(win_opt "$wname" @mux_worktree)" = "1"
  assert "new window flagged @wt_healthy=1" \
    test "$(win_opt "$wname" @wt_healthy)" = "1"
fi

echo "== T9: mux handoff seeds the next session into a WORKTREE, not main"
# Design intent (worktree-to-worktree handoffs): the seeded session is a
# second session on the desk — the shared-main commit-sweep class (1adc5ef)
# is what isolation prevents. Safe since the 40cc831 carve-out.
DESC="$SANDBOX/seed.json"
cat > "$DESC" <<JSON
{ "project": "sess", "project_path": "$PROJ", "name": "seeded-next",
  "seed_prompt": "continue the thing" }
JSON
before=$(win_count)
out=$(run_mux "$PROJ" handoff "$DESC" 2>&1); rc=$?
assert "handoff exits 0 ($rc)" test "$rc" -eq 0
assert "window opened" test "$(win_count)" -eq $((before + 1))
assert "worktree created for the seeded session" test -d "$WT_DIR/sess-seeded-next"
assert "seeded window flagged @mux_worktree=1" \
  test "$(win_opt seeded-next @mux_worktree)" = "1"
assert "seeded window flagged @wt_healthy=1" \
  test "$(win_opt seeded-next @wt_healthy)" = "1"
assert "launch message says worktree" grep -q "fresh worktree" <<<"$out"
assert "launch message does not claim main checkout" \
  bash -c "! grep -q \"main checkout\" <<<'$out'"

echo "== T9b: handoff without isolation falls through LOUDLY to main"
DESC2="$SANDBOX/seed2.json"
cat > "$DESC2" <<JSON
{ "project": "sess", "project_path": "$PLAIN_DIR", "name": "seeded-plain",
  "seed_prompt": "x" }
JSON
before=$(win_count)
out=$(run_mux "$PLAIN_DIR" handoff "$DESC2" 2>&1); rc=$?
assert "non-git project handoff exits 0 ($rc)" test "$rc" -eq 0
assert "window opened" test "$(win_count)" -eq $((before + 1))
assert "loud fall-through names why" grep -q "No worktree isolation" <<<"$out"
assert "main landing carries NO @mux_worktree" \
  test -z "$(win_opt seeded-plain @mux_worktree)"

echo "== T10: prompt-bearing launch stages the prompt as the CLI initial-prompt arg"
# queue_claude_start writes the prompt to a per-window seed file and launches
# `claude "$(cat <seedfile>)"` — auto-submit with no keystroke timing. We assert
# on that synchronous file (the pane's async `claude` run is not raced).
SEED_DIR="$FAKE_HOME/.local/share/mux/seed-prompts"
# A clean lead sentence + ". " gives slugify a valid window name; the gnarly
# body (newlines, quotes, $, backticks) still rides the seed file verbatim —
# queue_claude_start stages the full prompt, not the slug.
PROMPT=$'Fix the parser. Body line with "double quotes", a $DOLLAR and a `backtick`.\nSecond body line.'
printf '%s' "$PROMPT" > "$SANDBOX/expected-body.txt"
before=$(win_count)
out=$(run_mux "$PROJ" new "$PROMPT" 2>&1); rc=$?
assert "mux new with a prompt exits 0 ($rc)" test "$rc" -eq 0
assert "a worktree window opened" test "$(win_count)" -eq $((before + 1))
seedf=$(ls -t "$SEED_DIR"/sess-*.txt 2>/dev/null | head -1)
assert "a seed-prompt file was written" test -n "$seedf"
if [[ -n "$seedf" ]]; then
  assert "auto_orient=1 prepends the orient instruction as line 1" \
    bash -c "head -1 '$seedf' | grep -qx 'Run /orient-quick first to load state, then:'"
  tail -n +3 "$seedf" > "$SANDBOX/got-body.txt"
  assert "multi-line prompt body preserved byte-identical (quotes/\$/backticks intact)" \
    diff "$SANDBOX/expected-body.txt" "$SANDBOX/got-body.txt"
fi

echo "== T12: the prompt is an ARG to claude, never executed as shell (auto-submit safety)"
# The motivating hazard the reviewers flagged: auto-submitting by pasting the
# prompt into the pane and pressing Enter would EXECUTE it as shell commands if
# `claude` isn't up. Passing it as `claude "<prompt>"` makes that impossible —
# if claude can't launch the prompt is just an unused argument. There is no
# real `claude` on PATH here, so the launch fails; the sentinel proves the
# prompt's embedded command never ran.
SENTINEL="$SANDBOX/PROMPT_WAS_EXECUTED"
rm -f "$SENTINEL"
# Lead sentence + ". " for a valid slug; the touch sits on its own line, so if
# this ever regressed to paste-into-shell + Enter, line 2 WOULD run and create
# the sentinel. As a `claude "<arg>"` argument it never executes.
PWN_PROMPT=$'Reproduce the bug. \ntouch "'"$SENTINEL"$'"'
before=$(win_count)
out=$(run_mux "$PROJ" new "$PWN_PROMPT" 2>&1); rc=$?
assert "mux new with a command-shaped prompt exits 0 ($rc)" test "$rc" -eq 0
sleep 1   # let the pane's shell attempt `cd … && claude "$(cat …)"`
assert "prompt content was NOT executed as shell (no sentinel created)" \
  test ! -e "$SENTINEL"

echo ""
echo "RESULT: $pass passed, $fail failed (sandbox: $SANDBOX)"
exit $fail
