#!/usr/bin/env bash
# tests/dc-node-v6-carve/run-tests.sh -- offline harness for scripts/dc-node-v6-carve.py.
# Stubs `maas` with a fakebin returning controlled JSON. Never touches live MAAS.
# Exit: 0 all pass | 1 any case failed.  ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
ok()  { PASS=$((PASS+1)); printf '  PASS  %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf '  FAIL  %s\n' "$1"; shift; [ $# -gt 0 ] && printf '%s\n' "$*" | sed 's/^/        /'; }

# mkbin <dir> <v6:full|none|novlan> <nodev6:none|correct|wrong> <tag:ok|missing>
mkbin() {
  local d="$1"; mkdir -p "$d/fakebin"
  cat > "$d/fakebin/maas" <<FB
#!/usr/bin/env bash
if [ "\$2" = "subnets" ]; then python3 "$d/subnets.py"; exit 0; fi
if [ "\$2" = "machines" ]; then python3 "$d/machines.py"; exit 0; fi
if [ "\$2" = "interface" ]; then echo '{}'; exit 0; fi
exit 1
FB
  chmod +x "$d/fakebin/maas"
  cat > "$d/subnets.py" <<PY
import json
mode = "$2"
s = [{"cidr": "10.12.8.0/22", "id": 6, "vlan": {"id": 5005}},
     {"cidr": "10.12.4.0/22", "id": 7, "vlan": {"id": 5189}}]
if mode == "full":
    s += [{"cidr": "fd50:840e:74e2:220::/64", "id": 21, "vlan": {"id": 5005}},
          {"cidr": "2602:f3e2:f02:10::/64",   "id": 23, "vlan": {"id": 5189}}]
elif mode == "novlan":
    s += [{"cidr": "fd50:840e:74e2:220::/64", "id": 21, "vlan": {"id": 5005}}]
print(json.dumps(s))
PY
  cat > "$d/machines.py" <<PY
import json
nodev6, tag = "$3", "$4"
links_admin = [{"mode": "static", "ip_address": "10.12.8.121",
                "subnet": {"cidr": "10.12.8.0/22", "vlan": {"id": 5005}}}]
links_brex  = [{"mode": "static", "ip_address": "10.12.4.121",
                "subnet": {"cidr": "10.12.4.0/22", "vlan": {"id": 5189}}}]
if nodev6 == "correct":
    links_admin.append({"mode": "static", "ip_address": "fd50:840e:74e2:220::121",
                        "subnet": {"cidr": "fd50:840e:74e2:220::/64", "vlan": {"id": 5005}}})
    links_brex.append({"mode": "static", "ip_address": "2602:f3e2:f02:10::121",
                       "subnet": {"cidr": "2602:f3e2:f02:10::/64", "vlan": {"id": 5189}}})
elif nodev6 == "wrong":
    links_admin.append({"mode": "static", "ip_address": "fd50:840e:74e2:220::999",
                        "subnet": {"cidr": "fd50:840e:74e2:220::/64", "vlan": {"id": 5005}}})
m = {"hostname": "n1", "system_id": "aaa", "status_name": "Ready",
     "tag_names": (["virtual", "openstack-vr1-dc0"] if tag == "ok" else ["virtual"]),
     "interface_set": [
        {"name": "enp1s0", "id": 216, "links": links_admin},
        {"name": "enp2s0", "id": 223, "links": []},
        {"name": "br-ex",  "id": 404, "links": links_brex}]}
print(json.dumps([m]))
PY
}

run() { # run <want_rc> <regex> <label> <dir> <action> [args...]
  local want="$1" re="$2" label="$3" d="$4"; shift 4
  local out rc
  out="$(cd "$REPO" && PATH="$d/fakebin:$PATH" python3 scripts/dc-node-v6-carve.py "$@" 2>&1)"; rc=$?
  if [ "$rc" = "$want" ] && grep -qE "$re" <<<"$out"; then ok "$label"
  else bad "$label (rc=$rc want=$want)" "$(printf '%s' "$out" | tail -5)"; fi
}

echo "== dc-node-v6-carve =="

D="$TMP/none"; mkbin "$D" full none ok
run 1 'planned v6 links : 2' "T1 uncarved node plans one v6 link per v4-carrying iface" "$D" check vr1-dc0

# THE OCTET MIRROR IS TEXTUAL. .121 -> ::121, never the hex 0x121 or hex(121)=0x79.
run 1 'fd50:840e:74e2:220::121' "T2 mirror is TEXTUAL: .121 -> ::121" "$D" check vr1-dc0
out="$(cd "$REPO" && PATH="$D/fakebin:$PATH" python3 scripts/dc-node-v6-carve.py check vr1-dc0 2>&1)"
if ! grep -qE '::(79|121121)' <<<"$out"; then ok "T3 no hex conversion of the octet"
else bad "T3 octet was hex-converted" "$out"; fi

# The RAW provider NIC (D-100) has no v4 link and must be SKIPPED, not errored --
# and br-ex, which carries provider-public instead, must be INCLUDED.
if ! grep -q 'enp2s0' <<<"$out" && grep -q 'br-ex' <<<"$out"; then
  ok "T4 raw provider NIC skipped (no v4 link); br-ex included (has one)"
else bad "T4 provider-NIC/br-ex handling wrong" "$out"; fi

D="$TMP/done"; mkbin "$D" full correct ok
run 0 'PASS: dc-node-v6-carve' "T5 fully-carved fixture PASSES -- the gate can go green" "$D" check vr1-dc0

D="$TMP/wrong"; mkbin "$D" full wrong ok
run 1 'REFUSING to add a second address' \
  "T6 an interface with a DIFFERENT v6 refuses rather than adding a second" "$D" check vr1-dc0

D="$TMP/novlan"; mkbin "$D" novlan none ok
run 1 'NO v6 subnet on that vlan' \
  "T7 a vlan with no v6 subnet is a NAMED error, not a silent skip" "$D" check vr1-dc0

# A tag that matches nothing must REFUSE, never read as "0 planned, all clean".
D="$TMP/notag"; mkbin "$D" full none missing
run 2 'Refusing to fall back to a heuristic' \
  "T8 no tagged machines REFUSES (exit 2) rather than reporting a clean zero" "$D" check vr1-dc0

# apply without --commit must write nothing and say so.
D="$TMP/dry"; mkbin "$D" full none ok
run 0 'DRY RUN -- nothing was written' "T9 apply is DRY by default" "$D" apply vr1-dc0

# --- PROFILE RESOLUTION (2026-07-30) ---------------------------------------
# This script was ENV-BLIND: default "admin", --profile only, no environment
# read. Every bash tool here honours MAAS_PROFILE, so `export MAAS_PROFILE=...`
# left THIS tool silently targeting OFFICE1 -- the unrecoverable direction
# during the D-132 q1 per-DC region migration. These assertions pin all three
# resolution paths AND require the run to SAY which region it targets, because a
# capture that does not name the profile cannot be audited for wrong-region work.
pchk(){ # pchk <desc> <expected-substring> <env-value-or-empty> [extra args...]
  local desc="$1" want="$2" envv="$3"; shift 3
  local out
  if [ -n "$envv" ]; then out="$(MAAS_PROFILE="$envv" python3 "$REPO/scripts/dc-node-v6-carve.py" check vr1-dc0 "$@" 2>&1)"
  else out="$(env -u MAAS_PROFILE python3 "$REPO/scripts/dc-node-v6-carve.py" check vr1-dc0 "$@" 2>&1)"; fi
  case "$out" in *"$want"*) echo "  PASS  $desc"; PASS=$((PASS+1)) ;;
                 *) echo "  FAIL  $desc (wanted '$want' in: $(printf '%s' "$out" | head -1))"; FAIL=$((FAIL+1)) ;; esac
}
pchk "T10 MAAS_PROFILE env is honoured"          "profile 'env-region' (from MAAS_PROFILE env)" "env-region"
pchk "T11 --profile overrides the environment"   "profile 'flag-region' (from --profile)"       "env-region" --profile flag-region
pchk "T12 no env and no flag falls back to admin" "profile 'admin' (from built-in default)"     ""

