#!/usr/bin/env bash
# Fixture test for lib/deckctl/agents.sh filters and the stop safety rail.
set -uo pipefail
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
pass=0; fail=0
ok()  { echo "  PASS: $1"; pass=$((pass+1)); }
bad() { echo "  FAIL: $1"; fail=$((fail+1)); }

die() { echo "deckctl: ERROR: $*" >&2; exit 1; }
# shellcheck source=/dev/null
source "$ROOT/lib/deckctl/agents.sh"

# --- local filter: matches agent scopes, rejects everything else
out=$(agents_filter_local <<'EOF'
confine-agent-3961590-9381.scope
confine-build-521904-11758.scope
v2-child-dispatch-t1-3105035-4.scope
app-tmux-spawn.scope
session-42.scope
confine-agent-notascope.service
EOF
)
expected=$'confine-agent-3961590-9381.scope\nconfine-build-521904-11758.scope\nv2-child-dispatch-t1-3105035-4.scope'
[ "$out" = "$expected" ] && ok "local filter keeps agent scopes only" || bad "local filter: got [$out]"

# --- local filter: empty input stays empty, exit 0
out=$(agents_filter_local </dev/null); rc=$?
[ -z "$out" ] && [ "$rc" -eq 0 ] && ok "local filter empty input" || bad "local filter empty input rc=$rc out=[$out]"

# --- remote filter: slice members + seat-user container scopes, exempt interactive user's own session scopes
out=$(agents_filter_remote_paths 1000 <<'EOF'
/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/agent.slice/run-abc.scope
/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/agent-seat.slice/seat-1.scope
/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/build.slice/run-b.scope
/sys/fs/cgroup/user.slice/user-998.slice/user@998.service/user.slice/libpod-4f99.scope
/sys/fs/cgroup/user.slice/user-998.slice/user@998.service/user.slice/run-p375.scope
/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/session-9.scope
/sys/fs/cgroup/user.slice/user-1000.slice/session-140332.scope
/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-tmux.scope
EOF
)
want_lines=5
got_lines=$(printf '%s\n' "$out" | grep -c . || true)
echo "$out" | grep -q "libpod-4f99" && echo "$out" | grep -q "agent-seat.slice/seat-1" \
  && ! echo "$out" | grep -q "session-" && ! echo "$out" | grep -q "app-tmux" \
  && [ "$got_lines" -eq "$want_lines" ] \
  && ok "remote filter keeps agent slices + seat containers, drops sessions/apps" \
  || bad "remote filter: got [$out]"

# --- remote filter: seat-user run scopes kept even for uids other than 998
out=$(agents_filter_remote_paths 1000 <<'EOF'
/sys/fs/cgroup/user.slice/user-997.slice/user@997.service/user.slice/libpod-aa.scope
EOF
)
[ -n "$out" ] && ok "remote filter matches any seat uid" || bad "remote filter uid generality"

# --- stop safety rail: refuses cgroup paths outside user.slice
rc=0
( agents_stop_remote_path 22 u "" h "/sys/fs/cgroup/system.slice/ssh.service" ) >/dev/null 2>&1 || rc=$?
[ "$rc" -ne 0 ] && ok "stop refuses non-user.slice path" || bad "stop accepted system.slice path"

# --- stop safety rail: hostile scope-name chars refused locally
agents_ssh() { echo "SSH_REACHED" >&2; return 99; }
hostile_ok=1
assert_hostile_refused() {
  local hp=$1 label=$2 rc=0 err
  err=$( ( agents_stop_remote_path 22 u "" h "$hp" ) 2>&1 >/dev/null ) || rc=$?
  if [ "$rc" -eq 0 ] || ! printf '%s\n' "$err" | grep -q "refuse kill path with unsafe characters"; then
    bad "hostile path ($label) not refused locally: rc=$rc err=[$err]"; hostile_ok=0; return
  fi
  case "$err" in *SSH_REACHED*) bad "hostile path ($label) reached agents_ssh"; hostile_ok=0; return ;; esac
  case "$err" in *"$hp"*) ;; *) bad "hostile path ($label) not named in error: err=[$err]"; hostile_ok=0; return ;; esac
}
assert_hostile_refused "/sys/fs/cgroup/user.slice/user-1.slice/foo';id;.scope" "quote/semicolon"
assert_hostile_refused '/sys/fs/cgroup/user.slice/user-1.slice/foo`id`.scope' "backtick"
assert_hostile_refused "/sys/fs/cgroup/user.slice/user-1.slice/foo bar.scope" "space"
assert_hostile_refused $'/sys/fs/cgroup/user.slice/user-1.slice/foo\nbar.scope' "newline"
[ "$hostile_ok" -eq 1 ] && ok "stop refuses hostile path chars locally (quote/backtick/space/newline)"
unset -f agents_ssh

