#!/usr/bin/env bash
# tests/pre-flight-checks/run-tests.sh -- offline harness for scripts/pre-flight-checks.sh
# (preflight's P4 gate).
#
# WHY IT EXISTS. Until 2026-07-29 this script had NO harness at all, and the only place
# it had ever been observed running was a live jumphost where it FAILED -- eleven fatals,
# of which zero described the cloud. That is the repo's own inverse rule biting: "a
# checker that cannot fail is not a gate" has a twin, and a gate only ever observed
# FAILING needs a constructed fixture proving it can go green. So T1/T2/T3 build a
# CORRECT DC and require exit 0, and everything after them flips exactly one field of
# that same fixture and requires the gate to go red on the assertion that field feeds.
#
# HERMETIC: a fake `maas` on PATH serves JSON from files in the fixture; nothing reaches
# a real MAAS, a real cloud or the real repo. Fixtures are BUILT FROM lib-net.sh +
# lib-hosts.sh for the selected DC, so a green case attests that the gate agrees with the
# libraries rather than with a second hand-typed copy of the same numbers.
#
# 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()  { echo "  PASS  $1"; PASS=$((PASS+1)); }
bad() { echo "  FAIL  $1"; FAIL=$((FAIL+1)); }

command -v jq >/dev/null 2>&1 || { echo "  FAIL  jq is required by the script under test"; echo "RESULT: PASS=0 FAIL=1"; exit 1; }

