#!/usr/bin/env bash
# Fixture test for module cutover: sandbox HOME + fake old repo, units and state.
# Full run relocates state, symlinks and re-links units; induced failures roll back.
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
FIX=$(mktemp -d "${TMPDIR:-/tmp}/cutover-test-XXXXXX")
trap 'rm -rf "$FIX"' EXIT
pass=0; fail=0
ok()  { echo "  PASS: $1"; pass=$((pass+1)); }
bad() { echo "  FAIL: $1"; fail=$((fail+1)); }

export TEST_HOME="$FIX/home"
mkdir -p "$TEST_HOME/Projects" "$TEST_HOME/.config/systemd/user" \
  "$TEST_HOME/.local/state/overdeck" "$TEST_HOME/.claude/bin" "$FIX/bin"

export SYSTEMCTL_LOG="$FIX/systemctl.log"
: >"$SYSTEMCTL_LOG"

cat >"$FIX/bin/systemctl" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"${SYSTEMCTL_LOG:?}"
case "$*" in
  *is-active*) exit 3 ;;
  *start*) [[ "${SYSTEMCTL_FAIL_START:-0}" == "1" ]] && exit 1; exit 0 ;;
  *) exit 0 ;;
esac
STUB

cat >"$FIX/bin/fuser" <<'STUB'
#!/usr/bin/env bash
[[ "${FUSER_BUSY:-0}" == "1" ]] && exit 0
exit 1
STUB

cat >"$FIX/bin/crontab" <<'STUB'
#!/usr/bin/env bash
exit 1
STUB

chmod +x "$FIX/bin/systemctl" "$FIX/bin/fuser" "$FIX/bin/crontab"
export PATH="$FIX/bin:$PATH"

[[ "$(command -v systemctl)" == "$FIX/bin/systemctl" ]] || {
  echo "refusing to run: systemctl stub not first on PATH" >&2; exit 1; }
[[ "$(command -v fuser)" == "$FIX/bin/fuser" ]] || {
  echo "refusing to run: fuser stub not first on PATH" >&2; exit 1; }

git_t() { git -c user.email=t@t -c user.name=t "$@"; }

mk_overdeck() { # $1=dest
  local dest="$1"
  mkdir -p "$dest/lib/deckctl" "$dest/tools"
  cp "$ROOT/lib/deckctl/cutover-lib.sh" "$dest/lib/deckctl/"
  cp "$ROOT/tools/ref-sweep.sh" "$dest/tools/"
  printf '[]\n' >"$dest/tools/ref-inventory.json"
}

add_systray_module() { # $1=overdeck $2=health(ok|bad)
  local dest="$1" health="$2"
  mkdir -p "$dest/modules/systray/systemd"
  cp "$ROOT/modules/systray/cutover.sh" "$dest/modules/systray/"
  cat >"$dest/modules/systray/deck.module.json" <<'EOF'
{
  "schema_version": 1,
  "name": "systray",
  "kind": "service",
  "services": ["systemd/systray-ai.service"],
  "bootstrap": "./bootstrap.sh",
  "health": "./health.sh",
  "install": { "mode": "in-place", "needs_git": false },
  "state_dirs": ["runstate", "tmp"]
}
EOF
  if [[ "$health" == "ok" ]]; then
    printf '#!/usr/bin/env bash\nexit 0\n' >"$dest/modules/systray/health.sh"
  else
    printf '#!/usr/bin/env bash\necho "health: fixture failure" >&2\nexit 1\n' \
      >"$dest/modules/systray/health.sh"
  fi
  chmod +x "$dest/modules/systray/health.sh"
  cat >"$dest/modules/systray/systemd/systray-ai.service" <<'EOF'
[Unit]
Description=systray fixture (new)

[Service]
ExecStart=/bin/true
EOF
}

add_monitor_module() { # $1=overdeck
  local dest="$1"
  mkdir -p "$dest/modules/monitor"
  cp "$ROOT/modules/monitor/cutover.sh" "$dest/modules/monitor/"
  cat >"$dest/modules/monitor/deck.module.json" <<'EOF'
{
  "schema_version": 1,
  "name": "monitor",
  "kind": "config",
  "bootstrap": "./bootstrap.sh",
  "health": "./health.sh",
  "install": { "mode": "in-place", "needs_git": false }
}
EOF
  printf '#!/usr/bin/env bash\nexit 0\n' >"$dest/modules/monitor/health.sh"
  chmod +x "$dest/modules/monitor/health.sh"
}

