#!/usr/bin/env bash
# tests/render-dc-overlays/run-tests.sh
#
# Offline regression harness for scripts/render-dc-overlays.py (D-136 option (D)).
# No live infra, no network. Mutates NOTHING outside $TMP.
#
# THE CENTRAL ASSERTION is byte-for-byte reproduction of REVIEWED artifacts, on BOTH
# shapes: the FROZEN v4 fixture (immutable, so it stays a stable target) and the LIVE
# dual-family overlay (which is what actually deploys, and whose own header mandates
# that it remain renderer-reproducible). A renderer validated only against its own output
# is validated against nothing -- that is the objection D-136 raises against its
# own option (A). So the reproduction case is paired with THREE seeded faults
# proving the comparison can fail: a wrong octet, a dropped header, and wrong-DC
# prefixes. A byte-compare only ever observed passing is as untrustworthy as a
# gate only ever observed failing.
#
# Exit: 0 all pass | 1 any case failed.  ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
R="$REPO/scripts/render-dc-overlays.py"
CHK="$REPO/scripts/provider-bundle-check.py"
LIVE="$REPO/overlays/vr1-dc1-vips.yaml"
FIX="$REPO/tests/render-baseline/fixtures/vr1-dc1-vips.v4-11app.yaml"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
ok()  { echo "  PASS  $1"; PASS=$((PASS+1)); }
bad() { echo "  FAIL  $1"; FAIL=$((FAIL+1)); }
t_eq(){ if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (got '$2' want '$3')"; fi; }

echo "== render-dc-overlays: deterministic per-DC overlay renderer =="

# ---- derive ------------------------------------------------------------------
V4="$TMP/dc1-v4.yaml"      # v4 target = the FROZEN fixture (immutable)
# The dual target is the COMMITTED values file, not a fresh derive. `derive --from-overlay`
# lifts the header verbatim FROM the artifact being compared against, so deriving here and
# comparing against $LIVE made ~a third of the bytes self-referential and the header
# assertion unfalsifiable -- measured in the 2026-07-29 chain audit. Reading the committed
# values file makes T3b a real comparison and keeps it consistent with tests/render-drift.
V="$TMP/dc1-dual.yaml"
if python3 "$R" derive --dc vr1-dc1 --from-overlay "$FIX" \
     --output overlays/vr1-dc1-vips.yaml -o "$V4" 2>"$TMP/e1" \
   && cp "$REPO/render/values/vr1-dc1-vips.yaml" "$V"; then
  ok "T1 derive vr1-dc1 succeeds for both the v4 and dual-family shapes"
else
  bad "T1 derive failed: $(head -1 "$TMP/e1")"
fi

n_apps="$(python3 -c "import yaml,sys; print(len(yaml.safe_load(open(sys.argv[1]))['apps']))" "$V4" 2>/dev/null)"
t_eq "T2 derive defaults to the 11 BUILT apps (vault/designate excluded)" "$n_apps" "11"

# ---- the central assertion, on BOTH shapes -----------------------------------
if python3 "$R" check "$V4" --against "$FIX" >/dev/null 2>&1; then
  ok "T3 render reproduces the FROZEN v4 fixture BYTE-FOR-BYTE"
else
  bad "T3 render does NOT reproduce the frozen v4 fixture"
fi
if python3 "$R" check "$V" --against "$LIVE" >/dev/null 2>&1; then
  ok "T3b render reproduces the LIVE dual-family overlay BYTE-FOR-BYTE"
else
  bad "T3b render does NOT reproduce the live dual-family overlay"
fi

# T4 the same bytes as the FROZEN fixture, which is the durable target: the live
#    overlay is about to be rewritten by the R2/R11 reconciliation, the fixture is not.
got="$(python3 "$R" render "$V4" 2>/dev/null | sha256sum | cut -d' ' -f1)"
want="$(sha256sum "$FIX" | cut -d' ' -f1)"
t_eq "T4 rendered output matches the frozen render-baseline fixture hash" "$got" "$want"

# ---- proven able to FAIL (three seeded faults) --------------------------------
sed 's/octet: 50/octet: 51/' "$V4" > "$TMP/f1.yaml"
python3 "$R" check "$TMP/f1.yaml" --against "$FIX" >/dev/null 2>&1
t_eq "T5 a wrong octet FAILS the byte-compare" "$?" "1"

python3 - "$V4" "$TMP/f2.yaml" <<'PY'
import sys, yaml
v = yaml.safe_load(open(sys.argv[1])); v.pop("header", None)
yaml.safe_dump(v, open(sys.argv[2], "w"), sort_keys=False, width=4096)
PY
python3 "$R" check "$TMP/f2.yaml" --against "$FIX" >/dev/null 2>&1
t_eq "T6 a dropped comment header FAILS the byte-compare" "$?" "1"

python3 "$R" derive --dc vr1-dc0 --from-overlay "$FIX" -o "$TMP/f3.yaml" 2>/dev/null
python3 "$R" check "$TMP/f3.yaml" --against "$FIX" >/dev/null 2>&1
t_eq "T7 dc0 prefixes against the dc1 artifact FAIL (cross-DC)" "$?" "1"

# ---- determinism + purity ----------------------------------------------------
a="$(python3 "$R" render "$V4" 2>/dev/null | sha256sum)"
b="$(python3 "$R" render "$V4" 2>/dev/null | sha256sum)"
t_eq "T8 render is deterministic across runs" "$a" "$b"

# T9 render is PURE -- no apex, no network. A v4 values file must render with the
#    apex record deliberately unreachable, or the renderer is not offline-testable
#    and forward item F2's frozen interface is violated.
out="$(APEX_RECORD=/nonexistent/apex.json python3 "$R" render "$V4" 2>/dev/null | sha256sum | cut -d' ' -f1)"
t_eq "T9 render needs no apex (pure, offline)" "$out" "$want"

# T10 ...but DERIVING dual-family does, and must REFUSE rather than invent one.
APEX_RECORD=/nonexistent/apex.json python3 "$R" derive --dc vr1-dc1 --dual-family \
  -o "$TMP/nope.yaml" >/dev/null 2>&1
t_eq "T10 derive --dual-family REFUSES an unreadable apex (rc=2)" "$?" "2"

# ---- ordering ----------------------------------------------------------------
# T11 emission order is ASCENDING OCTET, not alphabetical. Sorting by name
#     reproduces nothing: keystone(.50) must precede barbican(.51).
first="$(python3 "$R" render "$V4" 2>/dev/null | grep -E '^  [a-z]' | head -1 | tr -d ' :')"
t_eq "T11 apps emit in ascending-octet order, not alphabetical" "$first" "keystone"

# ---- dual-family (R2) --------------------------------------------------------
DV="$V"
n2="$(python3 -c "import yaml,sys; print(len(yaml.safe_load(open(sys.argv[1]))['apps']))" "$DV" 2>/dev/null)"
t_eq "T12 --include-unbuilt adds R11's vault .61 + designate .62" "$n2" "13"

# T13 the v6 separator regression. The values file stores the /64 base with its
#     trailing colons stripped, so a single-colon join yields 2602:f3e2:f03:11:50
#     -- malformed, and it still LOOKS like an address. This was the renderer's
#     first dual-family output; provider-bundle-check caught it. Locked here.
if python3 "$R" render "$DV" 2>/dev/null | grep -q '2602:f3e2:f03:11::50'; then
  ok "T13 v6 legs use the '::' separator (the malformed-but-plausible regression)"
else
  bad "T13 v6 leg separator wrong -- check for a single-colon join"
fi
if python3 "$R" render "$DV" 2>/dev/null | grep -qE '2602:f3e2:f03:11:[0-9]+ '; then
  bad "T13b a single-colon v6 leg is present"
else
  ok "T13b no single-colon v6 leg emitted"
fi

# T14 the end-to-end acceptance criterion: the renderer's dual-family output is
#     VALID DEPLOY INPUT per the gate. This is what the 2026-07-28 arity work
#     exists to provide -- without it the dry-run below means nothing.
mkdir -p "$TMP/policies"
cp "$REPO/bundle.yaml" "$TMP/b.yaml"
cp "$REPO/policies/domain-manager-policy.yaml" "$REPO/policies/overrides.zip" "$TMP/policies/"
python3 "$R" render "$DV" -o "$TMP/vips.yaml" 2>/dev/null
# The MACHINES overlay is part of the dc1 deploy input, not optional scenery. Added
# 2026-07-29 with provider-bundle-check invariant 11: base + dc1 VIPs alone leaves the
# machines block tagged openstack-vr1-dc0, i.e. a dc1 model allocating dc0 hardware,
# and the gate now (correctly) rejects it. Asserting the renderer's output against a
# HALF deploy input was the reason this case went red.
if python3 "$CHK" "$TMP/b.yaml" --overlay "$REPO/overlays/vr1-dc1-machines.yaml" \
     --overlay "$TMP/vips.yaml" --dc vr1-dc1 >"$TMP/gate" 2>&1; then
  ok "T14 rendered dual-family overlay PASSES provider-bundle-check"
else
  bad "T14 rendered overlay rejected by the gate: $(grep -m1 FAIL "$TMP/gate")"
fi
if grep -q "13 clustered VIP(s).*13 dual-family" "$TMP/gate"; then
  ok "T15 the gate sees 13 VIPs, all dual-family"
else
  bad "T15 gate did not report 13 dual-family VIPs"
fi

# ---- refusals ----------------------------------------------------------------
printf 'dc: vr1-dc1\nlegs: [a]\n' > "$TMP/bad.yaml"
python3 "$R" render "$TMP/bad.yaml" >/dev/null 2>&1
t_eq "T16 a values file missing required keys REFUSES (rc=2)" "$?" "2"

echo
echo "RESULT: PASS=$PASS FAIL=$FAIL"
[[ "$FAIL" -eq 0 ]] && { echo "ALL PASS"; exit 0; } || exit 1