# ---------------------------------------------------------------------------
# mkfix <name> <dc> -> echoes the fixture repo path
# Builds a repo whose MAAS state is exactly what the selected DC's libraries describe.
mkfix() {
  local name="$1" dc="$2" d="$TMP/$1"
  mkdir -p "$d/scripts" "$d/overlays" "$d/fakebin" "$d/maasdata"
  cp "$REPO/scripts/pre-flight-checks.sh" "$REPO/scripts/lib-net.sh" \
     "$REPO/scripts/lib-hosts.sh" "$d/scripts/"
  printf 'applications:\n  keystone:\n    charm: keystone\n' > "$d/bundle.yaml"

  # The octavia-pki overlay is a gitignored SECRET in the real tree, so the fixture
  # mints its own with the 5 lb-mgmt-* keys the gate counts. No key material: the
  # values are placeholders and the gate never prints them anyway.
  {
    printf 'applications:\n  octavia:\n    options:\n'
    for k in lb-mgmt-issuing-cacert lb-mgmt-issuing-ca-private-key \
             lb-mgmt-issuing-ca-key-passphrase lb-mgmt-controller-cacert \
             lb-mgmt-controller-cert; do
      printf '      %s: PLACEHOLDER\n' "$k"
    done
  } > "$d/overlays/${dc}-octavia-pki.yaml"

  # Dump the DC's own view of planes and hosts. A SUBSHELL per dump: both libs refuse a
  # second, different selection in the same shell.
  ( . "$REPO/scripts/lib-net.sh"; lib_net_select_dc "$dc" >/dev/null
    for c in "${PLANE_CIDRS[@]}"; do printf 'P\t%s\t%s\t%s\n' "$c" "${PLANE_NAME[$c]}" "${PLANE_GW[$c]:-}"; done
    for c in "${DATA_PLANE_CIDRS[@]}"; do printf 'D\t%s\n' "$c"; done
    printf 'M\t%s\n' "$METAL_INTERNAL_CIDR"
    printf 'I\t%s\n' "${METAL_INTERNAL_IFACE:-}"
    printf 'T\t%s\n' "${METAL_INTERNAL_VID:-0}"
    printf 'V\t%s\t%s\t%s\t%s\t%s\n' "$VIP_PREFIX_PROVIDER" "$VIP_PREFIX_ADMIN" \
           "$VIP_PREFIX_INTERNAL" "$VIP_OCTET_MIN" "$VIP_COUNT_EXPECT"
  ) > "$d/maasdata/planes.tsv"
  ( . "$REPO/scripts/lib-hosts.sh"; lib_hosts_select_dc "$dc" >/dev/null
    for h in "${HOSTS[@]}"; do printf 'H\t%s\t%s\t%s\n' "$h" "${HOST_OCTET[$h]}" "${HOST_BOOT_MAC[$h]}"; done
  ) > "$d/maasdata/hosts.tsv"

  D="$d" python3 - <<'PY'
import json, os
d = os.environ["D"]
planes, data, hosts = [], [], []
mi = mi_iface = ""
mi_vid = 0
vip = None
for line in open(d + "/maasdata/planes.tsv"):
    f = line.rstrip("\n").split("\t")
    if   f[0] == "P": planes.append((f[1], f[2], f[3]))
    elif f[0] == "D": data.append(f[1])
    elif f[0] == "M": mi = f[1]
    elif f[0] == "I": mi_iface = f[1]
    elif f[0] == "T": mi_vid = int(f[1] or 0)
    elif f[0] == "V": vip = f[1:]
for line in open(d + "/maasdata/hosts.tsv"):
    f = line.rstrip("\n").split("\t")
    if f[0] == "H": hosts.append((f[1], int(f[2]), f[3]))

subnets = []
for i, (cidr, name, gw) in enumerate(planes):
    # VR0's metal-internal is the tagged VLAN-103 stack; every VR1 plane is untagged
    # (D-133 flat carve), and MAAS renders the untagged/default VLAN as vid 0.
    subnets.append({"id": 100 + i, "cidr": cidr, "name": name,
                    "vlan": {"vid": mi_vid if cidr == mi else 0},
                    "gateway_ip": gw or None, "dns_servers": []})
json.dump(subnets, open(d + "/maasdata/subnets.json", "w"), indent=1)

# The D-134 utility band (.4-.49) is per-DC infrastructure, not an OpenStack role node.
# VR0's .40-.43 index predates those bands, so vr0-dc0's fleet is all role nodes.
vr0 = any(h[0].startswith("openstack") for h in hosts)
machines = []
for n, (host, octet, mac) in enumerate(hosts):
    role = vr0 or octet >= 100
    sid = "sysid%02d" % n
    # A DELIBERATELY random MAAS hostname for VR1: lib-hosts' VR1 HOSTS entries are tofu
    # node names, and MAAS re-mints its own name on every re-enlistment. A gate that
    # resolves by hostname cannot pass this fixture.
    maas_hostname = host if vr0 else "maas-random-%02d" % n
    machines.append({"system_id": sid, "hostname": maas_hostname,
                     # role nodes are handed off Ready (DOCFIX-200); the D-104 Juju
                     # controller is legitimately Deployed once bootstrapped.
                     "status_name": "Ready" if role else "Deployed",
                     "power_state": "off",
                     "boot_interface": {"mac_address": mac}})
    ifaces = []
    if not vr0:
        ifaces.append({"name": "enp1s0", "type": "physical",
                       "links": [{"subnet": {"cidr": planes[1][0]},
                                  "ip_address": planes[1][0].split("/")[0].rsplit(".", 1)[0] + ".%d" % octet}]})
        ifaces.append({"name": "enp2s0", "type": "physical", "links": []})
    for j, cidr in enumerate(data):
        base = cidr.split("/")[0].rsplit(".", 1)[0]
        if cidr == mi and mi_iface:
            nm, ty = mi_iface, "bridge"        # VR0 Pattern A: the br-internal stack
        else:
            nm, ty = "enp%ds0" % (3 + j), "physical"   # D-133 flat carve
        ifaces.append({"name": nm, "type": ty,
                       "links": [{"subnet": {"cidr": cidr}, "ip_address": "%s.%d" % (base, octet)}]})
    json.dump(ifaces, open(d + "/maasdata/if-%s.json" % sid, "w"), indent=1)
json.dump(machines, open(d + "/maasdata/machines.json", "w"), indent=1)

# A VIP overlay for THIS DC, built from the DC's own bands so a green run attests the
# gate and lib-net agree. Column order provider / metal-admin / metal-internal.
pp, pa, pi, lo, cnt = vip[0], vip[1], vip[2], int(vip[3]), int(vip[4])
with open(d + "/overlays/%s-vips.yaml" % os.environ["DCNAME"], "w") as fh:
    fh.write("applications:\n")
    for k in range(cnt):
        o = lo + k
        fh.write("  app%02d:\n    options:\n      vip: \"%s.%d %s.%d %s.%d\"\n" % (k, pp, o, pa, o, pi, o))
PY
  echo "$d"
}

