#!/usr/bin/env bash
# tests/maas-role-tags/run-tests.sh
#
# Offline regression harness for scripts/maas-role-tags.sh. `maas` is STUBBED on PATH and
# driven by scenario files; no live MAAS, no writes anywhere outside $TMP.
#
# The script's job is to create the per-role tags the bundle constrains on. Its gate must
# therefore be provable in BOTH directions -- clean when everything is tagged, red when a
# tag or a node is missing -- and it must REFUSE rather than guess when its two role
# signals disagree. All three are exercised here.
#
# Exit: 0 all pass | 1 any case failed.  ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
S="$REPO/scripts/maas-role-tags.sh"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
ok()  { echo "  PASS  $1"; PASS=$((PASS+1)); }
bad() { echo "  FAIL  $1"; shift; [ $# -gt 0 ] && printf '        %s\n' "$@"; FAIL=$((FAIL+1)); }

mkdir -p "$TMP/bin"
cat > "$TMP/bin/maas" <<'EOF'
#!/usr/bin/env bash
# $1=profile $2=command ...
[ "${SC_UNREACHABLE:-0}" = "1" ] && exit 1
case "$2 $3" in
  "machines read")   cat "${SC_MACHINES:?}" ;;
  "tags read")       cat "${SC_TAGS:?}" ;;
  "tags create")     echo '{"name":"stub"}' ;;
  "tag update-nodes") echo '{}' ;;
  "machine read")    cat "${SC_MACHINES:?}" ;;
esac
exit 0
EOF
chmod +x "$TMP/bin/maas"

# dc0's nine pinned boot MACs, read from lib-hosts so the fixture cannot drift from it.
MACS="$(bash -c 'source "'"$REPO"'/scripts/lib-hosts.sh" >/dev/null 2>&1
  lib_hosts_select_dc vr1-dc0 >/dev/null 2>&1
  for h in "${HOSTS[@]}"; do printf "%s %s\n" "$h" "${HOST_BOOT_MAC[$h]}"; done')"
[ -n "$MACS" ] || { echo "  FAIL  fixture: lib-hosts yielded no dc0 hosts"; exit 1; }

mk_machines() { # mk_machines <out> <tagged: all|none>
  python3 - "$1" "$2" <<PY
import json, sys
rows = """$MACS""".strip().splitlines()
out = []
for i, r in enumerate(rows):
    host, mac = r.split()
    role = ("juju-controller-vr1-dc0" if "-juju-" in host
            else "control" if "-control-" in host
            else "compute" if "-compute-" in host else "storage")
    out.append({"system_id": "sid%02d" % i, "hostname": "random-%02d" % i,
                "boot_interface": {"mac_address": mac},
                "tag_names": ["openstack-vr1-dc0"] + ([role] if sys.argv[2] == "all" else [])})
json.dump(out, open(sys.argv[1], "w"))
PY
}
mk_tags() { printf '[%s]\n' "$(for t in "$@"; do printf '{"name":"%s"},' "$t"; done | sed 's/,$//')" ; }

mk_machines "$TMP/m-all.json"  all
mk_machines "$TMP/m-none.json" none
mk_tags control compute storage juju-controller-vr1-dc0 openstack-vr1-dc0 > "$TMP/t-all.json"
mk_tags openstack-vr1-dc0                          > "$TMP/t-none.json"

run() { # run <label> <want_rc> <regex> <action> [env...]
  local label="$1" want="$2" rx="$3" act="$4"; shift 4
  local out rc
  out="$(env PATH="$TMP/bin:$PATH" "$@" bash "$S" $act 2>&1)"; rc=$?
  if [ "$rc" = "$want" ] && printf '%s' "$out" | grep -qE "$rx"; then ok "$label"
  else bad "$label (rc=$rc want=$want)" "$(printf '%s' "$out" | tail -3)"; fi
}

echo "== maas-role-tags: offline harness =="

run "T1 fully tagged tree -> PASS" 0 'PASS: every ruled role tag exists' "check vr1-dc0" \
  SC_MACHINES="$TMP/m-all.json" SC_TAGS="$TMP/t-all.json"

# The state the chain audit MEASURED: role tags created nowhere.
run "T2 role tags absent -> FAIL, and says allocation would fail" 1 "tag 'control' does NOT exist" "check vr1-dc0" \
  SC_MACHINES="$TMP/m-none.json" SC_TAGS="$TMP/t-none.json"

run "T3 tags exist but nodes untagged -> FAIL" 1 '\[NEED\].*control' "check vr1-dc0" \
  SC_MACHINES="$TMP/m-none.json" SC_TAGS="$TMP/t-all.json"

run "T4 apply WITHOUT --commit writes nothing (dry by default)" 0 'DRY RUN -- would create' "apply vr1-dc0" \
  SC_MACHINES="$TMP/m-none.json" SC_TAGS="$TMP/t-none.json"

