#!/usr/bin/env python3
"""
Behavioral tests for netbox/dc-rack-mgmt-import.py -- drives the REAL main()/preflight
against an in-memory fake NB (tests/dc-rack-mgmt-import/fake_netbox.py) injected at the
get_nb() seam. No live NetBox. Invoked by run-tests.sh alongside the static greps.

The properties pinned here map to this repo's documented IPAM bug classes + the D-124
scheme (Scheme A -- transit-numbered mesh):
  * HALF-WRITE (roles-aggregates-import.py): a bad rack IP (the .1 gateway) must abort
    with ZERO creates, never "wrote the transit prefix then died on the rack IP".
  * ROLE/SCOPE binding: the transit prefix carries the `transit` role id and is
    dcim.site-scoped to vr1-dc0; the rack IP carries NO ipam role (NetBox ip-address
    `role` is a choice field) -- identity is in dns_name/description.
  * BAND arithmetic: rack IP within metal-admin 10.12.8.0/22, D-120 static band .2-.49,
    NOT the .1 gateway, NOT .0.
  * TRANSIT shape: /30 or /31, host-bits-clear, subnet_of the transit container.
  * ALREADY-PRESENT = SKIP (idempotent), distinct from missing-infra = die.
  * DRY BY DEFAULT: no --commit writes NOTHING.
  * UPSTREAM-WRITE guard: --commit to a non-sandbox host refuses without the flag.
  * NO INVENTED LITERAL: missing --transit-cidr / --rack-ip fails loud.
"""
from __future__ import annotations

import contextlib
import faulthandler
import importlib.util
import inspect
import io
import os
import sys

faulthandler.dump_traceback_later(30, exit=True)

HERE = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.dirname(os.path.dirname(HERE))
TARGET_PATH = os.path.join(REPO_ROOT, "netbox", "dc-rack-mgmt-import.py")
sys.path.insert(0, HERE)

import fake_netbox  # noqa: E402

P = 0
F = 0


def ok(label):
    global P
    P += 1
    print(f"PASS: {label}")


def no(label, detail=""):
    global F
    F += 1
    print(f"FAIL: {label}" + (f" ({detail})" if detail else ""))


def check(cond, label, detail=""):
    ok(label) if cond else no(label, detail)


def load_target():
    spec = importlib.util.spec_from_file_location("dc_rack_mgmt_import", TARGET_PATH)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


T = load_target()


@contextlib.contextmanager
def captured_stdout():
    buf = io.StringIO()
    with contextlib.redirect_stdout(buf):
        yield buf


# The operator-supplied, NetBox-assigned example values under test. Chosen INSIDE the
# scheme: transit /30 subnet_of the dedicated transit supernet 172.31.0.0/24; rack IP in the .2-.49 static band.
TRANSIT = "172.31.0.0/30"
TRANSIT31 = "172.31.0.0/31"
RACK = "10.12.8.5"
RACK_ADDR = "10.12.8.5/22"   # how the tool stores it (host + metal-admin /22 mask)


def run_main(argv, fake):
    T.get_nb = lambda base=None, token=None: fake
    old = sys.argv
    sys.argv = ["dc-rack-mgmt-import.py"] + argv
    try:
        return T.main()
    finally:
        sys.argv = old


def run_dies(label, argv, fake):
    # swallow stdout AND stderr -- die() prints "FAIL: ..." to stderr by design, and
    # for an EXPECTED death that is not a test failure, just noise on the gauntlet.
    try:
        with contextlib.redirect_stderr(io.StringIO()), captured_stdout():
            run_main(argv, fake)
        no(label, "did not raise SystemExit")
    except SystemExit:
        ok(label)


# Full-precondition fixture builders (transit role + transit container + DC sites).
ROLE = {"slug": "transit", "name": "Transit", "id": 7}
CONTAINER = {"prefix": "172.31.0.0/24", "id": 10}
SITE_DC0 = {"slug": "vr1-dc0", "name": "VR1 DC0", "id": 20}
SITE_DC1 = {"slug": "vr1-dc1", "name": "VR1 DC1", "id": 21}