# The python block needs the DC name for the overlay filename.
mkfix_dc() { DCNAME="$2" mkfix "$@"; }

# fakebin maas: serves the fixture JSON. Nothing else is answerable, and an
# unrecognised call exits non-zero rather than printing an empty success.
mkmaas() {
  local d="$1"
  cat > "$d/fakebin/maas" <<'FB'
#!/usr/bin/env bash
DD="$(dirname "$(dirname "$0")")/maasdata"
case "$2 $3" in
  "subnets read")    cat "$DD/subnets.json" ;;
  "machines read")   cat "$DD/machines.json" ;;
  "interfaces read") cat "$DD/if-$4.json" 2>/dev/null || echo "[]" ;;
  *) echo "fake maas: unhandled '$*'" >&2; exit 2 ;;
esac
FB
  chmod +x "$d/fakebin/maas"
}

# run <dir> <dc> <want_rc> <regex-or-"-"> <label>
run() {
  local d="$1" dc="$2" want="$3" rx="$4" label="$5" out rc
  out="$(PATH="$d/fakebin:$PATH" DC="$dc" bash "$d/scripts/pre-flight-checks.sh" 2>&1)"; rc=$?
  local hit=1
  [ "$rx" = "-" ] || { grep -qE "$rx" <<<"$out"; hit=$?; }
  if [ "$rc" = "$want" ] && [ "$hit" -eq 0 ]; then
    ok "$label"
  else
    bad "$label (rc=$rc want=$want)"
    printf '%s\n' "$out" | grep -E '^(FAIL|WARN|Summary)' | head -6 | sed 's/^/        /'
  fi
}
# jqedit <dir> <file> <jq-program>  -- rewrite one fixture file in place
jqedit() { local f="$1/maasdata/$2"; jq "$3" "$f" > "$f.new" && mv "$f.new" "$f"; }

echo "== pre-flight-checks: the gate can go GREEN on a correct DC =="
D1="$(mkfix_dc green_dc1 vr1-dc1)"; mkmaas "$D1"
run "$D1" vr1-dc1 0 'Summary: 0 fatal, 0 warning' "T1 vr1-dc1 correct fixture -> exit 0"
D0="$(mkfix_dc green_dc0 vr1-dc0)"; mkmaas "$D0"
run "$D0" vr1-dc0 0 'Summary: 0 fatal, 0 warning' "T2 vr1-dc0 correct fixture -> exit 0"
DV="$(mkfix_dc green_vr0 vr0-dc0)"; mkmaas "$DV"
run "$DV" vr0-dc0 0 'Summary: 0 fatal, 0 warning' "T3 vr0-dc0 correct fixture -> exit 0 (VR0 arm still works)"

echo "== it is actually DC-AWARE, not DC-blind =="
# The single most important negative: point a dc0 run at dc1's MAAS and it must NOT pass.
# Before 2026-07-29 this script never read DC, so both invocations were byte-identical.
run "$D1" vr1-dc0 1 'plane metal-admin \(10.12.8.0/22\) NOT FOUND' \
    "T4 DC=vr1-dc0 against dc1's MAAS -> FAILS (planes are per-DC)"
