#!/usr/bin/env bash
# Regression tests for generate-tool-shims / the generated PATH shims (bin/ccr,
# bin/podman, bin/systemctl) — the remote-runtime equivalent of deny-gate.mjs for
# the subset of tools.json rules that opt in via "shim_binary".
#
# Not exhaustion-class: unlike _git-guard-shim.sh these shims never re-exec `git`
# itself, so a bad pin cannot fork-loop. Runs directly, no danger-lab needed.
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
GEN="$ROOT/bin/generate-tool-shims"
PASS=0; FAIL=0
ok()  { PASS=$((PASS+1)); printf 'PASS %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf 'FAIL %s\n     %s\n' "$1" "$2"; }

TMP=$(mktemp -d "${TMPDIR:-/tmp}/gts-test-XXXX")
trap 'rm -rf "$TMP"' EXIT

pin_fake() { # $1=shimdir $2=home $3=binary -> writes a logging stub and pins it
  local shimdir="$1" home="$2" bin="$3" log
  log="$TMP/${bin}.calls"
  mkdir -p "$home/.local/state/overdeck/shim-real" "$TMP/realbin"
  cat > "$TMP/realbin/$bin" <<EOF
#!/usr/bin/env bash
echo "CALL \$*" >> "$log"
EOF
  chmod +x "$TMP/realbin/$bin"
  printf '%s\n' "$TMP/realbin/$bin" > "$home/.local/state/overdeck/shim-real/$bin"
}

run_shim() { # $1=home $2=binary $3=scope(0/1) ...args -> stdout captured; sets RC
  local home="$1" bin="$2" scope="$3"; shift 3
  if [[ "$scope" == 1 ]]; then
    OUT=$(HOME="$home" AGENT_BUILD_SCOPE_ACTIVE=1 "$ROOT/bin/$bin" "$@" 2>&1)
  else
    OUT=$(HOME="$home" "$ROOT/bin/$bin" "$@" 2>&1)
  fi
  RC=$?
}

# --- I. generator invariants -------------------------------------------------
"$GEN" >/tmp/gts-gen-out.$$ 2>&1
GEN_RC=$?
[[ $GEN_RC -eq 0 ]] && ok "generate-tool-shims exits 0" || bad "generate-tool-shims exits 0" "rc=$GEN_RC"

"$GEN" --check >/dev/null 2>&1
[[ $? -eq 0 ]] && ok "--check clean immediately after generation (idempotent)" \
  || bad "--check clean immediately after generation" "generated output does not match itself"

for b in ccr podman systemctl; do
  bash -n "$ROOT/bin/$b" 2>/tmp/gts-syn.$$ \
    && ok "bin/$b is syntactically valid bash" \
    || bad "bin/$b is syntactically valid bash" "$(cat /tmp/gts-syn.$$)"
  grep -q OD_TOOL_SHIM_MARKER "$ROOT/bin/$b" \
    && ok "bin/$b carries OD_TOOL_SHIM_MARKER" || bad "bin/$b carries OD_TOOL_SHIM_MARKER" "missing"
done

# A rule with all_of only (no any_of) must never silently get shim_binary honored —
# this is the "malformed/unsupported rule shape" fail-closed path.
python3 - "$GEN" <<'PY'
import json, subprocess, sys, tempfile, pathlib
gen = sys.argv[1]
root = pathlib.Path(gen).resolve().parent.parent
tools = json.loads((root / "tools.json").read_text())
bad_rule = {"id": "test-all-of-only", "shim_binary": "nonexistent-test-binary", "all_of": ["x"], "wrapper": "w", "reason": "r"}
tools["rules"].append(bad_rule)
with tempfile.TemporaryDirectory() as td:
    tp = pathlib.Path(td)
    (tp / "tools.json").write_text(json.dumps(tools))
    (tp / "bin").mkdir()
    (tp / "lib").mkdir()
    import importlib.util
    from importlib.machinery import SourceFileLoader
    loader = SourceFileLoader("gts", gen)
    spec = importlib.util.spec_from_loader("gts", loader)
    mod = importlib.util.module_from_spec(spec)
    loader.exec_module(mod)
    mod.TOOLS_JSON = tp / "tools.json"
    generated, snapshot = mod.build(tp / "bin", tp / "lib")
    assert "nonexistent-test-binary" not in generated, "all_of-only rule must not be shimmed"
    assert any(a["rule"] == "test-all-of-only" for a in snapshot["audit_skipped"]), "must appear in audit_skipped"
print("OK")
PY
[[ $? -eq 0 ]] && ok "all_of-only shim_binary rule is rejected to audit_skipped, not silently shimmed" \
  || bad "all_of-only shim_binary rule is rejected to audit_skipped, not silently shimmed" "python check failed"

# Malformed tools.json: generator fails loudly and writes nothing into the target dirs
# (proves the "unreadable registry never bricks already-generated shims" contract —
# the shims already committed on disk are simply untouched by a failed regeneration).
python3 - "$GEN" <<'PY'
import subprocess, sys, tempfile, pathlib, importlib.util
gen = sys.argv[1]
root = pathlib.Path(gen).resolve().parent.parent
with tempfile.TemporaryDirectory() as td:
    tp = pathlib.Path(td)
    (tp / "tools.json").write_text("{ not json")
    (tp / "bin").mkdir()
    (tp / "lib").mkdir()
    from importlib.machinery import SourceFileLoader
    loader = SourceFileLoader("gts2", gen)
    spec = importlib.util.spec_from_loader("gts2", loader)
    mod = importlib.util.module_from_spec(spec)
    loader.exec_module(mod)
    mod.TOOLS_JSON = tp / "tools.json"
    try:
        mod.build(tp / "bin", tp / "lib")
        sys.exit(1)  # must not reach here
    except SystemExit as e:
        assert e.code != 0, "malformed tools.json must exit non-zero"
    assert not any((tp / "bin").iterdir()), "malformed registry must not write any shim"
print("OK")
PY
[[ $? -eq 0 ]] && ok "malformed tools.json fails the generator loudly and writes nothing" \
  || bad "malformed tools.json fails the generator loudly and writes nothing" "python check failed"

# --- II. shim behavior: deny match -------------------------------------------
H1="$TMP/home1"; pin_fake "$ROOT/bin" "$H1" ccr
run_shim "$H1" ccr 1 start
if [[ $RC -eq 76 ]] && [[ "$OUT" == *"ccr-start-raw"* ]] && [[ "$OUT" == *"ccr-up.sh"* ]] && [[ ! -f "$TMP/ccr.calls" ]]; then
  ok "ccr start (agent scope) is denied, exit 76, real ccr never called"
else
  bad "ccr start (agent scope) is denied, exit 76, real ccr never called" "rc=$RC out=$OUT"
fi

# --- III. shim behavior: unless_contains escape hatch passes through --------
run_shim "$H1" ccr 1 start --via ccr-up.sh
if [[ $RC -eq 0 ]] && grep -q "CALL start --via ccr-up.sh" "$TMP/ccr.calls" 2>/dev/null; then
  ok "ccr start --via ccr-up.sh (unless_contains) passes through to real ccr"
else
  bad "ccr start --via ccr-up.sh (unless_contains) passes through to real ccr" "rc=$RC log=$(cat "$TMP/ccr.calls" 2>/dev/null)"
fi

# --- IV. shim behavior: safe verb passthrough --------------------------------
run_shim "$H1" ccr 1 status
if [[ $RC -eq 0 ]] && grep -q "CALL status" "$TMP/ccr.calls" 2>/dev/null; then
  ok "ccr status passes through to real ccr"
else
  bad "ccr status passes through to real ccr" "rc=$RC log=$(cat "$TMP/ccr.calls" 2>/dev/null)"
fi

# --- V. shim behavior: no AGENT_BUILD_SCOPE_ACTIVE -> real binary decides ----
# (this repo's own test runner is itself agent-launched, so AGENT_BUILD_SCOPE_ACTIVE
# is already 1 in the ambient environment; passing scope=0 to run_shim only omits
# setting it explicitly, it does not unset an inherited one — assert the CALL still
# reaches the real binary rather than asserting on the gate's verdict.)
run_shim "$H1" ccr 0 status
grep -q "CALL status" "$TMP/ccr.calls" 2>/dev/null \
  && ok "ccr status reaches the real binary regardless of scope var" \
  || bad "ccr status reaches the real binary regardless of scope var" "log=$(cat "$TMP/ccr.calls" 2>/dev/null)"

# --- VI. unpinned binary refuses closed, never falls through ----------------
H2="$TMP/home2"; mkdir -p "$H2"
run_shim "$H2" ccr 1 start
if [[ $RC -eq 78 ]] && [[ "$OUT" == *"install-tool-shims-real"* ]]; then
  ok "unpinned binary refuses (exit 78) and names the fix, never falls through"
else
  bad "unpinned binary refuses (exit 78) and names the fix, never falls through" "rc=$RC out=$OUT"
fi

# --- VII. podman deny + unless_contains --------------------------------------
H3="$TMP/home3"; pin_fake "$ROOT/bin" "$H3" podman
run_shim "$H3" podman 1 run --rm alpine
[[ $RC -eq 76 && "$OUT" == *"deck-podman"* ]] \
  && ok "podman run (raw) is denied with deck-podman guidance" \
  || bad "podman run (raw) is denied with deck-podman guidance" "rc=$RC out=$OUT"
run_shim "$H3" podman 1 run --rm alpine --note deck-podman
[[ $RC -eq 0 ]] && grep -q "CALL run --rm alpine --note deck-podman" "$TMP/podman.calls" 2>/dev/null \
  && ok "podman run naming deck-podman in argv passes through" \
  || bad "podman run naming deck-podman in argv passes through" "rc=$RC"

# --- VIII. systemctl deny + safe passthrough ---------------------------------
H4="$TMP/home4"; pin_fake "$ROOT/bin" "$H4" systemctl
run_shim "$H4" systemctl 1 --user set-property agent.slice CPUQuota=150
[[ $RC -eq 76 && "$OUT" == *"CPUWeight"* ]] \
  && ok "systemctl CPUQuota on agent.slice is denied with CPUWeight guidance" \
  || bad "systemctl CPUQuota on agent.slice is denied with CPUWeight guidance" "rc=$RC out=$OUT"
run_shim "$H4" systemctl 1 --user status agent.slice
[[ $RC -eq 0 ]] && grep -q "CALL --user status agent.slice" "$TMP/systemctl.calls" 2>/dev/null \
  && ok "systemctl status agent.slice passes through" \
  || bad "systemctl status agent.slice passes through" "rc=$RC"

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