# dc1 ratified values (D-124 AMENDMENT 2026-07-21): transit 172.31.0.4/30 (next /30 in
# the container), rack IP in metal-admin 10.12.68.0/22's D-120 static band.
TRANSIT_DC1 = "172.31.0.4/30"
RACK_DC1 = "10.12.68.2"
RACK_DC1_ADDR = "10.12.68.2/22"


def full_fake(prefixes_extra=(), ip_extra=()):
    return fake_netbox.FakeNB(
        roles=[ROLE],
        prefixes=[CONTAINER, *prefixes_extra],
        sites=[SITE_DC0, SITE_DC1],
        ip_addresses=list(ip_extra),
    )


def base_args(extra=()):
    return ["--site", "vr1-dc0", "--transit-cidr", TRANSIT, "--rack-ip", RACK, *extra]


os.environ["NETBOX_URL"] = "http://10.10.1.10:8000"   # a known sandbox (guard passes)
os.environ["NETBOX_TOKEN"] = "fake-token"

# -----------------------------------------------------------------------------
# 1. DRY RUN is the default and writes NOTHING.
# -----------------------------------------------------------------------------
fk = full_fake()
with captured_stdout() as out:
    rc = run_main(base_args(), fk)
check(rc == 0, "dry run rc==0", str(rc))
check(out.getvalue().count("would CREATE") == 2, "dry run PLANS both objects",
      str(out.getvalue().count("would CREATE")))
check(out.getvalue().count("CREATED ") == 0, "dry run CREATES nothing (no CREATED line)")
check("DRY RUN" in out.getvalue(), "dry run says so, loudly")
check(len(fk.creates) == 0, "dry run left the NetBox untouched (0 writes)", str(len(fk.creates)))

# -----------------------------------------------------------------------------
# 2. --commit writes exactly the transit prefix + the rack IP, correctly bound.
# -----------------------------------------------------------------------------
fk = full_fake()
with captured_stdout():
    rc = run_main(base_args(["--commit"]), fk)
check(rc == 0, "commit rc==0", str(rc))
check(len(fk.creates) == 2, "commit created exactly 2 objects", str(len(fk.creates)))
paths = [p for p, _ in fk.creates]
check(paths.count("ipam/prefixes") == 1, "commit wrote exactly one ipam/prefixes")
check(paths.count("ipam/ip-addresses") == 1, "commit wrote exactly one ipam/ip-addresses")
check("ipam/roles" not in paths and "dcim/sites" not in paths,
      "commit created NO role / site (those are preconditions)")
pfx = next(pl for pp, pl in fk.creates if pp == "ipam/prefixes")
ipa = next(pl for pp, pl in fk.creates if pp == "ipam/ip-addresses")
check(pfx.get("prefix") == TRANSIT, "transit prefix is the supplied CIDR", str(pfx.get("prefix")))
check(pfx.get("role") == 7, "transit prefix carries the transit role id (7)", str(pfx.get("role")))
check(pfx.get("scope_type") == "dcim.site", "transit prefix is dcim.site-scoped",
      str(pfx.get("scope_type")))
check(pfx.get("scope_id") == 20, "transit prefix binds vr1-dc0's site id (20)", str(pfx.get("scope_id")))
check(pfx.get("status") == "active", "transit prefix is status active")
check(ipa.get("address") == RACK_ADDR, "rack IP stored with the /22 metal-admin mask",
      str(ipa.get("address")))
check("role" not in ipa, "rack IP carries NO ipam role (NetBox ip role is a choice field)")
check(ipa.get("dns_name") == "vvr1-dc0", "rack IP dns_name identifies the rack controller vvr1-dc0",
      str(ipa.get("dns_name")))
check("rack controller" in ipa.get("description", "").lower(),
      "rack IP description notes it is the vr1-dc0 MAAS rack controller")
check(ipa.get("status") == "active", "rack IP is status active")

