#!/usr/bin/env bash
# Tests for tool-confine.sh (tool-shell migration) and tool-confine-install.sh
# (snapshot patching). Run: bash modules/workstation/claude/lib/test-tool-confine.sh
set -uo pipefail

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
confine="$here/tool-confine.sh"

# Once deployed, the shell running this suite is itself confined and every child inherits
# that scope, which would make the migration assertions pass without migrating anything.
# Re-run the suite from a scope of its own so each case starts unconfined.
self_cg=$(tr -d '\0' < /proc/self/cgroup)
if [[ "$self_cg" == */confine-* && -z "${TOOL_CONFINE_TEST_SCOPED:-}" ]]; then
  exec systemd-run --user --quiet --scope --unit="tcfn-test-$$.scope" --slice=agent.slice \
    env TOOL_CONFINE_TEST_SCOPED=1 bash "${BASH_SOURCE[0]}" "$@"
fi
install="$here/tool-confine-install.sh"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
ok() { printf 'ok - %s\n' "$*"; }

# --- PATH repair -----------------------------------------------------------------

path=$(HOME="$tmp/home" PATH=/usr/bin:/bin CONFINE_TOOL_SHELL=0 bash -c ". '$confine'; printf '%s' \"\$PATH\"")
[[ "$path" == "$tmp/home/.claude/bin:/usr/bin:/bin" ]] || fail "tool shim directory not prepended: $path"
ok "prepends the agent tool and safety-shim directory"

path=$(HOME="$tmp/home" PATH="$tmp/home/.claude/bin:/usr/bin:/bin" CONFINE_TOOL_SHELL=0 bash -c ". '$confine'; printf '%s' \"\$PATH\"")
[[ "$path" == "$tmp/home/.claude/bin:/usr/bin:/bin" ]] || fail "tool shim directory duplicated: $path"
ok "PATH repair is idempotent"

# --- migration -------------------------------------------------------------------

cg=$(bash -c ". '$confine'; read -r c < /proc/self/cgroup; printf '%s' \"\$c\"")
case "$cg" in
  *"/agent.slice/confine-agent-"*".scope") ok "tool shell lands in agent.slice: ${cg##*/}" ;;
  *) fail "expected a confine scope under agent.slice, got: $cg" ;;
esac

child=$(bash -c ". '$confine'; bash -c 'read -r c < /proc/self/cgroup; printf \"%s\" \"\$c\"'")
case "$child" in
  *"/agent.slice/confine-agent-"*".scope") ok "children inherit the scope" ;;
  *) fail "child escaped the scope: $child" ;;
esac

# The guard must kill what lands there: same predicate the machine-side killer uses.
pids_cgroup="$here/../../../monitor/lib/pids_cgroup.py"
if [[ -r "$pids_cgroup" ]]; then
  rel="${cg#0::}"
  python3 -c "
import sys, importlib.util
spec = importlib.util.spec_from_file_location('pids_cgroup', '$pids_cgroup')
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
sys.exit(0 if m.is_killable('$rel', $(id -u)) else 1)
" || fail "is_killable() rejects the scope this creates: $rel"
  ok "is_killable() accepts the scope"
else
  fail "pids_cgroup.py not found at $pids_cgroup"
fi

# The session process is the tool shell's PARENT: a tool shell that migrates must never
# drag it along. This is the owner's absolute constraint — the session stays unkillable.
parent_cg=$(bash -c "read -r before < /proc/self/cgroup; bash -c \". '$confine'\" ; read -r after < /proc/self/cgroup; printf '%s|%s' \"\$before\" \"\$after\"")
[[ "${parent_cg%%|*}" == "${parent_cg##*|}" ]] || fail "a child's migration moved its parent: $parent_cg"
ok "a migrating child leaves its parent's cgroup untouched"

# --- fail-open -------------------------------------------------------------------