# An ABSENT maas CLI must REFUSE (exit 2), not traceback (exit 1). A traceback is
# indistinguishable from a real failure and breaks the REFUSE contract.
OUT="$(PATH=/nonexistent-for-test:/usr/bin:/bin python3 "$REPO/scripts/dc-node-v6-carve.py" check vr1-dc0 2>&1)"; RC=$?
if [ "$RC" = 2 ]; then echo "  PASS  T13 absent maas CLI REFUSES with exit 2"; PASS=$((PASS+1));
else echo "  FAIL  T13 absent maas CLI REFUSES with exit 2 (got $RC)"; FAIL=$((FAIL+1)); fi
case "$OUT" in *Traceback*) echo "  FAIL  T14 absent maas CLI does not traceback"; FAIL=$((FAIL+1)) ;;
               *) echo "  PASS  T14 absent maas CLI does not traceback"; PASS=$((PASS+1)) ;; esac

# ---- v6 FAMILY + replace (added 2026-08-01, D-139 step 3) --------------------
# D-139 step 2 puts a GUA /64 alongside every ULA /64 on the SAME vlan, so from that
# moment `v6_by_vlan[vid] = s` decided the FAMILY of all 54 node statics by MAAS's JSON
# array order. Measured live: it picked GUA -- luck, not a contract, and the FOURTH copy
# of the defect the 2026-07-29 chain audit found in the apex readers.
mkfam() {   # $1 dir, $2 subnets: both|dupsame|guaonly, $3 node v6: none|ula|gua|two
  local d="$1"; mkdir -p "$d/fakebin"
  {
    echo '#!/usr/bin/env bash'
    echo "if [ \"\$2\" = \"subnets\" ]; then python3 $d/subnets.py; exit 0; fi"
    echo "if [ \"\$2\" = \"machines\" ]; then python3 $d/machines.py; exit 0; fi"
    echo "if [ \"\$2\" = \"interface\" ]; then echo '{}'; exit 0; fi"
    echo 'exit 1'
  } > "$d/fakebin/maas"
  chmod +x "$d/fakebin/maas"
  SUBMODE="$2" python3 -c '
import json, os, sys
mode = os.environ["SUBMODE"]
s = [{"cidr": "10.12.8.0/22", "id": 6, "vlan": {"id": 5005}}]
if mode in ("both", "guaonly"):
    s += [{"cidr": "2602:f3e2:f02:20::/64", "id": 31, "vlan": {"id": 5005}}]
if mode == "both":
    s += [{"cidr": "fd50:840e:74e2:220::/64", "id": 21, "vlan": {"id": 5005}}]
if mode == "dupsame":
    s += [{"cidr": "2602:f3e2:f02:20::/64", "id": 31, "vlan": {"id": 5005}},
          {"cidr": "2602:f3e2:f02:2f::/64", "id": 32, "vlan": {"id": 5005}}]
open(sys.argv[1], "w").write("import json\nprint(%r)\n" % json.dumps(s))
' "$d/subnets.py"
  NODEV6="$3" python3 -c '
import json, os, sys
w = os.environ["NODEV6"]
links = [{"mode": "static", "ip_address": "10.12.8.121", "id": 300,
          "subnet": {"cidr": "10.12.8.0/22", "vlan": {"id": 5005}}}]
ula = {"mode": "static", "ip_address": "fd50:840e:74e2:220::121", "id": 410,
       "subnet": {"cidr": "fd50:840e:74e2:220::/64", "vlan": {"id": 5005}}}
gua = {"mode": "static", "ip_address": "2602:f3e2:f02:20::121", "id": 411,
       "subnet": {"cidr": "2602:f3e2:f02:20::/64", "vlan": {"id": 5005}}}
if w == "ula": links.append(ula)
if w == "gua": links.append(gua)
if w == "two": links += [ula, gua]
m = [{"hostname": "vr1-dc0-compute-01", "system_id": "abc123",
      "tag_names": ["openstack-vr1-dc0"],
      "interface_set": [{"name": "enp1s0", "id": 99, "links": links}]}]
open(sys.argv[1], "w").write("import json\nprint(%r)\n" % json.dumps(m))
' "$d/machines.py"
}
runfam() { local d="$1"; shift
  OUT="$(PATH="$d/fakebin:$PATH" python3 "$REPO/scripts/dc-node-v6-carve.py" "$@" 2>&1)"; RC=$?; }