add_security_module() { # $1=overdeck
  local dest="$1"
  mkdir -p "$dest/modules/security"
  cp "$ROOT/modules/security/cutover.sh" "$dest/modules/security/"
  cp "$ROOT/modules/security/deck.module.json" "$dest/modules/security/"
  printf '#!/usr/bin/env bash\nexit 0\n' >"$dest/modules/security/health.sh"
  chmod +x "$dest/modules/security/health.sh"
}

add_harness_module() { # $1=overdeck
  local dest="$1"
  mkdir -p "$dest/modules/harness"
  cp "$ROOT/modules/harness/cutover.sh" "$dest/modules/harness/"
  cat >"$dest/modules/harness/deck.module.json" <<'EOF'
{
  "schema_version": 1,
  "name": "harness",
  "kind": "service",
  "services": ["systemd/harnessd.service"],
  "bootstrap": "./bootstrap.sh",
  "health": "./health.sh",
  "install": { "mode": "artifact", "needs_git": true },
  "state_dirs": ["runstate", "tmp"]
}
EOF
}

mk_harness_artifact() { # $1=rev
  local rev="$1" base="$TEST_HOME/.local/opt/overdeck/harness"
  mkdir -p "$base/$rev/systemd"
  printf '#!/usr/bin/env bash\n[[ -n "${DECKCTL_ARTIFACT_ROOT:-}" ]] || exit 1\nexit 0\n' \
    >"$base/$rev/health.sh"
  chmod +x "$base/$rev/health.sh"
  cat >"$base/$rev/systemd/harnessd.service" <<'EOF'
[Unit]
Description=harness fixture (artifact)

[Service]
ExecStart=/bin/true
EOF
  ln -sfn "$rev" "$base/current"
}

mk_old_repo() { # $1=path $2=unit
  local path="$1" unit="$2"
  mkdir -p "$path/runstate/nested" "$path/tmp" "$path/systemd"
  printf 'runstate-payload\n' >"$path/runstate/nested/data.txt"
  ln "$path/runstate/nested/data.txt" "$path/runstate/nested/data.hard.txt"
  printf 'tmp-payload\n' >"$path/tmp/cache.bin"
  : >"$path/runstate/session.lock"
  cat >"$path/systemd/$unit" <<EOF
[Unit]
Description=legacy fixture

[Service]
ExecStart=$path/bin/start
EOF
  ln -sfn "$path/systemd/$unit" "$TEST_HOME/.config/systemd/user/$unit"
}

plant_ref_inventory() { # $1=overdeck +=legacy checkout basenames
  local od="$1" base ref="$TEST_HOME/.claude/skills/legacy/SKILL.md"
  shift
  mkdir -p "$(dirname "$ref")"
  : >"$ref"
  for base in "$@"; do
    printf 'launcher: ~/Projects/%s/bin/start\n' "$base" >>"$ref"
  done
  HOME="$TEST_HOME" REF_INVENTORY="$od/tools/ref-inventory.json" \
    bash "$od/tools/ref-sweep.sh" >/dev/null
  jq -e --argjson n "$#" 'length >= $n' "$od/tools/ref-inventory.json" >/dev/null \
    || { echo "refusing to run: planted refs not inventoried ($*)" >&2; exit 1; }
}

snapshot_tree() { # $1=dir — path+content fingerprint
  [[ -d "$1" ]] || { printf 'absent'; return 0; }
  (
    cd "$1"
    find . \( -type f -o -type l \) -print0 | LC_ALL=C sort -z | while IFS= read -r -d '' p; do
      if [[ -L "$p" ]]; then
        printf '%s L %s\n' "$p" "$(readlink "$p")"
      else
        printf '%s F %s\n' "$p" "$(sha256sum <"$p" | awk '{print $1}')"
      fi
    done
  ) | sha256sum | awk '{print $1}'
}

run_cutover() { # $1=overdeck $2=module +=args
  local od="$1" mod="$2"
  shift 2
  HOME="$TEST_HOME" bash "$od/modules/$mod/cutover.sh" "$@"
}

