#!/usr/bin/env bash
# Regression test for fs-schema-guard.sh.
#
# Covers:
#   1. Non-Write/Edit/NotebookEdit tool                    -> ALLOW (early exit)
#   2. Write to projects/Acme/a.txt (flat)                 -> ALLOW
#   3. Write to documents/a.pdf (loose)                    -> ALLOW
#   4. Write to output/deep/nested/x.png (tool-owned)      -> ALLOW (any depth)
#   5. Write to projects/Acme/deploy/sources/a.txt         -> BLOCK over-deep
#   6. Write to projects/Acme/deploy/a.txt                 -> BLOCK over-deep
#   7. Write to whim/x.txt (novel top-level)               -> BLOCK top-level
#   8. Write to an absolute path outside the account dir   -> ALLOW (not our concern)
#   9. NotebookEdit over-deep (notebook_path)              -> BLOCK over-deep
#  10. Empty stdin (cannot inspect)                        -> BLOCK fail-closed
#  11. Allowed-set parse matches the documented set        -> assertion
set -u

HOOK="$(cd "$(dirname "$0")/.." && pwd)/fs-schema-guard.sh"
[ -x "$HOOK" ] || { echo "FAIL: $HOOK not executable" >&2; exit 1; }

# A scratch account dir with a seeded SCHEMA.md (copied from the shipped template).
REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
TEMPLATE="$REPO_ROOT/templates/account-schema/SCHEMA.md"
[ -f "$TEMPLATE" ] || { echo "FAIL: template missing at $TEMPLATE" >&2; exit 1; }
ACCT=$(mktemp -d)
trap 'rm -rf "$ACCT"' EXIT
cp "$TEMPLATE" "$ACCT/SCHEMA.md"
mkdir -p "$ACCT/projects/acme" "$ACCT/documents" "$ACCT/output"

# A second account modelling a provisioned construction account: jobs/ is a
# derived domain bucket (the generator appends it to the allowed block, like a
# plugin-owned dir). Kept separate so the template-parity assertion below still
# checks the pristine shipped set.
ACCT_D=$(mktemp -d)
trap 'rm -rf "$ACCT" "$ACCT_D"' EXIT
cp "$TEMPLATE" "$ACCT_D/SCHEMA.md"
awk '1; /^```allowed-top-level$/{print "jobs"}' "$ACCT_D/SCHEMA.md" > "$ACCT_D/SCHEMA.md.tmp" && mv "$ACCT_D/SCHEMA.md.tmp" "$ACCT_D/SCHEMA.md"
mkdir -p "$ACCT_D/jobs/5-oaktree/Quotations"

PASS=0; FAIL=0
# Run one case in the account dir as cwd.
#   $1 name  $2 stdin  $3 expected_exit  $4 expected_log_re ("" = none)
run_case() {
  local name="$1" stdin="$2" exp="$3" re="$4" acct="${5:-$ACCT}" ef actual
  ef=$(mktemp)
  ( cd "$acct" && printf '%s' "$stdin" | bash "$HOOK" >/dev/null 2>"$ef" )
  actual=$?
  local sc; sc=$(cat "$ef"); rm -f "$ef"
  local ok=1
  [ "$actual" -eq "$exp" ] || ok=0
  if [ -n "$re" ] && ! printf '%s' "$sc" | grep -Eq "$re"; then ok=0; fi
  if [ $ok -eq 1 ]; then echo "PASS: $name (exit=$actual)"; PASS=$((PASS+1));
  else echo "FAIL: $name (exit=$actual, want $exp; log want /$re/)" >&2; FAIL=$((FAIL+1)); fi
}

mkenv() { printf '{"hook_event_name":"PreToolUse","tool_name":"%s","tool_input":{"%s":"%s"}}' "$1" "$2" "$3"; }