# -----------------------------------------------------------------------------
# 3. A /31 transit is also accepted (point-to-point).
# -----------------------------------------------------------------------------
fk = full_fake()
with captured_stdout():
    rc = run_main(["--site", "vr1-dc0", "--transit-cidr", TRANSIT31, "--rack-ip", RACK, "--commit"], fk)
check(rc == 0 and len(fk.creates) == 2, "a /31 transit is accepted and writes both objects",
      str((rc, len(fk.creates))))

# -----------------------------------------------------------------------------
# 4. Already-present = idempotent SKIP (both present -> 0 creates, exit clean).
# -----------------------------------------------------------------------------
fk = full_fake(prefixes_extra=[{"prefix": TRANSIT, "id": 30}],
               ip_extra=[{"address": RACK_ADDR, "id": 31}])
with captured_stdout() as out:
    rc = run_main(base_args(["--commit"]), fk)
check(rc == 0, "both-present idempotent run rc==0")
check(out.getvalue().count("EXISTS") == 2, "both-present run reports both as EXISTS")
check(len(fk.creates) == 0, "both-present run creates nothing new")

# -----------------------------------------------------------------------------
# 5. Partial-present: transit already there, rack IP absent -> creates ONLY the rack IP.
# -----------------------------------------------------------------------------
fk = full_fake(prefixes_extra=[{"prefix": TRANSIT, "id": 30}])
with captured_stdout():
    rc = run_main(base_args(["--commit"]), fk)
check(rc == 0 and len(fk.creates) == 1 and fk.creates[0][0] == "ipam/ip-addresses",
      "transit-present run creates ONLY the missing rack IP",
      str([p for p, _ in fk.creates]))

# 5b. Partial-present the other way: rack IP present, transit absent -> creates ONLY transit.
fk = full_fake(ip_extra=[{"address": RACK_ADDR, "id": 31}])
with captured_stdout():
    rc = run_main(base_args(["--commit"]), fk)
check(rc == 0 and len(fk.creates) == 1 and fk.creates[0][0] == "ipam/prefixes",
      "rack-IP-present run creates ONLY the missing transit prefix",
      str([p for p, _ in fk.creates]))

# -----------------------------------------------------------------------------
# 6. HALF-WRITE GUARD -- a bad rack IP (the .1 gateway) aborts with ZERO creates,
#    even though the transit prefix itself is perfectly valid. The transit must NOT
#    be written before the rack-IP check fails.
# -----------------------------------------------------------------------------
fk = full_fake()
run_dies("half-write: rack IP == .1 gateway is REJECTED before any write",
         ["--site", "vr1-dc0", "--transit-cidr", TRANSIT, "--rack-ip", "10.12.8.1", "--commit"], fk)
check(len(fk.creates) == 0, "half-write: the rejected run wrote NOTHING (transit not created)")

# -----------------------------------------------------------------------------
# 7. Missing infra = die (each precondition), each writing nothing.
# -----------------------------------------------------------------------------
fk = fake_netbox.FakeNB(roles=[], prefixes=[CONTAINER], sites=[SITE_DC0])
run_dies("missing transit role is REJECTED", base_args(["--commit"]), fk)
check(len(fk.creates) == 0, "missing-role run wrote nothing")

fk = fake_netbox.FakeNB(roles=[ROLE], prefixes=[], sites=[SITE_DC0])
run_dies("missing dedicated transit supernet 172.31.0.0/24 is REJECTED", base_args(["--commit"]), fk)
check(len(fk.creates) == 0, "missing-container run wrote nothing")

fk = fake_netbox.FakeNB(roles=[ROLE], prefixes=[CONTAINER], sites=[])
run_dies("missing vr1-dc0 site is REJECTED", base_args(["--commit"]), fk)
check(len(fk.creates) == 0, "missing-site run wrote nothing")

