#!/usr/bin/env bash
set -euo pipefail

# scope-guard hard-reject contract: the rescue fixer may modify ONLY files the
# coder already changed, and NEVER a protected check surface. Snapshots are
# temp-index write-tree tree-ishes so untracked files the fixer plants are seen.

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GATES="$ROOT_DIR/lib/gates.sh"

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

repo="$tmp/wt"
mkdir -p "$repo"
git -C "$repo" init -q
git -C "$repo" config user.email t@example.com
git -C "$repo" config user.name t
printf 'orig\n' >"$repo/a.js"
printf 'orig\n' >"$repo/b.js"
printf 'export const a = 1\n' >"$repo/generated.ts"
printf 'export const manual = 1\n' >"$repo/manual.ts"
mkdir -p "$repo/test"
printf 'export const testGenerated = 1\n' >"$repo/test/generated.ts"
mkdir -p "$repo/packages/commerce-checkout/src/testing"
printf 'export const testing = true\n' >"$repo/packages/commerce-checkout/src/testing/index.ts"
mkdir -p "$repo/packages/billing/src"
printf 'test("stripe", () => {})\n' >"$repo/packages/billing/src/stripe.test.ts"
git -C "$repo" add -A
git -C "$repo" commit -qm base
base="$(git -C "$repo" rev-parse HEAD)"

snapshot() {
  local idx="$tmp/idx.$1"
  GIT_INDEX_FILE="$idx" git -C "$repo" read-tree HEAD
  GIT_INDEX_FILE="$idx" git -C "$repo" add -A
  local tree
  tree="$(GIT_INDEX_FILE="$idx" git -C "$repo" write-tree)"
  rm -f "$idx"
  printf '%s' "$tree"
}

reset_wt() {
  git -C "$repo" checkout -q -- .
  git -C "$repo" clean -fdq
}

assert_case() {
  local name="$1" expected_rc="$2" pre="$3" post="$4" want_class="${5:-}" exemptions="${6:-}"
  local out rc=0
  out="$(bash "$GATES" scope-guard "$repo" "$base" "$pre" "$post" "$exemptions" 2>&1)" || rc=$?
  if [[ "$rc" -ne "$expected_rc" ]]; then
    printf 'FAIL %s: rc=%s expected=%s\n%s\n' "$name" "$rc" "$expected_rc" "$out" >&2
    exit 1
  fi
  if [[ -n "$want_class" && "$out" != *"FAILCLASS=$want_class"* ]]; then
    printf 'FAIL %s: expected FAILCLASS=%s in output\n%s\n' "$name" "$want_class" "$out" >&2
    exit 1
  fi
  printf 'PASS %s (rc=%s)\n' "$name" "$rc"
}

# 1. clean fix: fixer edits only a.js, which the coder already touched -> allowed
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot clean)"
printf 'fixer\n' >"$repo/a.js"
post="$(snapshot clean2)"
assert_case "clean-fix-in-scope" 0 "$pre" "$post"

# 2. out-of-scope existing file: fixer edits b.js the coder never touched -> reject
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot oos)"
printf 'fixer\n' >"$repo/b.js"
post="$(snapshot oos2)"
assert_case "out-of-scope-existing" 1 "$pre" "$post" "scope-violation"

# 3. protected surface via NEW untracked file: fixer plants .warnignore -> reject
#    (proves untracked files are captured by the write-tree snapshot)
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot prot)"
printf 'node_modules\n' >"$repo/.warnignore"
post="$(snapshot prot2)"
assert_case "protected-new-untracked" 1 "$pre" "$post" "scope-violation"

# 4. out-of-scope NEW untracked file: fixer creates c.js -> reject
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot new)"
printf 'planted\n' >"$repo/c.js"
post="$(snapshot new2)"
assert_case "out-of-scope-new-untracked" 1 "$pre" "$post" "scope-violation"

# 5. out-of-scope declaration source without a matching TypeScript input stays rejected
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot declaration)"
printf 'export declare const manual: string;\n' >"$repo/manual.d.ts"
post="$(snapshot declaration2)"
assert_case "out-of-scope-new-declaration-source" 1 "$pre" "$post" "scope-violation"