reset_home_state() {
  rm -rf "$TEST_HOME/Projects" "$TEST_HOME/.config/systemd/user" \
    "$TEST_HOME/.local/state/overdeck" "$TEST_HOME/.harness" \
    "$TEST_HOME/.claude/skills/legacy"
  mkdir -p "$TEST_HOME/Projects" "$TEST_HOME/.config/systemd/user" \
    "$TEST_HOME/.local/state/overdeck"
}

echo "=== prereq: cutover scripts exist ==="
for f in \
  "$ROOT/lib/deckctl/cutover-lib.sh" \
  "$ROOT/modules/harness/cutover.sh" \
  "$ROOT/modules/systray/cutover.sh" \
  "$ROOT/modules/monitor/cutover.sh" \
  "$ROOT/modules/security/cutover.sh"; do
  if [[ -f "$f" ]]; then ok "$(basename "$(dirname "$f")")/$(basename "$f") exists"; else bad "missing $f"; fi
done

echo "=== bash -n clean ==="
for f in \
  "$ROOT/lib/deckctl/cutover-lib.sh" \
  "$ROOT/modules/harness/cutover.sh" \
  "$ROOT/modules/systray/cutover.sh" \
  "$ROOT/modules/monitor/cutover.sh" \
  "$ROOT/modules/security/cutover.sh"; do
  if [[ -f "$f" ]] && bash -n "$f"; then
    ok "bash -n $(basename "$(dirname "$f")")/$(basename "$f")"
  else
    bad "bash -n $f"
  fi
done

if [[ ! -f "$ROOT/lib/deckctl/cutover-lib.sh" ]]; then
  echo "----"
  echo "PASS=$pass FAIL=$fail"
  exit 1
fi

echo "=== engine refusal guard ==="
reset_home_state
OD="$FIX/od-engine"
mk_overdeck "$OD"
add_systray_module "$OD" ok
OLD="$TEST_HOME/Projects/systray-ai"
mk_old_repo "$OLD" systray-ai.service
plant_ref_inventory "$OD" systray-ai
for marker in HARNESS_TRANSCRIPT_PATH HARNESS_DELIVERY_ID HARNESS_WORKSPACE_LEASE_TOKEN; do
  set +e
  out=$(env "$marker=set" bash -c 'HOME="$1" bash "$2"' _ "$TEST_HOME" "$OD/modules/systray/cutover.sh" 2>&1)
  rc=$?
  set -e
  if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q "$marker"; then
    ok "refuses under engine ($marker)"
  else
    bad "engine refusal for $marker rc=$rc output=$out"
  fi
done
if [[ -d "$OLD" && ! -L "$OLD" ]]; then ok "refusal mutated nothing"; else bad "old path mutated by refused run"; fi

echo "=== phase 1: ref inventory drift aborts ==="
reset_home_state
OD="$FIX/od-drift"
mk_overdeck "$OD"
add_systray_module "$OD" ok
OLD="$TEST_HOME/Projects/systray-ai"
mk_old_repo "$OLD" systray-ai.service
plant_ref_inventory "$OD" systray-ai
mkdir -p "$TEST_HOME/.claude/skills/drift"
printf 'see ~/Projects/systray-ai/bin/start\n' >"$TEST_HOME/.claude/skills/drift/SKILL.md"
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
rm -rf "$TEST_HOME/.claude/skills/drift"
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'phase 1'; then
  ok "ref inventory drift aborts naming phase 1"
else
  bad "drift abort rc=$rc output=$out"
fi
if [[ -d "$OLD" && ! -L "$OLD" ]]; then ok "drift abort left old path intact"; else bad "old path mutated on drift abort"; fi