fok(){ PASS=$((PASS+1)); echo "  PASS  $1"; }
fbad(){ FAIL=$((FAIL+1)); echo "  FAIL  $1"; }

D="$TMP/f1"; mkfam "$D" both none; runfam "$D" check vr1-dc0 --profile p
{ [ "$RC" -ne 0 ] && printf '%s' "$OUT" | grep -q -- "--v6-family"; } \
  && fok "F1 both families + no flag REFUSES and names --v6-family" \
  || fbad "F1 did not refuse/name the flag (rc=$RC)"

D="$TMP/f2"; mkfam "$D" both none; runfam "$D" check vr1-dc0 --profile p --v6-family gua
printf '%s' "$OUT" | grep -q '2602:f3e2:f02:20::121' \
  && fok "F2 --v6-family gua selects the GUA /64" || fbad "F2 gua not selected"

# F3 proves F2 is a CHOICE, not the tool happening to prefer GUA.
D="$TMP/f3"; mkfam "$D" both none; runfam "$D" check vr1-dc0 --profile p --v6-family ula
printf '%s' "$OUT" | grep -q 'fd50:840e:74e2:220::121' \
  && fok "F3 --v6-family ula selects the ULA /64" || fbad "F3 ula not selected"

D="$TMP/f4"; mkfam "$D" dupsame none; runfam "$D" check vr1-dc0 --profile p --v6-family gua
{ [ "$RC" -ne 0 ] && printf '%s' "$OUT" | grep -qi 'TWO gua'; } \
  && fok "F4 two SAME-family v6 subnets on one vlan REFUSE" || fbad "F4 same-family dup not refused"

