#!/usr/bin/env bash
# tests/sandbox-seed/run-tests.sh
#
# Harness for netbox/sandbox-seed.py and netbox/prod-draft-dump.py -- the
# two-phase upstream->sandbox seeding loop.
#
# OFFLINE. Touches no NetBox, real or sandbox. It exercises the arg contract and
# the SAFETY PROPERTIES, which are the whole reason these two scripts are split:
#
#   1. prod-draft-dump.py must have NO write path at all (no --commit, no POST).
#   2. sandbox-seed.py must be DRY BY DEFAULT and must REFUSE an upstream URL.
#   3. The upstream User-Agent trap must stay encoded (a 403 that looks like auth).
#
# Each of these cost real time on 2026-07-13. A regression that quietly drops one
# would not surface until something was written to the production IPAM apex.
# ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NBDIR="$(cd "$HERE/../../netbox" && pwd)"
SEED="$NBDIR/sandbox-seed.py"
DUMP="$NBDIR/prod-draft-dump.py"
pass=0; fail=0
ok()  { pass=$((pass+1)); }
bad() { fail=$((fail+1)); echo "  FAIL: $1"; }

command -v python3 >/dev/null 2>&1 || { echo "FAIL: python3 required"; exit 1; }

# 0. both parse
python3 -c "import ast;ast.parse(open('$SEED').read())" 2>/dev/null && ok || bad "sandbox-seed.py does not parse"
python3 -c "import ast;ast.parse(open('$DUMP').read())" 2>/dev/null && ok || bad "prod-draft-dump.py does not parse"

# 1. arg contract
python3 "$SEED" --help >/dev/null 2>&1 && ok || bad "sandbox-seed --help -> 0"
python3 "$DUMP" --help >/dev/null 2>&1 && ok || bad "prod-draft-dump --help -> 0"
python3 "$SEED" >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "sandbox-seed with no --draft must fail"
python3 "$DUMP" >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "prod-draft-dump with no --out must fail"

# 2. THE DUMP HAS NO WRITE PATH. It reads production. If it ever grows a POST,
#    that is a write to the IPAM apex from a script whose whole contract is
#    "read-only", and no reviewer would be looking for it.
grep -qE 'add_argument\("--commit"' "$DUMP" && bad "prod-draft-dump.py has a --commit flag -- it MUST be read-only" || ok
grep -qE '"(POST|PATCH|PUT|DELETE)"' "$DUMP" && bad "prod-draft-dump.py contains a WRITE method -- it MUST be read-only" || ok

# 3. sandbox-seed is DRY BY DEFAULT (the D-117 lesson: a write-by-default IPAM
#    tool is how an off-by-one lands in production unannounced).
grep -q '"--commit", action="store_true"' "$SEED" && ok || bad "sandbox-seed has no --commit flag"
grep -q 'DRY RUN -- nothing will be written' "$SEED" && ok || bad "sandbox-seed does not announce its dry run"

# 4. sandbox-seed must REFUSE to point at upstream. "The sim never writes
#    upstream" is enforced structurally, not by remembering a flag.
grep -q 'REFUSING: NETBOX_URL points at the UPSTREAM' "$SEED" && ok \
  || bad "sandbox-seed lost its upstream-URL refusal guard"
grep -q 'Guards go FIRST' "$SEED" && ok \
  || bad "the upstream guard is no longer documented as running FIRST (it was dead code once)"
tmp="$(mktemp)"; printf '{"_source":"https://netbox.baldurkeep.com"}' > "$tmp"
out="$(NETBOX_URL=https://netbox.baldurkeep.com NETBOX_TOKEN=x python3 "$SEED" --draft "$tmp" --commit 2>&1)"
rm -f "$tmp"
printf '%s' "$out" | grep -q "REFUSING" && ok \
  || bad "sandbox-seed did NOT refuse an upstream NETBOX_URL (it would have written to production)"

# 5. the upstream User-Agent trap: a 403 that is NOT an auth failure.
grep -q 'User-Agent' "$DUMP" && ok || bad "prod-draft-dump lost the User-Agent header -- upstream 403s the default python UA"
grep -q 'User-Agent filter, NOT the token' "$DUMP" && ok \
  || bad "prod-draft-dump lost the 403 explanation -- the next person will blame the token"

# 6. ids must never be carried across instances.
grep -q 'IDs are deliberately DISCARDED' "$DUMP" && ok || bad "prod-draft-dump lost the id-discard contract"

# 7. region scopes. Mapping only dcim.site silently dropped the scope on 17 of the
#    90 upstream prefixes -- including the ENTIRE VR1 region-scoped draft. Caught
#    only because a spot-check printed 'site None'.
grep -q 'dcim.region' "$DUMP" && ok || bad "prod-draft-dump no longer carries REGION-scoped prefixes (17 of 90 upstream)"
grep -q 'dcim.region' "$SEED" && ok || bad "sandbox-seed no longer resolves REGION scopes"

# 8. the fidelity checker must exist and must be READ-ONLY. Counts and
#    idempotency cannot prove a faithful seed -- the region-scope drop proved that
#    (17 of 90 prefixes lost their scope; every count still matched).
FID="$NBDIR/sandbox-fidelity-check.py"
[ -f "$FID" ] && ok || bad "netbox/sandbox-fidelity-check.py is gone -- nothing proves the seed is faithful"
python3 -c "import ast;ast.parse(open('$FID').read())" 2>/dev/null && ok || bad "sandbox-fidelity-check.py does not parse"
grep -qE '"(POST|PATCH|PUT|DELETE)"' "$FID" && bad "sandbox-fidelity-check.py can WRITE -- it must be read-only" || ok
grep -q 'FIELD BY FIELD' "$FID" && ok || bad "the fidelity check no longer compares field-by-field -- counts alone hid the last bug"

echo
total=$((pass+fail))
if [ "$fail" -eq 0 ]; then echo "sandbox-seed: $pass/$total PASS"; exit 0; fi
echo "sandbox-seed: $fail/$total FAIL"; exit 1