echo "=== phase 2: open handle aborts ==="
reset_home_state
OD="$FIX/od-busy"
mk_overdeck "$OD"
add_systray_module "$OD" ok
OLD="$TEST_HOME/Projects/systray-ai"
mk_old_repo "$OLD" systray-ai.service
plant_ref_inventory "$OD" systray-ai
set +e
out=$(FUSER_BUSY=1 run_cutover "$OD" systray 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'phase 2'; then
  ok "open handle aborts naming phase 2"
else
  bad "busy abort rc=$rc output=$out"
fi
if [[ -d "$OLD" && ! -L "$OLD" ]]; then ok "busy abort left old path intact"; else bad "old path mutated on busy abort"; fi

echo "=== phase 2: surviving process inside the checkout aborts ==="
reset_home_state
OD="$FIX/od-proc"
mk_overdeck "$OD"
add_systray_module "$OD" ok
OLD="$TEST_HOME/Projects/systray-ai"
mk_old_repo "$OLD" systray-ai.service
plant_ref_inventory "$OD" systray-ai
( cd "$OLD/runstate" && exec sleep 60 ) &
squatter=$!
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
kill "$squatter" 2>/dev/null
wait "$squatter" 2>/dev/null || true
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q "phase 2: pid $squatter"; then
  ok "process with cwd in the checkout aborts naming phase 2"
else
  bad "surviving process abort rc=$rc output=$out"
fi
if [[ -d "$OLD" && ! -L "$OLD" ]]; then ok "process abort left old path intact"; else bad "old path mutated on process abort"; fi

echo "=== full cutover: state relocated, symlink, units re-linked ==="
reset_home_state
OD="$FIX/od-ok"
mk_overdeck "$OD"
add_systray_module "$OD" ok
OLD="$TEST_HOME/Projects/systray-ai"
mk_old_repo "$OLD" systray-ai.service
plant_ref_inventory "$OD" systray-ai
before_hash=$(snapshot_tree "$OLD")
: >"$SYSTEMCTL_LOG"
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
ARCHIVE="$TEST_HOME/Projects/archive/systray-pre-ai-os"
UNIT="$TEST_HOME/.config/systemd/user/systray-ai.service"
STATE="$TEST_HOME/.local/state/overdeck/systray"

if [[ $rc -eq 0 ]]; then ok "cutover exits 0"; else bad "cutover rc=$rc: $out"; fi
if [[ -L "$OLD" && "$(readlink -f "$OLD")" == "$(readlink -f "$OD/modules/systray")" ]]; then
  ok "old path symlinked to module dir"
else
  bad "old path symlink wrong: $(ls -ld "$OLD" 2>&1)"
fi
if [[ -d "$ARCHIVE" && "$(snapshot_tree "$ARCHIVE")" == "$before_hash" ]]; then
  ok "old checkout archived byte-identical"
else
  bad "archive missing or drifted"
fi
if [[ "$(cat "$STATE/runstate/nested/data.txt" 2>/dev/null)" == "runstate-payload" ]] \
  && [[ "$(cat "$STATE/tmp/cache.bin" 2>/dev/null)" == "tmp-payload" ]]; then
  ok "state_dirs relocated under state root"
else
  bad "state relocation failed"
fi
relinked=0
for d in runstate tmp; do
  if [[ -L "$OD/modules/systray/$d" && "$(readlink "$OD/modules/systray/$d")" == "$STATE/$d" ]]; then
    relinked=$((relinked+1))
  else
    bad "in-place state dir $d not linked back: $(ls -ld "$OD/modules/systray/$d" 2>&1)"
  fi
done
if [[ $relinked -eq 2 ]]; then ok "in-place state dirs linked back to state root"; fi
if [[ "$(cat "$OD/modules/systray/runstate/nested/data.txt" 2>/dev/null)" == "runstate-payload" ]]; then
  ok "migrated state readable through the in-place link"
else
  bad "state not readable through in-place link"
fi
if jq -e '[.linked_state_dirs[].dir] == ["runstate","tmp"] and .status == "complete"' \
  "$TEST_HOME/.local/state/overdeck/cutover/systray.json" >/dev/null; then
  ok "state file records linked dirs and complete status"
else
  bad "state file wrong: $(cat "$TEST_HOME/.local/state/overdeck/cutover/systray.json" 2>&1)"
fi
if [[ "$(readlink "$UNIT")" == "$OD/modules/systray/systemd/systray-ai.service" ]]; then
  ok "unit re-linked to module dir"
else
  bad "unit link wrong: $(readlink "$UNIT" 2>&1)"
fi
if grep -q 'daemon-reload' "$SYSTEMCTL_LOG"; then ok "daemon-reload invoked"; else bad "daemon-reload missing"; fi
if grep -q 'start systray-ai.service' "$SYSTEMCTL_LOG"; then ok "unit started"; else bad "unit not started"; fi
if jq -e --arg p "$OLD" 'any(.[]; .old_path == $p)' "$OD/tools/ref-inventory.json" >/dev/null \
  && HOME="$TEST_HOME" bash "$OD/tools/ref-sweep.sh" --assert; then
  ok "inventoried legacy ref resolves through the new symlink"
else
  bad "phase 6 assertion vacuous or failing"
fi

echo "=== idempotent re-run ==="
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]] && printf '%s\n' "$out" | grep -qi 'already cut over'; then
  ok "already-cut module is a no-op"