# "could not look" is never "nothing there", and an ABSENT client is not an unreachable
# service -- this deployment misdiagnosed that three times.
run "T5 unreachable MAAS REFUSES (rc=2), distinct from absent" 2 "machines read' failed" "check vr1-dc0" \
  SC_MACHINES="$TMP/m-all.json" SC_TAGS="$TMP/t-all.json" SC_UNREACHABLE=1

out="$(env PATH="/usr/bin:/bin" bash "$S" check vr1-dc0 2>&1)"; rc=$?
if [ "$rc" = 2 ] && printf '%s' "$out" | grep -q "ABSENT on"; then
  ok "T6 absent maas CLI REFUSES and says so, not 'MAAS unreachable'"
else bad "T6 absent-client refusal wrong (rc=$rc)"; fi

# A node whose pinned MAC has no MAAS record must be reported, and apply must refuse.
# Drop a ROLE node, not the last entry: since the D-134 amendment of 2026-07-29 the last
# lib-hosts entry is <dc>-juju-01, which sits in the UTILITY band and is correctly SKIPPED,
# so removing it produces no finding at all.
python3 - "$TMP/m-short.json" "$TMP/m-all.json" <<'PY'
import json, sys
m = json.load(open(sys.argv[2]))
json.dump([x for x in m if "juju" not in x["hostname"]][1:], open(sys.argv[1], "w"))
PY
run "T7 a pinned MAC missing from MAAS is reported" 1 '\[MISS\].*not found in MAAS' "check vr1-dc0" \
  SC_MACHINES="$TMP/m-short.json" SC_TAGS="$TMP/t-all.json"
run "T8 apply REFUSES while a node is unaccounted for" 2 'no MAAS record for their pinned MAC' "apply vr1-dc0" \
  SC_MACHINES="$TMP/m-short.json" SC_TAGS="$TMP/t-all.json"

# The two role signals must AGREE. Copy the tree and make lib-hosts disagree with the
# D-134 band, then require a refusal rather than a pick.
cp -r "$REPO/scripts" "$TMP/repo-scripts-parent-scripts" 2>/dev/null || true
mkdir -p "$TMP/fake/scripts"; cp "$REPO"/scripts/*.sh "$TMP/fake/scripts/" 2>/dev/null
sed -i 's/\[vr1-dc0-control-01\]=100/[vr1-dc0-control-01]=155/' "$TMP/fake/scripts/lib-hosts.sh"
out="$(env PATH="$TMP/bin:$PATH" SC_MACHINES="$TMP/m-all.json" SC_TAGS="$TMP/t-all.json" \
       bash "$TMP/fake/scripts/maas-role-tags.sh" check vr1-dc0 2>&1)"; rc=$?
if [ "$rc" = 2 ] && printf '%s' "$out" | grep -q 'band says storage but name says control'; then
  ok "T9 band/name disagreement REFUSES rather than picking one"
else bad "T9 derivation-conflict refusal wrong (rc=$rc)" "$(printf '%s' "$out" | tail -2)"; fi

# ---- D-134 AMENDMENT 2026-07-29: the utility band is per-DC INFRASTRUCTURE ----------
# <dc>-juju-01 sits at .5 and is NOT an OpenStack role node. It must be SKIPPED (reported,
# never silent) rather than refused -- but the refusal must keep its teeth for a genuine
# disagreement, which T11 proves.
# RE-POINTED 2026-07-29: juju-01 used to be SKIPPED as "not a role node". Since the script
# learned the D-104 controller tag it is TAGGED instead, with its own tag and NO role tag --
# which is the property that lets a constrained bootstrap target it deterministically.
run "T10 the controller carries juju-controller-<dc>, not a role tag" 0 "tag 'juju-controller-vr1-dc0' exists" "check vr1-dc0" \
  SC_MACHINES="$TMP/m-all.json" SC_TAGS="$TMP/t-all.json"
# ...and its ABSENCE is caught, so the new tag is gated like the three role tags.
run "T10b a missing controller tag is REPORTED" 1 "tag 'juju-controller-vr1-dc0' does NOT exist" "check vr1-dc0" \
  SC_MACHINES="$TMP/m-none.json" SC_TAGS="$TMP/t-none.json"

mkdir -p "$TMP/fake2/scripts"; cp "$REPO"/scripts/*.sh "$TMP/fake2/scripts/" 2>/dev/null
sed -i 's/\[vr1-dc0-storage-01\]=150/[vr1-dc0-storage-01]=7/' "$TMP/fake2/scripts/lib-hosts.sh"
out="$(env PATH="$TMP/bin:$PATH" SC_MACHINES="$TMP/m-all.json" SC_TAGS="$TMP/t-all.json" \
       bash "$TMP/fake2/scripts/maas-role-tags.sh" check vr1-dc0 2>&1)"; rc=$?
if [ "$rc" = 2 ] && printf '%s' "$out" | grep -q 'utility-band octet .7 but name says storage'; then
  ok "T11 a ROLE-named host in the utility band still REFUSES (teeth intact)"
else bad "T11 utility/role conflict not refused (rc=$rc)" "$(printf '%s' "$out" | tail -2)"; fi

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