#!/usr/bin/env bash
# tests/sandbox-fidelity-check/run-tests.sh
#
# Harness for netbox/sandbox-fidelity-check.py. Offline: builds synthetic upstream
# / sandbox JSON dumps and asserts the checker's verdict. Touches no NetBox.
#
# WHY THIS HARNESS EXISTS AT ALL (2026-07-14): the checker SHIPPED WITH NO HARNESS,
# and it was the script we were citing as PROOF the sandbox was a faithful replica.
# It could return a FALSE GREEN: `unexpected = extra - expected` proves the delta is
# a SUBSET of the plan but NEVER proved the plan was CARRIED OUT. With ZERO of the
# 36 DC prefixes created, extra={} -> unexpected={} -> it printed "Nothing lost,
# nothing stray" and exited 0. EXPECTED_NEW was an upper bound masquerading as an
# assertion. T3 is that exact scenario, now caught.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
CHECK="$REPO/netbox/sandbox-fidelity-check.py"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
P=0; F=0
ok() { echo "  [PASS] $1"; P=$((P+1)); }
no() { echo "  [FAIL] $1"; F=$((F+1)); }

[ -f "$CHECK" ] || { echo "FAIL: $CHECK not found"; exit 1; }

# --- fixtures -----------------------------------------------------------------
# A minimal upstream draft: one shared prefix. The sandbox must reproduce it
# EXACTLY, and may add only what EXPECTED_NEW allows.
mk() { # mk <file> <json>
  printf '%s' "$2" > "$TMP/$1"
}

SHARED='{"prefix":"10.99.0.0/24","status":"active","role":"storage","scope_type":"dcim.site","scope_id":1,"description":"shared","is_pool":false,"mark_utilized":false}'
# an EXPECTED delta prefix in DUMP FORMAT (scope_site slug, as prod-draft-dump.py
# rewrites it; scope_id is popped on the way in). 10.12.4.0/22 is intended for site
# vr1-dc0 per EXPECTED_PREFIX_SCOPE.
DELTA_OK='{"prefix":"10.12.4.0/22","status":"active","role":"provider-public","scope_type":"dcim.site","scope_site":"vr1-dc0","description":"VR1 DC0 provider-public","is_pool":false,"mark_utilized":false}'
# the SAME expected prefix but UNSCOPED -- the 17-prefix-scope-drop bug's shape
DELTA_NOSCOPE='{"prefix":"10.12.4.0/22","status":"active","role":"provider-public","scope_type":null,"description":"VR1 DC0 provider-public","is_pool":false,"mark_utilized":false}'
# the SAME expected prefix bound to the WRONG DC site (vr1-dc1) -- the D-117 near-miss
DELTA_WRONGSITE='{"prefix":"10.12.4.0/22","status":"active","role":"provider-public","scope_type":"dcim.site","scope_site":"vr1-dc1","description":"wrong site","is_pool":false,"mark_utilized":false}'
# a stray object nobody planned
STRAY='{"prefix":"192.0.2.0/24","status":"active","role":"storage","scope_type":"dcim.site","scope_id":1,"description":"stray","is_pool":false,"mark_utilized":false}'

up_json() { printf '{"ipam/prefixes":[%s]}' "$SHARED"; }
run() {  # run <upstream-json> <sandbox-json> ; echoes rc
  printf '%s' "$1" > "$TMP/u.json"; printf '%s' "$2" > "$TMP/s.json"
  python3 "$CHECK" --upstream "$TMP/u.json" --sandbox "$TMP/s.json" >"$TMP/out" 2>&1
  echo $?
}

echo "=== sandbox-fidelity-check ==="

# T1: THE POSITIVE PATH. A faithful replica carrying the COMPLETE planned delta
#     must pass. The expected set is DERIVED FROM THE CHECKER ITSELF (its own
#     EXPECTED_NEW), never re-typed here -- a harness that hardcodes its own copy of
#     the expectation cannot detect the two drifting apart.
python3 - "$CHECK" "$TMP" <<'PY'
import json, re, sys
src = open(sys.argv[1]).read()
tmp = sys.argv[2]
# exec only the declarative prologue (up to the comparison loop), with the two
# argparse-required inputs stubbed, to harvest EXPECTED_NEW without running the diff.
head = src.split("bad=0")[0]
head = re.sub(r"^ap\s*=.*$|^ap\.add_argument.*$|^args\s*=.*$|^up\s*=.*$|^sb\s*=.*$",
              "", head, flags=re.M)
ns = {}
exec(compile(head, "checker-prologue", "exec"), ns)
SHARED = {"prefix":"10.99.0.0/24","status":"active","role":"storage",
          "scope_type":"dcim.site","scope_id":1,"description":"shared",
          "is_pool":False,"mark_utilized":False}