run "$D0" vr1-dc1 1 'plane metal-admin \(10.12.68.0/22\) NOT FOUND' \
    "T5 DC=vr1-dc1 against dc0's MAAS -> FAILS (planes are per-DC)"
run "$D1" vr1-dc1 0 'gating DC=vr1-dc1' "T6 the header names the DC it measured"
run "$D1" bogus-dc 1 'refused DC' "T7 unknown DC token REFUSES (no default-to-success)"
run "$D1" dc1 1 'RETIRED' "T8 bare 'dc1' is rejected as RETIRED (D-119)"

echo "== item 3.7: the metal-internal expectations are D-133-correct, and still falsifiable =="
DT="$(mkfix_dc vid_tagged vr1-dc1)"; mkmaas "$DT"
jqedit "$DT" subnets.json '(.[] | select(.cidr=="10.12.72.0/22") | .vlan.vid) = 103'
run "$DT" vr1-dc1 1 'D-133 rules it UNTAGGED' "T9 a VLAN tag on VR1 metal-internal -> FAIL (abolished stack returning)"
DN="$(mkfix_dc vid_unreadable vr1-dc1)"; mkmaas "$DN"
jqedit "$DN" subnets.json '(.[] | select(.cidr=="10.12.72.0/22") | .vlan) = null'
run "$DN" vr1-dc1 1 'NO readable VLAN' "T10 an unreadable VLAN REFUSES (does not read as untagged)"
DB="$(mkfix_dc mi_bridged vr1-dc1)"; mkmaas "$DB"
jqedit "$DB" if-sysid00.json '(.[] | select(.links[0].subnet.cidr=="10.12.72.0/22") | .type) = "bridge"'
run "$DB" vr1-dc1 1 'D-133 rules a FLAT carve' "T11 a bridge over VR1 metal-internal -> FAIL (flat-carve invariant)"
DVV="$(mkfix_dc vr0_vid_wrong vr0-dc0)"; mkmaas "$DVV"
jqedit "$DVV" subnets.json '(.[] | select(.cidr=="10.12.12.0/22") | .vlan.vid) = 0'
run "$DVV" vr0-dc0 1 'want 103' "T12 VR0 still asserts VID 103 (the guard did not delete the VR0 branch)"

echo "== the retired metal-admin gateway expectation =="
# lib-net's VR1 arms no longer pin 10.12.8.1 / 10.12.68.1: measured 2026-07-27, MAAS
# carries no gateway there and the address answers nothing (D-134 rules .1 gateways for
# the PROVIDER subnets only). T1/T2 above are the proof that gateway=none now PASSES.
# This case is the other half: the expectation was retired, not deleted -- an
# unexplained gateway appearing on metal-admin is still reported.
DG="$(mkfix_dc spurious_gw vr1-dc1)"; mkmaas "$DG"
jqedit "$DG" subnets.json '(.[] | select(.cidr=="10.12.68.0/22") | .gateway_ip) = "10.12.68.1"'
run "$DG" vr1-dc1 2 'metal-admin has gateway 10.12.68.1' "T13 a spurious metal-admin gateway is still reported (WARN)"
DGP="$(mkfix_dc missing_gw vr1-dc1)"; mkmaas "$DGP"
jqedit "$DGP" subnets.json '(.[] | select(.cidr=="10.12.64.0/22") | .gateway_ip) = null'
run "$DGP" vr1-dc1 1 'provider-public gateway=none' "T14 a MISSING provider-public gateway still FAILS (ruled edge LAN gw)"