# 6. a lone same-stem .js file remains an out-of-scope source change
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot manualsource)"
printf 'export const manual = 2;\n' >"$repo/manual.js"
post="$(snapshot manualsource2)"
assert_case "out-of-scope-same-stem-javascript-source" 1 "$pre" "$post" "scope-violation"

# 7. paired TypeScript outputs remain protected when they are under test/
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot protectedgenerated)"
printf 'export declare const testGenerated: number;\n' >"$repo/test/generated.d.ts"
printf '"use strict";\n' >"$repo/test/generated.js"
post="$(snapshot protectedgenerated2)"
assert_case "protected-generated-typescript-artifacts" 1 "$pre" "$post" "scope-violation"

# 8. paired TypeScript gate outputs are discarded instead of treated as fixer source changes
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot generated)"
printf 'export declare const a: number;\n' >"$repo/generated.d.ts"
printf '"use strict";\n' >"$repo/generated.js"
post="$(snapshot generated2)"
assert_case "generated-typescript-artifacts" 0 "$pre" "$post"
if [[ -e "$repo/generated.js" || -e "$repo/generated.d.ts" ]]; then
  printf 'FAIL generated-typescript-artifacts: generated outputs remain in worktree\n' >&2
  exit 1
fi

# 9. exact declared source deliverable may be fixed even when the coder did not touch it
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot declaredsource)"
printf 'export const testing = false\n' >"$repo/packages/commerce-checkout/src/testing/index.ts"
post="$(snapshot declaredsource2)"
assert_case "declared-source-deliverable" 0 "$pre" "$post" "" "packages/commerce-checkout/src/testing/index.ts"

# 10. a declared directory does not authorize child paths
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot declareddirectory)"
printf 'export const testing = false\n' >"$repo/packages/commerce-checkout/src/testing/index.ts"
post="$(snapshot declareddirectory2)"
assert_case "declared-directory-does-not-bypass-scope" 1 "$pre" "$post" "scope-violation" "packages/commerce-checkout/src/testing"

# 11. declared deliverable under test/: fixer edits coder-touched deliverable -> allowed
reset_wt
mkdir -p "$repo/test/chaos"
printf 'coder\n' >"$repo/test/chaos/run.sh"
pre="$(snapshot testdeliverable)"
printf 'fixer\n' >"$repo/test/chaos/run.sh"
post="$(snapshot testdeliverable2)"
assert_case "declared-test-deliverable" 0 "$pre" "$post" "" "test/chaos/run.sh"

# 12. undeclared test/ path stays protected even when coder touched it -> reject
reset_wt
mkdir -p "$repo/test/chaos"
printf 'coder\n' >"$repo/test/chaos/other.sh"
pre="$(snapshot testundeclared)"
printf 'fixer\n' >"$repo/test/chaos/other.sh"
post="$(snapshot testundeclared2)"
assert_case "undeclared-test-path-protected" 1 "$pre" "$post" "scope-violation" "test/chaos/run.sh"

# 13. absolute denies ignore exemptions: .warnignore remains protected
reset_wt
printf 'coder\n' >"$repo/.warnignore"
pre="$(snapshot warnignore)"
printf 'fixer\n' >"$repo/.warnignore"
post="$(snapshot warnignore2)"
assert_case "warnignore-exemption-ignored" 1 "$pre" "$post" "scope-violation" ".warnignore"

# 14. an exact declared test file outside test/ may be fixed
reset_wt
printf 'coder\n' >"$repo/a.js"
pre="$(snapshot declaredtestbasename)"
printf 'test("stripe", () => { throw new Error("fixed"); })\n' >"$repo/packages/billing/src/stripe.test.ts"
post="$(snapshot declaredtestbasename2)"
assert_case "declared-test-basename-deliverable" 0 "$pre" "$post" "" "packages/billing/src/stripe.test.ts"

# 15. comma in declared deliverable is literal, not a delimiter for other exemptions
reset_wt
mkdir -p "$repo/src" "$repo/test/chaos"
printf 'coder\n' >"$repo/test/chaos/other.sh"
pre="$(snapshot comma)"
printf 'fixer\n' >"$repo/test/chaos/other.sh"
post="$(snapshot comma2)"
assert_case "comma-deliverable-not-split" 1 "$pre" "$post" "scope-violation" "src/a,test/chaos/other.sh"

printf 'gate-scope-guard: ok\n'