sandbox = {}
upstream = {}
for ep, key in ns["KEY"].items():
    upstream[ep] = [SHARED] if ep == "ipam/prefixes" else []
    rows = [SHARED] if ep == "ipam/prefixes" else []
    for v in sorted(ns["EXPECTED_NEW"].get(ep, set())):
        rec = {key: v}
        if ep == "ipam/prefixes":
            rec.update({"status":"active","role":"provider-public",
                        "description":"planned","is_pool":False,"mark_utilized":False})
            sc = ns["EXPECTED_PREFIX_SCOPE"].get(v)   # INTENDED scope, harvested from the checker itself
            if sc is not None:
                kind, slug = sc
                rec["scope_type"] = "dcim.site" if kind == "site" else "dcim.region"
                rec["scope_site" if kind == "site" else "scope_region"] = slug
            else:
                rec["scope_type"] = None              # intentionally-unscoped org container
        rows.append(rec)
    sandbox[ep] = rows
json.dump(upstream, open(f"{tmp}/u1.json","w"))
json.dump(sandbox,  open(f"{tmp}/s1.json","w"))
PY
python3 "$CHECK" --upstream "$TMP/u1.json" --sandbox "$TMP/s1.json" >"$TMP/out" 2>&1; rc=$?
[ "$rc" = 0 ] && ok "T1 faithful replica WITH the complete planned delta -> exit 0" \
              || { no "T1 the complete, correct delta should PASS (rc=$rc)"; sed 's/^/       /' "$TMP/out" | tail -6; }

# T2: a shared object with a MANGLED field -> FAIL (the original 17-prefix bug,
#     as it appeared on SHARED objects; the check already caught this)
MANGLED='{"prefix":"10.99.0.0/24","status":"active","role":"storage","scope_type":null,"scope_id":null,"description":"shared","is_pool":false,"mark_utilized":false}'
rc=$(run "$(up_json)" "$(printf '{"ipam/prefixes":[%s]}' "$MANGLED")")
[ "$rc" = 1 ] && grep -q "FIELD DRIFT" "$TMP/out" && ok "T2 dropped scope on a SHARED object -> FIELD DRIFT, exit 1" \
              || { no "T2 should fail on shared-object field drift (rc=$rc)"; sed 's/^/       /' "$TMP/out"; }

# T3: THE FALSE GREEN. The planned delta was NOT applied (zero DC prefixes created).
#     A subset-only check calls this clean. It must FAIL.
rc=$(run "$(up_json)" "$(printf '{"ipam/prefixes":[%s]}' "$SHARED")")
# ^ T1 is the no-delta-expected case; to exercise EXPECTED-BUT-ABSENT we need the
#   checker's own EXPECTED_NEW to be non-empty, which it always is. T1 above already
#   proves it: with an empty sandbox delta, the 36+ EXPECTED_NEW prefixes are absent.
grep -q "EXPECTED-BUT-ABSENT" "$TMP/out" \
  && ok "T3 FALSE GREEN CAUGHT: the planned delta was never applied -> EXPECTED-BUT-ABSENT" \
  || no "T3 the false green is BACK -- a subset-only check passes an unapplied plan"
[ "$rc" = 1 ] && ok "T3 an unapplied plan exits 1 (was exit 0 -- 'Nothing lost, nothing stray')" \
              || no "T3 unapplied plan still exits $rc"

# T4: an UNEXPECTED stray write -> FAIL
rc=$(run "$(up_json)" "$(printf '{"ipam/prefixes":[%s,%s]}' "$SHARED" "$STRAY")")
[ "$rc" = 1 ] && grep -q "UNEXPECTED EXTRA" "$TMP/out" && ok "T4 a stray write -> UNEXPECTED EXTRA, exit 1" \
              || { no "T4 should fail on a stray write (rc=$rc)"; sed 's/^/       /' "$TMP/out"; }

# T5: an object MISSING from the sandbox -> FAIL
rc=$(run "$(up_json)" '{"ipam/prefixes":[]}')
[ "$rc" = 1 ] && grep -q "MISSING FROM SANDBOX" "$TMP/out" && ok "T5 a lost upstream object -> MISSING, exit 1" \
              || { no "T5 should fail on a lost object (rc=$rc)"; sed 's/^/       /' "$TMP/out"; }

# T6: DELTA SCOPE-CORRECTNESS. An EXPECTED new prefix whose INTENDED scope was
#     DROPPED (10.12.4.0/22 should be site vr1-dc0, arrives unscoped) must fail. The
#     shared-object drift loop CANNOT see this (the object exists only in the
#     sandbox) -- it is precisely the 17-prefix scope-drop bug, relocated into the
#     delta where the old check was blind to it.
rc=$(run "$(up_json)" "$(printf '{"ipam/prefixes":[%s,%s]}' "$SHARED" "$DELTA_NOSCOPE")")
grep -q "!= intended" "$TMP/out" && ok "T6 an expected DELTA prefix with its scope DROPPED -> caught" \
                                  || { no "T6 delta scope-correctness is blind again"; sed 's/^/       /' "$TMP/out"; }
[ "$rc" = 1 ] && ok "T6 a scope-dropped delta prefix exits 1" || no "T6 scope-dropped delta exits $rc"