echo "== node identity: VR1 resolves by pinned boot MAC, never by MAAS hostname =="
DM="$(mkfix_dc mac_gone vr1-dc1)"; mkmaas "$DM"
jqedit "$DM" machines.json '(.[] | select(.system_id=="sysid00") | .boot_interface.mac_address) = "52:54:00:de:ad:00"'
run "$DM" vr1-dc1 1 'vr1-dc1-control-01 not enrolled in MAAS' "T15 a role node whose pinned MAC is absent -> FAIL"
DS="$(mkfix_dc not_ready vr1-dc1)"; mkmaas "$DS"
jqedit "$DS" machines.json '(.[] | select(.system_id=="sysid03") | .status_name) = "Broken"'
run "$DS" vr1-dc1 1 'status=Broken \(want Ready\)' "T16 a role node not Ready -> FAIL (DOCFIX-200 hand-off state)"
DL="$(mkfix_dc link_gone vr1-dc1)"; mkmaas "$DL"
jqedit "$DL" if-sysid05.json '[ .[] | select(.links[0].subnet.cidr != "10.12.80.0/22") ]'
run "$DL" vr1-dc1 1 'missing link on storage' "T17 a role node missing a data-plane link -> FAIL"
DO="$(mkfix_dc bad_octet vr1-dc1)"; mkmaas "$DO"
jqedit "$DO" if-sysid01.json '(.[] | select(.links[0].subnet.cidr=="10.12.76.0/22") | .links[0].ip_address) = "10.12.76.199"'
run "$DO" vr1-dc1 2 'want last octet .101' "T18 a wrong D-134 octet -> WARN"

echo "== the D-104 Juju controller is classified, not silently skipped =="
run "$D1" vr1-dc1 0 'utility-band infrastructure, not an OpenStack role node' \
    "T19 the .5 utility node is NAMED and excluded with its reason"
# ... and its own record is still asserted: it is a pinned node of this DC.
DJ="$(mkfix_dc juju_gone vr1-dc1)"; mkmaas "$DJ"
jqedit "$DJ" machines.json '[ .[] | select(.system_id != "sysid09") ]'
run "$DJ" vr1-dc1 1 'utility band .5\) has NO MAAS record' "T20 the utility node vanishing still FAILS"
# It must NOT be asserted Ready: it is legitimately Deployed once bootstrapped, and the
# green fixture above already carries status Deployed for it. T1 passing IS that proof;
# this pins the reason so a later edit cannot re-add the assertion unnoticed.
if PATH="$D1/fakebin:$PATH" DC=vr1-dc1 bash "$D1/scripts/pre-flight-checks.sh" 2>&1 \
   | grep -q 'juju-01 status='; then
  bad "T21 the bootstrapped controller must NOT be asserted Ready"
else
  ok "T21 the bootstrapped controller (status Deployed) is not asserted Ready"
fi

echo "== CHECK 0 / CHECK 1 are per-DC artifacts =="
DP="$(mkfix_dc pki_gone vr1-dc1)"; mkmaas "$DP"; rm -f "$DP/overlays/vr1-dc1-octavia-pki.yaml"
run "$DP" vr1-dc1 1 'MISSING overlays/vr1-dc1-octavia-pki.yaml' "T22 CHECK 0 names the PER-DC overlay (F1)"
DK="$(mkfix_dc pki_short vr1-dc1)"; mkmaas "$DK"
printf 'applications:\n  octavia:\n    options:\n      lb-mgmt-controller-cert: X\n' \
  > "$DK/overlays/vr1-dc1-octavia-pki.yaml"