# -----------------------------------------------------------------------------
# 8. Transit CIDR shape / placement rejects.
# -----------------------------------------------------------------------------
run_dies("a transit OUTSIDE the transit container is REJECTED",
         ["--site", "vr1-dc0", "--transit-cidr", "192.168.0.0/30", "--rack-ip", RACK, "--commit"], full_fake())
run_dies("a /29 transit is REJECTED (not point-to-point)",
         ["--site", "vr1-dc0", "--transit-cidr", "172.31.0.0/29", "--rack-ip", RACK, "--commit"], full_fake())
run_dies("a /32 transit is REJECTED",
         ["--site", "vr1-dc0", "--transit-cidr", "172.31.0.1/32", "--rack-ip", RACK, "--commit"], full_fake())
run_dies("a host-bits-set transit (172.31.0.1/30) is REJECTED",
         ["--site", "vr1-dc0", "--transit-cidr", "172.31.0.1/30", "--rack-ip", RACK, "--commit"], full_fake())
run_dies("a non-CIDR transit is REJECTED",
         ["--site", "vr1-dc0", "--transit-cidr", "not-a-cidr", "--rack-ip", RACK, "--commit"], full_fake())
# each of the above must have written nothing -- prove it once with a fresh fake
_fk = full_fake()
run_dies("bad transit writes nothing (proof)",
         ["--site", "vr1-dc0", "--transit-cidr", "172.31.0.0/29", "--rack-ip", RACK, "--commit"], _fk)
check(len(_fk.creates) == 0, "the rejected bad-transit run wrote NOTHING")

# -----------------------------------------------------------------------------
# 9. Rack-IP band rejects.
# -----------------------------------------------------------------------------
run_dies("rack IP OUTSIDE metal-admin 10.12.8.0/22 is REJECTED",
         ["--site", "vr1-dc0", "--transit-cidr", TRANSIT, "--rack-ip", "10.13.0.5", "--commit"], full_fake())
run_dies("rack IP == .1 gateway is REJECTED",
         ["--site", "vr1-dc0", "--transit-cidr", TRANSIT, "--rack-ip", "10.12.8.1", "--commit"], full_fake())
run_dies("rack IP == .0 network address is REJECTED",
         ["--site", "vr1-dc0", "--transit-cidr", TRANSIT, "--rack-ip", "10.12.8.0", "--commit"], full_fake())
run_dies("rack IP in-/22-but-above-static-band (.50) is REJECTED",
         ["--site", "vr1-dc0", "--transit-cidr", TRANSIT, "--rack-ip", "10.12.8.50", "--commit"], full_fake())
run_dies("rack IP in-/22-but-in-a-higher-/24 (10.12.9.5) is REJECTED",
         ["--site", "vr1-dc0", "--transit-cidr", TRANSIT, "--rack-ip", "10.12.9.5", "--commit"], full_fake())
run_dies("rack IP with a non-/22 mask is REJECTED",
         ["--site", "vr1-dc0", "--transit-cidr", TRANSIT, "--rack-ip", "10.12.8.5/24", "--commit"], full_fake())
# accepted: the boundaries of the static band (.2 low, .49 high) both write.
for edge_ip in ("10.12.8.2", "10.12.8.49"):
    fk = full_fake()
    with captured_stdout():
        rc = run_main(["--site", "vr1-dc0", "--transit-cidr", TRANSIT, "--rack-ip", edge_ip, "--commit"], fk)
    check(rc == 0 and len(fk.creates) == 2, f"rack IP band edge {edge_ip} is ACCEPTED",
          str((rc, len(fk.creates))))

# -----------------------------------------------------------------------------
# 10. NO INVENTED LITERAL -- missing input fails loud (does not guess a value).
# -----------------------------------------------------------------------------
run_dies("missing --transit-cidr fails loud", ["--site", "vr1-dc0", "--rack-ip", RACK], full_fake())
run_dies("missing --rack-ip fails loud", ["--site", "vr1-dc0", "--transit-cidr", TRANSIT], full_fake())
run_dies("missing --site fails loud (target DC is never inferred)",
         ["--transit-cidr", TRANSIT, "--rack-ip", RACK], full_fake())