out=$(PATH=/nonexistent /bin/bash -c ". '$confine'; /bin/echo RAN" 2>&1)
[[ "$out" == RAN ]] || fail "no busctl on PATH must still run the command, got: $out"
ok "fails open when busctl is absent"

out=$(DBUS_SESSION_BUS_ADDRESS=unix:path=/nonexistent/bus XDG_RUNTIME_DIR=/nonexistent \
  bash -c ". '$confine'; echo RAN" 2>&1)
[[ "$out" == RAN ]] || fail "unreachable user manager must still run the command, got: $out"
ok "fails open when the user manager is unreachable"

ambient=$(bash -c 'read -r c < /proc/self/cgroup; printf "%s" "$c"')
out=$(CONFINE_TOOL_SHELL=0 bash -c ". '$confine'; read -r c < /proc/self/cgroup; printf '%s' \"\$c\"")
[[ "$out" == "$ambient" ]] || fail "CONFINE_TOOL_SHELL=0 did not disable migration: $out"
ok "CONFINE_TOOL_SHELL=0 disables migration"

# Sourcing twice must not strand a scope with no pids.
out=$(bash -c ". '$confine'; first=\$(sed -n 1p /proc/self/cgroup); . '$confine'; second=\$(sed -n 1p /proc/self/cgroup); [[ \"\$first\" == \"\$second\" ]] && echo SAME || echo MOVED")
[[ "$out" == SAME ]] || fail "re-sourcing migrated a second time"
ok "re-sourcing is a no-op"

# --- installer -------------------------------------------------------------------

snap="$tmp/home/.claude/shell-snapshots"
mkdir -p "$snap" "$tmp/home/.claude/lib"
cp "$confine" "$tmp/home/.claude/lib/tool-confine.sh"
printf 'alias x=y\nexport PATH=/usr/bin\n' > "$snap/complete.sh"
printf 'alias x=y\nfunction half() {\n' > "$snap/partial.sh"
printf 'export PATH=/usr/bin\nshopt -u xpg_echo\n' > "$snap/oldformat.sh"
touch -d '1 hour ago' "$snap/oldformat.sh"
printf 'export PATH=/usr/bin\n. "$HOME/.claude/lib/tool-confine.sh" 2>/dev/null || true  # overdeck:tool-confine\n' > "$snap/already.sh"

HOME="$tmp/home" bash "$install" --verbose > "$tmp/out1" 2>&1 || fail "installer exited non-zero: $(<"$tmp/out1")"
grep -q 'patched 2 snapshot' "$tmp/out1" || fail "expected exactly 2 patches, got: $(<"$tmp/out1")"
grep -q 'overdeck:tool-confine' "$snap/complete.sh" || fail "complete snapshot not patched"
grep -q 'overdeck:tool-confine' "$snap/oldformat.sh" || fail "settled older-format snapshot not patched"
grep -q 'overdeck:tool-confine' "$snap/partial.sh" && fail "half-written snapshot must be skipped"
[[ $(grep -c 'overdeck:tool-confine' "$snap/already.sh") == 1 ]] || fail "already-patched snapshot patched twice"
ok "installer patches complete snapshots only"

HOME="$tmp/home" bash "$install" --verbose > "$tmp/out2" 2>&1
grep -q 'patched 0 snapshot' "$tmp/out2" || fail "second run was not idempotent: $(<"$tmp/out2")"
ok "installer is idempotent"

HOME="$tmp/empty" bash "$install" || fail "missing snapshot dir must exit 0"
ok "missing snapshot dir is a no-op"

# A patched snapshot must still be sourceable and must confine.
cg2=$(HOME="$tmp/home" bash -c ". '$snap/complete.sh' 2>/dev/null || true; read -r c < /proc/self/cgroup; printf '%s' \"\$c\"")
case "$cg2" in
  *"/agent.slice/confine-agent-"*) ok "sourcing a patched snapshot confines the shell" ;;
  *) fail "patched snapshot did not confine: $cg2" ;;
esac

printf '\nall tests passed\n'
