#!/usr/bin/env bash
# Tests for ship-init.sh — the frozen-wrapper generator. Builds synthetic repos and asserts:
# Phase A proposes the right mode from evidence + writes nothing; Phase B emits a correct thin
# wrapper that delegates + drift-validates; fail-closed on merge-to-main without a deploy anchor.
set -uo pipefail
GEN="$(cd "$(dirname "$0")" && pwd)/ship-init.sh"
PASS=0; FAIL=0
ok()  { PASS=$((PASS+1)); printf '  ok   %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf '  FAIL %s\n     %s\n' "$1" "$2"; }
jget() { printf '%s' "$1" | python3 -c "import sys,json;d=json.load(sys.stdin);print($2)" 2>/dev/null; }

# build a synthetic project. $1=name $2=deploy(yes/no) $3=ignore_claude(yes/no). Echoes ROOT.
mkproj() {
  local name=$1 deploy=$2 ign=$3 T R B
  T=$(mktemp -d "/tmp/sin-$name-XXXX"); B="$T/o.git"; R="$T/proj"
  git init -q --bare -b main "$B"
  git init -q -b main "$R"; git -C "$R" config user.email t@t; git -C "$R" config user.name t
  printf '{"packageManager":"pnpm@9.7.0","scripts":{"gate":"turbo run test"}}\n' > "$R/package.json"
  printf 'lock\n' > "$R/pnpm-lock.yaml"
  if [[ "$ign" == "yes" ]]; then printf '.claude/\n' > "$R/.gitignore"; else printf '.claude/worktrees/\n' > "$R/.gitignore"; fi
  if [[ "$deploy" == "yes" ]]; then
    mkdir -p "$R/.github/workflows"
    printf 'on:\n  push:\n    branches: [main]\njobs:\n  deploy:\n    steps: [cloudflare pages deploy]\n' > "$R/.github/workflows/deploy-prod.yml"
  fi
  git -C "$R" add -A; git -C "$R" commit -q -m seed
  git -C "$R" remote add origin "$B"; git -C "$R" push -q -u origin main 2>/dev/null
  git -C "$R" remote set-head origin main 2>/dev/null
  printf '%s' "$R"
}

echo "ship-init tests:"

# 1. Phase A on a no-deploy repo → proposes pr, exit 10, writes nothing.
R=$(mkproj a-pr no no)
out=$(bash "$GEN" "$R" 2>/dev/null); rc=$?
[[ $rc -eq 10 && "$(jget "$out" 'd["proposed_mode"]')" == "pr" && ! -e "$R/.claude/scripts/ship.sh" ]] \
  && ok "Phase A: no-deploy → proposes pr, writes nothing" || bad "phaseA pr" "rc=$rc $out"
rm -rf "$(dirname "$R")"

# 2. Phase A on a deploy-on-push repo (no protection) → proposes merge-to-main.
R=$(mkproj a-merge yes no)
out=$(bash "$GEN" "$R" 2>/dev/null); rc=$?
[[ $rc -eq 10 && "$(jget "$out" 'd["proposed_mode"]')" == "merge-to-main" \
   && "$(jget "$out" 'd["deploy_workflow"]')" == ".github/workflows/deploy-prod.yml" ]] \
  && ok "Phase A: deploy-on-push → proposes merge-to-main" || bad "phaseA merge" "rc=$rc $out"
rm -rf "$(dirname "$R")"

# 3. Phase B --mode pr → emits a delegating wrapper; emitted smoke test passes.
R=$(mkproj b-pr no no)
out=$(bash "$GEN" "$R" --mode pr 2>/dev/null); rc=$?
[[ $rc -eq 0 && "$(jget "$out" 'd["anchor"]')" == remote:* ]] \
  && grep -q 'exec bash "$LIB" land' "$R/.claude/scripts/ship.sh" \
  && ok "Phase B pr: emits delegating wrapper" || bad "phaseB pr emit" "rc=$rc $out"
bash "$R/.claude/scripts/test-ship.sh" >/dev/null 2>&1 \
  && ok "Phase B pr: generated smoke test passes" || bad "phaseB pr smoke" "smoke failed"
grep -qxF '**/.claude/scripts/' "$R/.git/info/exclude" \
  && ok "Phase B pr: exclude registered (.claude tracked)" || bad "phaseB pr exclude" "not in info/exclude"
rm -rf "$(dirname "$R")"

# 3b. ROOT-CAUSE: a $-laden --testcmd must bake SINGLE-quoted (shq) so the wrapper stays runnable under
#     `set -u`. The old double-quoted bake re-expanded $t at wrapper-load → "unbound variable" → drift crashed.
R=$(mkproj b-dollar no no)
bash "$GEN" "$R" --mode pr --testcmd 'for t in a b; do echo $t; done' >/dev/null 2>&1; rc=$?
WRP="$R/.claude/scripts/ship.sh"
grep -qF "TESTCMD='for t in a b; do echo \$t; done'" "$WRP" \
  && ( cd "$R" && bash "$WRP" drift >/dev/null 2>&1 ) \
  && ok "Phase B pr: \$-laden testcmd bakes single-quoted, wrapper runs (no set-u crash)" \
  || bad "phaseB pr \$-testcmd" "rc=$rc; baked wrapper not safe"
rm -rf "$(dirname "$R")"

# 4. Phase B --mode merge-to-main on a deploy repo → anchor deploy:<wf>, emits.
R=$(mkproj b-merge yes no)
out=$(bash "$GEN" "$R" --mode merge-to-main 2>/dev/null); rc=$?
[[ $rc -eq 0 && "$(jget "$out" 'd["anchor"]')" == "deploy:.github/workflows/deploy-prod.yml" ]] \
  && ok "Phase B merge: deploy anchor frozen" || bad "phaseB merge emit" "rc=$rc $out"
bash "$R/.claude/scripts/test-ship.sh" >/dev/null 2>&1 \
  && ok "Phase B merge: generated smoke test passes" || bad "phaseB merge smoke" "smoke failed"
rm -rf "$(dirname "$R")"

# 5. merge-to-main, NO deploy workflow but WITH origin remote → falls back to remote:<url> anchor, emits.
#    Solo-dev repos without CI deploy must not be blocked; the remote URL is a sufficient drift anchor.
R=$(mkproj b-remotefallback no no)
out=$(bash "$GEN" "$R" --mode merge-to-main 2>/dev/null); rc=$?
[[ $rc -eq 0 && "$(jget "$out" 'd["anchor"]')" == remote:* ]] \
  && ok "merge-to-main w/o deploy but with remote → remote: anchor fallback, emits" || bad "merge remote-fallback" "rc=$rc $out"
bash "$R/.claude/scripts/test-ship.sh" >/dev/null 2>&1 \
  && ok "Phase B merge remote-fallback: generated smoke test passes" || bad "merge remote-fallback smoke" "smoke failed"
rm -rf "$(dirname "$R")"

# 5b. TRUE FAIL-CLOSED: merge-to-main with NO deploy AND NO remote → refuse (exit 3), no wrapper.
R=$(mkproj b-failclosed no no); git -C "$R" remote remove origin
bash "$GEN" "$R" --mode merge-to-main >/dev/null 2>&1; rc=$?
[[ $rc -eq 3 && ! -e "$R/.claude/scripts/ship.sh" ]] \
  && ok "fail-closed: merge-to-main w/o deploy AND w/o remote → exit3, no wrapper" || bad "failclosed" "rc=$rc (wrapper may exist)"
rm -rf "$(dirname "$R")"

# 6. .claude IGNORED → no info/exclude edit needed (exclude == n/a).
R=$(mkproj b-ignored no yes)
out=$(bash "$GEN" "$R" --mode pr 2>/dev/null)
[[ "$(jget "$out" 'd["exclude"]')" == n/a* ]] && ! grep -qxF '**/.claude/scripts/' "$R/.git/info/exclude" 2>/dev/null \
  && ok ".claude ignored → no exclude edit" || bad "ignored no-exclude" "$out"
rm -rf "$(dirname "$R")"

# 7. idempotency: emit twice without --force → second refuses (exit 3).
R=$(mkproj b-idem no no)
bash "$GEN" "$R" --mode pr >/dev/null 2>&1
bash "$GEN" "$R" --mode pr >/dev/null 2>&1; rc=$?
[[ $rc -eq 3 ]] && ok "idempotency: re-emit without --force → exit3" || bad "idempotency" "rc=$rc"
rm -rf "$(dirname "$R")"

# add preview-deploy (wrangler) + e2e (playwright) capability to a repo built by mkproj.
add_dv_caps() { local R=$1; printf 'name="x"\n' > "$R/wrangler.toml"; printf 'export default {}\n' > "$R/playwright.config.ts"; git -C "$R" add -A; git -C "$R" commit -q -m caps; }

# 8. Phase A: preview-deploy + e2e capability → proposes deploy-verify, writes nothing.
R=$(mkproj a-dv no no); add_dv_caps "$R"
out=$(bash "$GEN" "$R" 2>/dev/null); rc=$?
[[ $rc -eq 10 && "$(jget "$out" 'd["proposed_mode"]')" == "deploy-verify" \
   && "$(jget "$out" 'd["preview_capability"]')" == config:* \
   && "$(jget "$out" 'd["e2e_capability"]')" == config:* \
   && "$(jget "$out" 'd["anchor_candidate"]')" == "deploy:wrangler.toml" \
   && ! -e "$R/.claude/scripts/ship.sh" ]] \
  && ok "Phase A: preview+e2e → proposes deploy-verify, writes nothing" || bad "phaseA deploy-verify" "rc=$rc $out"
rm -rf "$(dirname "$R")"

# 9. Phase A: preview capability but NO e2e → does NOT propose deploy-verify (both signals required).
R=$(mkproj a-dv-noe2e no no); printf 'name="x"\n' > "$R/wrangler.toml"; git -C "$R" add -A; git -C "$R" commit -q -m wrangler-only
out=$(bash "$GEN" "$R" 2>/dev/null)
[[ "$(jget "$out" 'd["proposed_mode"]')" != "deploy-verify" \
   && "$(jget "$out" 'd["e2e_capability"]')" == "" ]] \
  && ok "Phase A: preview w/o e2e → NOT deploy-verify" || bad "phaseA dv requires e2e" "$out"
rm -rf "$(dirname "$R")"

# 10. Phase B --mode deploy-verify → wrapper carries DEPLOYCMD/E2ECMD/PROMOTE + delegate passes them; smoke passes.
R=$(mkproj b-dv no no); add_dv_caps "$R"
out=$(bash "$GEN" "$R" --mode deploy-verify --deploycmd 'pnpm deploy:preview' --e2ecmd 'pnpm test:e2e' --promote pr 2>/dev/null); rc=$?
WRP="$R/.claude/scripts/ship.sh"
[[ $rc -eq 0 && "$(jget "$out" 'd["anchor"]')" == "deploy:wrangler.toml" ]] \
  && grep -q "DEPLOYCMD='pnpm deploy:preview'" "$WRP" && grep -q "E2ECMD='pnpm test:e2e'" "$WRP" \
  && grep -q "PROMOTE='pr'" "$WRP" && grep -q -- '--deploycmd "$DEPLOYCMD" --e2ecmd "$E2ECMD" --promote "$PROMOTE"' "$WRP" \
  && ok "Phase B deploy-verify: wrapper carries facts + passes them to land" || bad "phaseB dv emit" "rc=$rc $out"
bash "$R/.claude/scripts/test-ship.sh" >/dev/null 2>&1 \
  && ok "Phase B deploy-verify: generated smoke test passes" || bad "phaseB dv smoke" "smoke failed"
rm -rf "$(dirname "$R")"

# 10b. FAIL-CLOSED: --mode deploy-verify WITHOUT --deploycmd → refuse (exit 3), no wrapper.
R=$(mkproj b-dv-fc no no); add_dv_caps "$R"
bash "$GEN" "$R" --mode deploy-verify --e2ecmd 'pnpm test:e2e' --promote pr >/dev/null 2>&1; rc=$?
[[ $rc -eq 3 && ! -e "$R/.claude/scripts/ship.sh" ]] \
  && ok "fail-closed: deploy-verify w/o --deploycmd → exit3, no wrapper" || bad "dv failclosed" "rc=$rc"
rm -rf "$(dirname "$R")"

# 11/12. sniff_protection honesty: only GitHub's canonical 404 "Branch not protected" → base_protected
#        "no"; any other gh-api failure (403/no-scope) → "unknown" — never conflate them (a false "no"
#        nudges the proposal toward merge-to-main). Direction-neutral (both != yes) so assert the FIELD.
GHBIN=$(mktemp -d /tmp/sin-ghbin-XXXX)
cat > "$GHBIN/gh" <<'EOF'
#!/usr/bin/env bash
case "$1" in
  auth) exit 0;;
  api)  printf '%s\n' "$GH_API_BODY"; exit 1;;   # gh api on HTTP error: prints body, non-zero
  *)    exit 0;;
esac
EOF
chmod +x "$GHBIN/gh"
R=$(mkproj p-404 yes no)
out=$( cd "$R" && PATH="$GHBIN:$PATH" GH_API_BODY="Branch not protected (HTTP 404)" bash "$GEN" "$R" 2>/dev/null )
[[ "$(jget "$out" 'd["base_protected"]')" == "no" ]] \
  && ok "sniff_protection: canonical 404 → base_protected=no" || bad "protection 404→no" "$out"
rm -rf "$(dirname "$R")"
R=$(mkproj p-403 yes no)
out=$( cd "$R" && PATH="$GHBIN:$PATH" GH_API_BODY="HTTP 403: Resource not accessible by integration" bash "$GEN" "$R" 2>/dev/null )
[[ "$(jget "$out" 'd["base_protected"]')" == "unknown" ]] \
  && ok "sniff_protection: 403/no-scope → base_protected=unknown (not a false 'no')" || bad "protection 403→unknown" "$out"
rm -rf "$(dirname "$R")" "$GHBIN"

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