else
  bad "idempotent re-run rc=$rc output=$out"
fi

echo "=== --rollback restores old checkout, state and unit ==="
set +e
out=$(run_cutover "$OD" systray --rollback 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]]; then ok "--rollback exits 0"; else bad "--rollback rc=$rc: $out"; fi
if [[ -d "$OLD" && ! -L "$OLD" ]]; then ok "--rollback restored old checkout"; else bad "old path not restored"; fi
if [[ "$(snapshot_tree "$OLD")" == "$before_hash" ]]; then
  ok "--rollback state byte-identical"
else
  bad "--rollback state drift"
fi
if [[ ! -e "$ARCHIVE" ]]; then ok "--rollback removed archive"; else bad "archive still present"; fi
if [[ "$(readlink "$UNIT")" == "$OLD/systemd/systray-ai.service" ]]; then
  ok "--rollback restored unit link"
else
  bad "unit link not restored: $(readlink "$UNIT" 2>&1)"
fi
if [[ ! -e "$OD/modules/systray/runstate" && ! -L "$OD/modules/systray/runstate" \
   && ! -e "$OD/modules/systray/tmp" && ! -L "$OD/modules/systray/tmp" ]]; then
  ok "--rollback removed the in-place state links"
else
  bad "in-place state links survived rollback: $(ls -ld "$OD/modules/systray"/{runstate,tmp} 2>&1)"
fi
if [[ ! -e "$TEST_HOME/.local/state/overdeck/cutover/systray.json" ]]; then
  ok "--rollback cleared the cutover state file"
else
  bad "state file survived a complete rollback"
fi

echo "=== phase 3 failure rolls back byte-identical ==="
reset_home_state
OD="$FIX/od-p3"
mk_overdeck "$OD"
add_systray_module "$OD" ok
OLD="$TEST_HOME/Projects/systray-ai"
mk_old_repo "$OLD" systray-ai.service
plant_ref_inventory "$OD" systray-ai
before_hash=$(snapshot_tree "$OLD")
mkdir -p "$TEST_HOME/.local/state/overdeck/systray/runstate"
printf 'stale\n' >"$TEST_HOME/.local/state/overdeck/systray/runstate/orphan.txt"
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
ARCHIVE="$TEST_HOME/Projects/archive/systray-pre-ai-os"
if [[ $rc -ne 0 ]]; then ok "phase 3 failure exits nonzero"; else bad "phase 3 failure rc=$rc: $out"; fi
if printf '%s\n' "$out" | grep -q 'phase 3'; then ok "failure names phase 3"; else bad "phase not named: $out"; fi
if printf '%s\n' "$out" | grep -qi 'rollback'; then ok "rollback reported"; else bad "rollback not reported: $out"; fi
if [[ -d "$OLD" && ! -L "$OLD" ]]; then ok "old path still a directory"; else bad "old path replaced on phase 3 failure"; fi
if [[ ! -e "$ARCHIVE" ]]; then ok "no archive created"; else bad "archive created on phase 3 failure"; fi
if [[ "$(snapshot_tree "$OLD")" == "$before_hash" ]]; then
  ok "state byte-identical after phase 3 rollback"
else
  bad "state drift after phase 3 rollback"
fi