run_case "non-write allow"       "$(mkenv Bash command 'ls')"                                  0 ""
run_case "flat project allow"    "$(mkenv Write file_path 'projects/acme/a.txt')"              0 ""
run_case "loose document allow"  "$(mkenv Write file_path 'documents/a.pdf')"                  0 ""
run_case "tool-owned deep allow" "$(mkenv Write file_path 'output/deep/nested/x.png')"         0 ""
run_case "over-deep block"       "$(mkenv Write file_path 'projects/acme/deploy/sources/a.txt')" 2 "fs-guard. blocked path=projects/acme/deploy/sources/a.txt reason=over-deep"
run_case "over-deep one block"   "$(mkenv Write file_path 'projects/acme/deploy/a.txt')"       2 "reason=over-deep"
run_case "top-level block"       "$(mkenv Write file_path 'whim/x.txt')"                       2 "fs-guard. blocked path=whim/x.txt reason=top-level"
run_case "outside acct allow"    "$(mkenv Write file_path '/etc/passwd')"                      0 ""
run_case "notebook over-deep"    "$(mkenv NotebookEdit notebook_path 'projects/acme/deploy/n.ipynb')" 2 "reason=over-deep"
run_case "empty stdin block"     ""                                                            2 "no stdin"

# Name convention (Task 1886): operator-authored segments under projects/contacts/
# documents must be [a-z0-9-] (folders) or lowercase dot-segments (files). Checked
# AFTER top-level and depth, so an over-deep path still reports over-deep first.
run_case "bad folder space block" "$(mkenv Write file_path 'projects/Bad Name/x.txt')"          2 "fs-guard. blocked path=projects/Bad Name/x.txt reason=bad-name"
run_case "bad folder underscore"  "$(mkenv Write file_path 'contacts/ACME_Corp/x.txt')"         2 "reason=bad-name"
run_case "bad folder parens"      "$(mkenv Write file_path 'documents/Draft (v2)/x.txt')"       2 "reason=bad-name"
run_case "bad file basename"      "$(mkenv Write file_path 'projects/acme/Final DRAFT.md')"     2 "reason=bad-name"
run_case "conforming name allow"  "$(mkenv Write file_path 'projects/acme/quote-2024.pdf')"     0 ""
run_case "conforming doc folder"  "$(mkenv Write file_path 'documents/site-survey/notes.md')"   0 ""
# Dot-prefixed segments are skipped at every level system-wide (.gitkeep etc.).
run_case "dotfile segment allow"  "$(mkenv Write file_path 'documents/.gitkeep')"               0 ""
# JSON null file_path must coerce to empty (allow), not the literal string "None".
run_case "null path allow"       '{"hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":null}}' 0 ""

# Derived domain buckets (construction account $ACCT_D): jobs/ passes at any
# depth (it is outside the three flat operator-data buckets), an undeclared
# top-level segment still blocks.
run_case "domain bucket deep allow" "$(mkenv Write file_path 'jobs/5-oaktree/Quotations/quote.pdf')" 0 "" "$ACCT_D"
run_case "domain bucket file allow"  "$(mkenv Write file_path 'jobs/5-oaktree/notes.md')"           0 "" "$ACCT_D"
run_case "undeclared still blocks"   "$(mkenv Write file_path 'invoices/x.pdf')"                     2 "reason=top-level" "$ACCT_D"

# Allowed-set parse == documented set.
EXPECT="projects contacts documents url-get output generated extracted uploads agents specialists sites public cache secrets state logs tmp .quarantine SCHEMA.md account.json AGENTS.md .claude .git"
GOT=$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | tr '\n' ' ' | sed 's/ *$//')
if [ "$GOT" = "$EXPECT" ]; then echo "PASS: allowed-set parse"; PASS=$((PASS+1));
else echo "FAIL: allowed-set parse: got [$GOT]" >&2; FAIL=$((FAIL+1)); fi

echo "----- $PASS passed, $FAIL failed -----"
[ $FAIL -eq 0 ]