# --- remote kill scriptlet: rejects outside user.slice
_transport_kill=$(mktemp -d)
_fake_cgroup="$_transport_kill/system.slice/ssh.service"
mkdir -p "$_fake_cgroup"
rc=0
printf '%s\0' "$_fake_cgroup" \
  | xargs -0 sh -c 'case "$1" in /sys/fs/cgroup/user.slice/*) printf 1 > "$1"/cgroup.kill;; *) exit 2;; esac' _ \
  || rc=$?
[ "$rc" -ne 0 ] && [ ! -f "$_fake_cgroup/cgroup.kill" ] \
  && ok "remote kill scriptlet rejects non-user.slice prefix" \
  || bad "remote kill scriptlet prefix reject: rc=$rc kill=$([ -f "$_fake_cgroup/cgroup.kill" ] && echo yes || echo no)"
rm -rf "$_transport_kill"

# --- valid path: NUL-datum transport
_transport_dir=$(mktemp -d)
agents_ssh() {
  shift 4
  printf '%s' "$*" > "$_transport_dir/cmd"
  cat > "$_transport_dir/stdin"
  return 0
}
rc=0
agents_stop_remote_path 22 u "" h "/sys/fs/cgroup/user.slice/user-1.slice/agent.slice/run-ok.scope" || rc=$?
got_cmd=$(cat "$_transport_dir/cmd" 2>/dev/null || true)
got_stdin=$(tr -d '\0' < "$_transport_dir/stdin" 2>/dev/null || true)
rm -rf "$_transport_dir"
unset -f agents_ssh
[ "$rc" -eq 0 ] \
  && [ "$got_stdin" = "/sys/fs/cgroup/user.slice/user-1.slice/agent.slice/run-ok.scope" ] \
  && [[ "$got_cmd" == *"xargs -0 sh -c"* ]] \
  && [[ "$got_cmd" != *run-ok.scope* ]] \
  && ok "valid path sent as NUL stdin, not embedded in remote shell string" \
  || bad "valid path transport: rc=$rc stdin=[$got_stdin] cmd=[$got_cmd]"

# --- human exemption: a tty anywhere in the scope means the owner is attached.
# Both branches use real processes and real /proc/<pid>/stat, not a mocked reader.
_tty_dir=$(mktemp -d)
setsid sleep 30 >/dev/null 2>&1 &
_headless_pid=$!
script -qec "sleep 30 & echo \$! > $_tty_dir/pid; sleep 6" /dev/null >/dev/null 2>&1 &
_script_pid=$!
for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$_tty_dir/pid" ] && break; sleep 0.3; done
_tty_pid=$(cat "$_tty_dir/pid" 2>/dev/null || true)

printf '%s\n' "$_headless_pid" > "$_tty_dir/agent.procs"
if [ -n "$_tty_pid" ]; then
  printf '%s\n%s\n' "$_headless_pid" "$_tty_pid" > "$_tty_dir/human.procs"
fi

agents_procs_is_agent "$_tty_dir/agent.procs" \
  && ok "dispatched seat (no tty in scope) classified agent" \
  || bad "headless scope misclassified as human"

if [ -n "$_tty_pid" ]; then
  agents_procs_is_agent "$_tty_dir/human.procs" \
    && bad "interactive session (tty in scope) would be listed and STOPPED" \
    || ok "interactive session (tty in scope) exempt from list and stop"
else
  bad "could not obtain a pty-attached pid — human-exemption branch unproven"
fi

agents_procs_is_agent "$_tty_dir/does-not-exist.procs" \
  && bad "unreadable procs classified agent (would kill on no evidence)" \
  || ok "unreadable procs resolves to human, never to a kill"

kill "$_headless_pid" "$_script_pid" 2>/dev/null
[ -n "$_tty_pid" ] && kill "$_tty_pid" 2>/dev/null
wait "$_headless_pid" "$_script_pid" 2>/dev/null
rm -rf "$_tty_dir"

# --- dispatcher: agents registered, bad subcommand fails closed
out=$("$ROOT/bin/deckctl" agents frobnicate 2>&1); rc=$?
[ "$rc" -ne 0 ] && echo "$out" | grep -q "usage: deckctl agents" && ok "dispatcher routes agents, bad sub fails closed" || bad "dispatcher agents: rc=$rc out=[$out]"

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