echo "=== phase 5 health failure rolls back rename + state + unit ==="
reset_home_state
OD="$FIX/od-p5"
mk_overdeck "$OD"
add_systray_module "$OD" bad
OLD="$TEST_HOME/Projects/systray-ai"
mk_old_repo "$OLD" systray-ai.service
plant_ref_inventory "$OD" systray-ai
before_hash=$(snapshot_tree "$OLD")
UNIT="$TEST_HOME/.config/systemd/user/systray-ai.service"
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
ARCHIVE="$TEST_HOME/Projects/archive/systray-pre-ai-os"
if [[ $rc -ne 0 ]]; then ok "phase 5 failure exits nonzero"; else bad "phase 5 failure rc=$rc: $out"; fi
if printf '%s\n' "$out" | grep -q 'phase 5'; then ok "failure names phase 5"; else bad "phase 5 not named: $out"; fi
if [[ -d "$OLD" && ! -L "$OLD" ]]; then ok "old checkout restored from archive"; else bad "old path not restored"; fi
if [[ ! -e "$ARCHIVE" ]]; then ok "archive consumed by rollback"; else bad "archive left behind"; fi
if [[ "$(snapshot_tree "$OLD")" == "$before_hash" ]]; then
  ok "state byte-identical after phase 5 rollback"
else
  bad "state drift after phase 5 rollback"
fi
if [[ "$(readlink "$UNIT")" == "$OLD/systemd/systray-ai.service" ]]; then
  ok "unit link restored to old checkout"
else
  bad "unit link not restored: $(readlink "$UNIT" 2>&1)"
fi

echo "=== sequential cutover: serviceless module after a first cutover ==="
reset_home_state
OD="$FIX/od-seq"
mk_overdeck "$OD"
add_systray_module "$OD" ok
add_monitor_module "$OD"
OLD="$TEST_HOME/Projects/systray-ai"
MOLD="$TEST_HOME/Projects/system-monitor"
mk_old_repo "$OLD" systray-ai.service
mkdir -p "$MOLD/grafana"
printf 'monitor-config\n' >"$MOLD/grafana/dashboard.json"
plant_ref_inventory "$OD" systray-ai system-monitor
mold_hash=$(snapshot_tree "$MOLD")
: >"$SYSTEMCTL_LOG"
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]]; then ok "first module cuts over"; else bad "first cutover rc=$rc: $out"; fi
set +e
out=$(run_cutover "$OD" monitor 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]]; then
  ok "serviceless module cuts over after the first (no phase 1 drift)"
else
  bad "monitor cutover rc=$rc: $out"
fi
if [[ -L "$MOLD" && "$(readlink -f "$MOLD")" == "$(readlink -f "$OD/modules/monitor")" ]]; then
  ok "monitor path symlinked to module dir"
else
  bad "monitor symlink wrong: $(ls -ld "$MOLD" 2>&1)"
fi
if [[ "$(snapshot_tree "$TEST_HOME/Projects/archive/monitor-pre-ai-os")" == "$mold_hash" ]]; then
  ok "monitor checkout archived byte-identical"
else
  bad "monitor archive missing or drifted"
fi
if [[ ! -e "$TEST_HOME/.local/state/overdeck/monitor" ]]; then
  ok "no state root created for a module without state_dirs"
else
  bad "unexpected monitor state root"
fi
if ! grep -q 'monitor' "$SYSTEMCTL_LOG"; then
  ok "no unit touched for a module without services"
else
  bad "systemctl invoked for serviceless module"
fi

echo "=== phase 3: populated in-place state dir aborts without data loss ==="
reset_home_state
OD="$FIX/od-occupied"
mk_overdeck "$OD"
add_systray_module "$OD" ok
OLD="$TEST_HOME/Projects/systray-ai"
mk_old_repo "$OLD" systray-ai.service
plant_ref_inventory "$OD" systray-ai
mkdir -p "$OD/modules/systray/runstate"
printf 'occupant\n' >"$OD/modules/systray/runstate/keep.txt"
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'phase 3: .*non-empty directory'; then
  ok "populated in-place state dir aborts naming phase 3"
else
  bad "populated state dir abort rc=$rc output=$out"
fi
if [[ "$(cat "$OD/modules/systray/runstate/keep.txt" 2>/dev/null)" == "occupant" ]]; then
  ok "occupied state dir left untouched"
else
  bad "occupied state dir destroyed"
fi
if [[ -d "$OLD" && ! -L "$OLD" ]]; then ok "occupied-dir abort left old path intact"; else bad "old path mutated"; fi