run "$DK" vr1-dc1 1 'lb-mgmt-\* keys \(want 5\)' "T23 CHECK 0 asserts CONTENT, not existence"
# The inherit_errexit trap this repo has hit three times: a bare $( ) whose pipeline
# fails ABORTS instead of reaching the refusal branch. bundle.yaml is VIP-FREE, so with
# no per-DC overlay the vip grep matches NOTHING -- the normal case here, not an edge
# one. The gate must REFUSE with a message AND still reach its summary.
DW="$(mkfix_dc vips_gone vr1-dc1)"; mkmaas "$DW"; rm -f "$DW/overlays/vr1-dc1-vips.yaml"
run "$DW" vr1-dc1 1 'no VIP overlay for DC=vr1-dc1' "T24 a missing per-DC VIP overlay REFUSES with a message"
run "$DW" vr1-dc1 1 'Summary: [0-9]+ fatal' "T25 ... and the run still reaches its summary (no silent abort)"
DX="$(mkfix_dc vips_wrong vr1-dc1)"; mkmaas "$DX"
sed -i 's/10\.12\.68\.50/10.12.99.50/' "$DX/overlays/vr1-dc1-vips.yaml"
run "$DX" vr1-dc1 1 'WRONG admin col' "T26 a VIP outside this DC's admin band -> FAIL"
# The REAL committed artifact must pass too: a fixture-only green would prove nothing
# about the file the deploy actually consumes (R2 dual-family sextets, 13 apps).
DR="$(mkfix_dc real_overlay vr1-dc1)"; mkmaas "$DR"
cp "$REPO/overlays/vr1-dc1-vips.yaml" "$DR/overlays/vr1-dc1-vips.yaml"
run "$DR" vr1-dc1 0 'Summary: 0 fatal, 0 warning' "T27 the REAL committed vr1-dc1 VIP overlay passes CHECK 1"
# ... and the dc0 twin, which matters MORE: vr1-dc0 is the DEFAULT DC, so this is the
# artifact preflight feeds its gates when nobody sets DC at all.
DR0="$(mkfix_dc real_overlay_dc0 vr1-dc0)"; mkmaas "$DR0"
cp "$REPO/overlays/vr1-dc0-vips.yaml" "$DR0/overlays/vr1-dc0-vips.yaml"
run "$DR0" vr1-dc0 0 'Summary: 0 fatal, 0 warning' "T28 the REAL committed vr1-dc0 VIP overlay passes CHECK 1 (default DC)"

# T28b PROOF-OF-TEETH for the D-020 vault metal-only branch: restore vault's OLD
# provider/admin/internal triple and require CHECK 1 to reject it. Without this the
# vault exception could accept anything named vault -- a gate that cannot fail.
DVT="$(mkfix_dc vault_triple vr1-dc0)"; mkmaas "$DVT"
cp "$REPO/overlays/vr1-dc0-vips.yaml" "$DVT/overlays/vr1-dc0-vips.yaml"
sed -i 's/vip: "10\.12\.8\.61 10\.12\.12\.61"/vip: "10.12.4.61 10.12.8.61 10.12.12.61"/' "$DVT/overlays/vr1-dc0-vips.yaml"
run "$DVT" vr1-dc0 1 'vault NOT the metal PAIR' "T28b vault as a provider/admin/internal triple FAILS CHECK 1 (metal-only)"

echo "== an octet in NO ruled D-134 band REFUSES, once =="
# The classification pass runs ONCE and stores its verdict, because CHECK 2 and CHECK 4
# both need it: deriving it per-check emitted the refusal twice and then printed a
# "utility-band" note contradicting the refusal that had just fired.
DU="$(mkfix_dc unclassified vr1-dc1)"; mkmaas "$DU"
sed -i 's/\[vr1-dc1-compute-02\]=121/[vr1-dc1-compute-02]=60/' "$DU/scripts/lib-hosts.sh"
run "$DU" vr1-dc1 1 'octet .60 is in NO ruled D-134 band' "T29 an out-of-band octet REFUSES"
NREF="$(PATH="$DU/fakebin:$PATH" DC=vr1-dc1 bash "$DU/scripts/pre-flight-checks.sh" 2>&1 \
        | grep -c 'is in NO ruled D-134 band')"
if [ "$NREF" = "1" ]; then ok "T30 ... exactly once (classified once, not per check)"
else bad "T30 the out-of-band refusal fired $NREF time(s), want 1"; fi
if PATH="$DU/fakebin:$PATH" DC=vr1-dc1 bash "$DU/scripts/pre-flight-checks.sh" 2>&1 \
   | grep -q 'vr1-dc1-compute-02 (.*utility-band infrastructure'; then
  bad "T31 a refused node must not also be reported as utility-band"
else
  ok "T31 a refused node is not also reported as utility-band"
fi

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