# T7: a correctly-SCOPED expected delta prefix (right target) is NOT flagged
rc=$(run "$(up_json)" "$(printf '{"ipam/prefixes":[%s,%s]}' "$SHARED" "$DELTA_OK")")
grep -q "!= intended" "$TMP/out" && no "T7 a correctly-scoped delta prefix was wrongly flagged" \
                                  || ok "T7 a correctly-scoped delta prefix is NOT flagged (guard is not a blanket refusal)"

# T9: WRONG-TARGET scope. 10.12.4.0/22 is intended for vr1-dc0; bound to vr1-dc1 it
#     must fail. This is the D-117 near-miss (a DC's prefixes silently bound to the
#     OTHER DC's site) -- a mere "has a scope?" test was blind to it; asserting scope
#     == INTENDED catches it.
rc=$(run "$(up_json)" "$(printf '{"ipam/prefixes":[%s,%s]}' "$SHARED" "$DELTA_WRONGSITE")")
[ "$rc" = 1 ] && grep -q "!= intended" "$TMP/out" && ok "T9 a WRONG-TARGET scope (DC0 prefix on vr1-dc1) -> caught, exit 1" \
              || { no "T9 wrong-target scope not caught (rc=$rc)"; sed 's/^/       /' "$TMP/out"; }

# T10: the legitimately-UNSCOPED role container (10.10.0.0/16, intended None) present
#      as unscoped must NOT be flagged. This is the exact false-positive the fix
#      removes: the old "has a scope?" heuristic FAILED a faithful sandbox on this
#      prefix (mirroring the 27 unscoped role containers already in the upstream apex).
DELTA_UNSCOPED_OK='{"prefix":"10.10.0.0/16","status":"container","role":"office","scope_type":null,"description":"office role container","is_pool":false,"mark_utilized":false}'
rc=$(run "$(up_json)" "$(printf '{"ipam/prefixes":[%s,%s]}' "$SHARED" "$DELTA_UNSCOPED_OK")")
grep -q "!= intended" "$TMP/out" && no "T10 an intentionally-unscoped role container was wrongly flagged (false positive is back)" \
                                 || ok "T10 an intentionally-unscoped role container (10.10.0.0/16) is NOT flagged"

# T11: a D-120 ip-range DROPPED from an otherwise-complete sandbox -> EXPECTED-BUT-ABSENT.
#      Proves the new ip-ranges endpoint is wired into the both-bounds check (the D-120
#      "tooling gap" this closes: those objects used to be invisible to the checker).
python3 - "$TMP" <<'PY'
import json, sys
tmp = sys.argv[1]
sb = json.load(open(f"{tmp}/s1.json"))
sb["ipam/ip-ranges"] = [r for r in sb["ipam/ip-ranges"] if r["range"] != "10.10.1.2/24-10.10.1.49/24"]
json.dump(sb, open(f"{tmp}/s11.json", "w"))
PY
python3 "$CHECK" --upstream "$TMP/u1.json" --sandbox "$TMP/s11.json" >"$TMP/out" 2>&1; rc=$?
[ "$rc" = 1 ] && grep -q "EXPECTED-BUT-ABSENT" "$TMP/out" && grep -q "10.10.1.2/24-10.10.1.49/24" "$TMP/out" \
  && ok "T11 a dropped D-120 ip-range -> EXPECTED-BUT-ABSENT, exit 1" \
  || { no "T11 a missing D-120 range should fail (rc=$rc)"; grep -i "range\|absent" "$TMP/out" | sed 's/^/       /'; }

# T12: a STRAY ip-address not in EXPECTED_NEW -> UNEXPECTED EXTRA. Proves the
#      ip-addresses endpoint is likewise covered (a stray write cannot hide).
python3 - "$TMP" <<'PY'
import json, sys
tmp = sys.argv[1]
sb = json.load(open(f"{tmp}/s1.json"))
sb["ipam/ip-addresses"].append({"address": "10.10.1.99/24"})
json.dump(sb, open(f"{tmp}/s12.json", "w"))
PY
python3 "$CHECK" --upstream "$TMP/u1.json" --sandbox "$TMP/s12.json" >"$TMP/out" 2>&1; rc=$?
[ "$rc" = 1 ] && grep -q "UNEXPECTED EXTRA" "$TMP/out" && grep -q "10.10.1.99/24" "$TMP/out" \
  && ok "T12 a stray ip-address -> UNEXPECTED EXTRA, exit 1" \
  || { no "T12 a stray ip-address should fail (rc=$rc)"; grep -i "address\|extra" "$TMP/out" | sed 's/^/       /'; }

# T8: read-only -- no write verbs anywhere in the checker
grep -qE '\.(create|update|delete|save)\(' "$CHECK" \
  && no "T8 the checker contains a WRITE verb -- it must be read-only" \
  || ok "T8 read-only: no create/update/delete/save in the checker"

echo
if [ "$F" = 0 ]; then echo "sandbox-fidelity-check: $P/$P PASS"; exit 0; fi
echo "sandbox-fidelity-check: FAILURES: $F (passed $P)"; exit 1