# env fallback works (args-or-env): all three via env, no flags.
os.environ["RACK_SITE"] = "vr1-dc0"
os.environ["TRANSIT_CIDR"] = TRANSIT
os.environ["RACK_IP"] = RACK
fk = full_fake()
with captured_stdout():
    rc = run_main(["--commit"], fk)
check(rc == 0 and len(fk.creates) == 2, "env RACK_SITE/TRANSIT_CIDR/RACK_IP are honored (args-or-env)",
      str((rc, len(fk.creates))))
del os.environ["TRANSIT_CIDR"]
del os.environ["RACK_IP"]

# a BOGUS env site dies (argparse choices does not validate env-supplied defaults).
os.environ["RACK_SITE"] = "vr9-dc9"
run_dies("bogus RACK_SITE env value fails loud (choices bypass covered)",
         ["--transit-cidr", TRANSIT, "--rack-ip", RACK], full_fake())
del os.environ["RACK_SITE"]

# -----------------------------------------------------------------------------
# 11. Missing NETBOX_URL/TOKEN fails loud (does not guess a target).
# -----------------------------------------------------------------------------
_u = os.environ.pop("NETBOX_URL", None)
run_dies("missing NETBOX_URL fails loud", base_args(), full_fake())
os.environ["NETBOX_URL"] = _u

# -----------------------------------------------------------------------------
# 12. UPSTREAM-WRITE guard: --commit to a non-sandbox host refuses without the flag,
#     proceeds with it. Sandbox needs no flag (covered above).
# -----------------------------------------------------------------------------
os.environ["NETBOX_URL"] = "https://netbox.baldurkeep.com"   # the production apex
fk = full_fake()
run_dies("upstream --commit WITHOUT --yes-write-upstream is REFUSED", base_args(["--commit"]), fk)
check(len(fk.creates) == 0, "the refused upstream --commit wrote NOTHING")

fk = full_fake()
with captured_stdout():
    rc = run_main(base_args(["--commit", "--yes-write-upstream"]), fk)
check(rc == 0 and len(fk.creates) == 2,
      "upstream --commit --yes-write-upstream IS allowed and writes both objects",
      str((rc, len(fk.creates))))
# a DRY upstream run needs no flag and writes nothing (the guard is a --commit gate).
fk = full_fake()
with captured_stdout():
    rc = run_main(base_args(), fk)
check(rc == 0 and len(fk.creates) == 0, "a DRY upstream run is fine and writes nothing")
os.environ["NETBOX_URL"] = "http://10.10.1.10:8000"

# -----------------------------------------------------------------------------
# 13. Per-site behavior: --site vr1-dc1 (D-124 AMENDMENT 2026-07-21).
# -----------------------------------------------------------------------------
# dc1 happy path: binds vr1-dc1's site id, dc1 metal-admin mask, dns vvr1-dc1.
fk = full_fake()
with captured_stdout():
    rc = run_main(["--site", "vr1-dc1", "--transit-cidr", TRANSIT_DC1,
                   "--rack-ip", RACK_DC1, "--commit"], fk)
check(rc == 0 and len(fk.creates) == 2, "dc1 commit writes both objects",
      str((rc, len(fk.creates))))
pfx1 = next(pl for pp, pl in fk.creates if pp == "ipam/prefixes")
ipa1 = next(pl for pp, pl in fk.creates if pp == "ipam/ip-addresses")
check(pfx1.get("prefix") == TRANSIT_DC1, "dc1 transit prefix is 172.31.0.4/30",
      str(pfx1.get("prefix")))
check(pfx1.get("scope_id") == 21, "dc1 transit prefix binds vr1-dc1's site id (21), NOT dc0's",
      str(pfx1.get("scope_id")))
check(ipa1.get("address") == RACK_DC1_ADDR, "dc1 rack IP stored with the 10.12.68.0/22 mask",
      str(ipa1.get("address")))