echo "=== failed rollback marks partial state and blocks re-runs ==="
reset_home_state
OD="$FIX/od-partial"
mk_overdeck "$OD"
add_systray_module "$OD" ok
OLD="$TEST_HOME/Projects/systray-ai"
mk_old_repo "$OLD" systray-ai.service
plant_ref_inventory "$OD" systray-ai
MARKER="$TEST_HOME/.local/state/overdeck/cutover/systray.json"
before_hash=$(snapshot_tree "$OLD")
set +e
out=$(SYSTEMCTL_FAIL_START=1 run_cutover "$OD" systray 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'ROLLBACK INCOMPLETE'; then
  ok "failed rollback reports ROLLBACK INCOMPLETE"
else
  bad "induced rollback failure rc=$rc output=$out"
fi
if [[ -f "$MARKER" ]] && [[ "$(jq -r '.status' "$MARKER")" == "rollback-incomplete" ]]; then
  ok "partial state recorded in the cutover state file"
else
  bad "no partial marker: $(cat "$MARKER" 2>&1)"
fi
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] \
  && printf '%s\n' "$out" | grep -q 'rollback-incomplete' \
  && printf '%s\n' "$out" | grep -q 'archive=' \
  && printf '%s\n' "$out" | grep -q 'backup=' \
  && printf '%s\n' "$out" | grep -q "old_path=$OLD"; then
  ok "re-run after a failed rollback refuses and names archive, backup and old path"
else
  bad "re-run after failed rollback rc=$rc output=$out"
fi
set +e
out=$(run_cutover "$OD" systray --rollback 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]] && [[ ! -f "$MARKER" ]]; then
  ok "the --rollback the refusal directs the operator to clears the partial marker"
else
  bad "recovery rollback rc=$rc marker=$(cat "$MARKER" 2>&1) output=$out"
fi
if [[ -d "$OLD" && ! -L "$OLD" ]] && [[ "$(snapshot_tree "$OLD")" == "$before_hash" ]]; then
  ok "recovery rollback leaves the checkout byte-identical"
else
  bad "recovery rollback drifted the checkout"
fi
set +e
out=$(SYSTEMCTL_FAIL_START=1 run_cutover "$OD" systray 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && [[ "$(jq -r '.status' "$MARKER" 2>/dev/null)" == "rollback-incomplete" ]]; then
  ok "a second failed rollback re-marks the partial state"
else
  bad "second induced failure rc=$rc output=$out"
fi
rm -rf "$OLD"
ln -sfn "$OD/modules/systray" "$OLD"
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'rollback-incomplete'; then
  ok "already-cut re-run also refuses while partial state stands"
else
  bad "already-cut path fail-open rc=$rc output=$out"
fi
rm -f "$MARKER"
set +e
out=$(run_cutover "$OD" systray 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]] && printf '%s\n' "$out" | grep -qi 'already cut over'; then
  ok "clearing the marker restores normal operation"
else
  bad "marker override rc=$rc output=$out"
fi

echo "=== security: in-place cli module cuts over ==="
reset_home_state
OD="$FIX/od-sec"
mk_overdeck "$OD"
add_security_module "$OD"
SOLD="$TEST_HOME/Projects/security-gate"
mkdir -p "$SOLD/prevent"
printf 'gate-config\n' >"$SOLD/prevent/rules.json"
plant_ref_inventory "$OD" security-gate
sold_hash=$(snapshot_tree "$SOLD")
: >"$SYSTEMCTL_LOG"
set +e
out=$(run_cutover "$OD" security 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]]; then ok "security cutover exits 0"; else bad "security cutover rc=$rc: $out"; fi
for p in 1 2 3 5 6; do
  if printf '%s\n' "$out" | grep -q "phase $p"; then
    ok "security ran phase $p"
  else
    bad "security skipped phase $p: $out"
  fi
done
if [[ -L "$SOLD" && "$(readlink -f "$SOLD")" == "$(readlink -f "$OD/modules/security")" ]]; then
  ok "security path symlinked to module dir"
else
  bad "security symlink wrong: $(ls -ld "$SOLD" 2>&1)"
fi
if [[ "$(snapshot_tree "$TEST_HOME/Projects/archive/security-pre-ai-os")" == "$sold_hash" ]]; then
  ok "security checkout archived byte-identical"
else
  bad "security archive missing or drifted"
fi
if jq -e --arg p "$SOLD" 'any(.[]; .old_path == $p)' "$OD/tools/ref-inventory.json" >/dev/null \
  && HOME="$TEST_HOME" bash "$OD/tools/ref-sweep.sh" --assert; then
  ok "security legacy ref resolves through the new symlink"