D="$TMP/f5"; mkfam "$D" both ula; runfam "$D" apply vr1-dc0 --profile p --v6-family gua
printf '%s' "$OUT" | grep -q 'use the .replace. action' \
  && fok "F5 apply still refuses a second address, points at replace" || fbad "F5 apply did not refuse"

D="$TMP/f6"; mkfam "$D" both ula; runfam "$D" replace vr1-dc0 --profile p --v6-family gua
printf '%s' "$OUT" | grep -q 'planned v6 links : 1' \
  && fok "F6 replace plans the ULA->GUA swap" || fbad "F6 replace did not plan the swap"

D="$TMP/f7"; mkfam "$D" guaonly gua; runfam "$D" replace vr1-dc0 --profile p --v6-family gua
printf '%s' "$OUT" | grep -qi 'correct\|SAME family' \
  && fok "F7 replace declines a same-family change" || fbad "F7 same-family not declined"

# F8 a NIC carrying the WANTED address AND another global is NOT "already correct".
# ASSERTION REPLACED, not deleted: it first read 'exactly ONE|guess which', which is the
# >1-existing-v6 refusal on the replace path. Writing this case showed the tool never
# reached that path -- `want in have` short-circuited to skipped++ and counted a NIC with
# two globals as CORRECT, silently masking a half-done family migration that G19 forbids.
# The tool was fixed to error there; this is the new invariant.
D="$TMP/f8"; mkfam "$D" both two; runfam "$D" replace vr1-dc0 --profile p --v6-family gua
printf '%s' "$OUT" | grep -qi "more than one global v6 on a NIC" \
  && fok "F8 a NIC with the wanted addr PLUS another global is an ERROR, not 'correct'" \
  || fbad "F8 multi-global NIC not flagged"

# F9 the >1-existing-v6 refusal on the replace path itself, reached when the wanted
# address is ABSENT and two other globals are present -- the case F8 was written for.
D="$TMP/f9"; mkfam "$D" both two; runfam "$D" replace vr1-dc0 --profile p --v6-family ula
printf '%s' "$OUT" | grep -qi 'exactly ONE\|guess which\|more than one global' \
  && fok "F9 replace will not guess which of two v6 links to drop" \
  || fbad "F9 did not refuse two existing v6 links"

echo
echo "RESULT: PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ] || exit 1