check(ipa1.get("dns_name") == "vvr1-dc1", "dc1 rack IP dns_name is vvr1-dc1",
      str(ipa1.get("dns_name")))
check("vr1-dc1" in ipa1.get("description", ""), "dc1 rack IP description names vr1-dc1")

# CROSS-SITE GUARD: dc0's rack IP under --site vr1-dc1 is outside dc1's metal-admin -> die.
fk = full_fake()
run_dies("dc0 rack IP under --site vr1-dc1 is REJECTED (band is per-site)",
         ["--site", "vr1-dc1", "--transit-cidr", TRANSIT_DC1, "--rack-ip", RACK, "--commit"], fk)
check(len(fk.creates) == 0, "the cross-site rejected run wrote NOTHING")
# and dc1's band edges hold on the dc1 /22.
for edge_ip in ("10.12.68.2", "10.12.68.49"):
    fk = full_fake()
    with captured_stdout():
        rc = run_main(["--site", "vr1-dc1", "--transit-cidr", TRANSIT_DC1,
                       "--rack-ip", edge_ip, "--commit"], fk)
    check(rc == 0 and len(fk.creates) == 2, f"dc1 rack IP band edge {edge_ip} is ACCEPTED",
          str((rc, len(fk.creates))))
run_dies("dc1 rack IP .1 gateway (10.12.68.1) is REJECTED",
         ["--site", "vr1-dc1", "--transit-cidr", TRANSIT_DC1, "--rack-ip", "10.12.68.1",
          "--commit"], full_fake())
# a dc1 run against a fake WITHOUT the vr1-dc1 site dies (site precondition is per-site).
fk = fake_netbox.FakeNB(roles=[ROLE], prefixes=[CONTAINER], sites=[SITE_DC0])
run_dies("missing vr1-dc1 site is REJECTED for --site vr1-dc1",
         ["--site", "vr1-dc1", "--transit-cidr", TRANSIT_DC1, "--rack-ip", RACK_DC1,
          "--commit"], fk)
check(len(fk.creates) == 0, "missing-dc1-site run wrote nothing")

# -----------------------------------------------------------------------------
# 14. Structural pins (a changed constant a behavioral test alone would miss).
# -----------------------------------------------------------------------------
check(T.ROLE_SLUG == "transit", "ROLE_SLUG is 'transit'")
check(T.CONTAINER == "172.31.0.0/24", "CONTAINER is 172.31.0.0/24 (dedicated transit supernet)")
check(set(T.SITES) == {"vr1-dc0", "vr1-dc1"}, "SITES covers exactly vr1-dc0 + vr1-dc1")
check(T.SITES["vr1-dc0"]["metal_admin"] == "10.12.8.0/22", "dc0 metal-admin is 10.12.8.0/22")
check(T.SITES["vr1-dc1"]["metal_admin"] == "10.12.68.0/22",
      "dc1 metal-admin is 10.12.68.0/22 (D-124 amendment 2026-07-21)")
check(T.STATIC_BAND_LOW == 2 and T.STATIC_BAND_HIGH == 49,
      "static band is .2-.49 (D-120)")
check(T.STATUS == "active", "STATUS is active")
check(T.SITES["vr1-dc0"]["rack_dns"] == "vvr1-dc0", "dc0 rack dns is vvr1-dc0")
check(T.SITES["vr1-dc1"]["rack_dns"] == "vvr1-dc1", "dc1 rack dns is vvr1-dc1")
check(T.SANDBOX_HOSTS == {"localhost", "127.0.0.1", "10.10.1.10"},
      "SANDBOX_HOSTS matches the sibling importers")
check("get_nb" in dir(T) and callable(T.get_nb), "get_nb() injection seam exists")
_src = inspect.getsource(T)
check('User-Agent"' in _src and "curl/8.5.0" in _src, "UA-aware (WAF-safe User-Agent set)")

print()
if F == 0:
    print(f"test_logic: ALL PASS ({P} checks)")
    sys.exit(0)
print(f"test_logic: {F} FAIL of {P + F}")
sys.exit(1)