else
  bad "security phase 6 assertion vacuous or failing"
fi
if ! grep -q 'security' "$SYSTEMCTL_LOG"; then
  ok "no unit touched for the serviceless security module"
else
  bad "systemctl invoked for security"
fi
set +e
out=$(run_cutover "$OD" security --rollback 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]] && [[ -d "$SOLD" && ! -L "$SOLD" ]] \
  && [[ "$(snapshot_tree "$SOLD")" == "$sold_hash" ]]; then
  ok "security --rollback restores the checkout byte-identical"
else
  bad "security rollback rc=$rc output=$out"
fi

echo "=== harness: surviving worktree aborts phase 4 ==="
reset_home_state
OD="$FIX/od-h-wt"
mk_overdeck "$OD"
add_harness_module "$OD"
mk_harness_artifact rev0001
OLD="$TEST_HOME/Projects/mega-plan-harness"
mk_old_repo "$OLD" harnessd.service
mkdir -p "$TEST_HOME/.harness/engine"
printf 'engine-state\n' >"$TEST_HOME/.harness/engine/CURRENT"
harness_home_hash=$(snapshot_tree "$TEST_HOME/.harness")
git_t init -q -b main "$OLD"
git_t -C "$OLD" add -A
git_t -C "$OLD" commit -qm fixture
git_t -C "$OLD" worktree add -q --detach "$OLD/.wt-demo-int" >/dev/null
printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_HOME/.claude/bin/wt-reaper.sh"
chmod +x "$TEST_HOME/.claude/bin/wt-reaper.sh"
plant_ref_inventory "$OD" mega-plan-harness
before_hash=$(snapshot_tree "$OLD")
set +e
out=$(run_cutover "$OD" harness 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'phase 4'; then
  ok "surviving worktree aborts naming phase 4"
else
  bad "phase 4 abort rc=$rc output=$out"
fi
if [[ -d "$OLD" && ! -L "$OLD" ]]; then ok "phase 4 abort left checkout in place"; else bad "checkout moved on phase 4 abort"; fi

echo "=== harness: reaped worktrees complete the cutover ==="
cat >"$TEST_HOME/.claude/bin/wt-reaper.sh" <<'REAPER'
#!/usr/bin/env bash
set -euo pipefail
root=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --root) root="$2"; shift 2 ;;
    *) shift ;;
  esac
done
[[ -n "$root" ]] || exit 2
rm -rf "$root"/.wt-*-int
REAPER
chmod +x "$TEST_HOME/.claude/bin/wt-reaper.sh"
: >"$SYSTEMCTL_LOG"
set +e
out=$(run_cutover "$OD" harness 2>&1)
rc=$?
set -e
HUNIT="$TEST_HOME/.config/systemd/user/harnessd.service"
ART="$TEST_HOME/.local/opt/overdeck/harness/current"
if [[ $rc -eq 0 ]]; then ok "harness cutover exits 0"; else bad "harness cutover rc=$rc: $out"; fi
if [[ -L "$OLD" && "$(readlink -f "$OLD")" == "$(readlink -f "$ART")" ]]; then
  ok "old harness path symlinked to artifact current"
else
  bad "harness symlink wrong: $(ls -ld "$OLD" 2>&1)"
fi
if [[ "$(readlink "$HUNIT")" == "$ART/systemd/harnessd.service" ]]; then
  ok "harness unit re-linked to artifact"
else
  bad "harness unit link wrong: $(readlink "$HUNIT" 2>&1)"
fi
if [[ "$(cat "$TEST_HOME/.local/state/overdeck/harness/runstate/nested/data.txt" 2>/dev/null)" == "runstate-payload" ]]; then
  ok "harness state relocated"
else
  bad "harness state relocation failed"
fi
if [[ "$(snapshot_tree "$TEST_HOME/.harness")" == "$harness_home_hash" ]]; then
  ok "HARNESS_HOME untouched"
else
  bad "HARNESS_HOME modified"
fi
inv_count=$(find "$TEST_HOME/.local/state/overdeck/backups" -name 'harness-home-inventory.txt' | wc -l)
if [[ "$inv_count" -ge 1 ]]; then ok "HARNESS_HOME inventoried into backup"; else bad "no HARNESS_HOME inventory"; fi

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