import json, subprocess, sys, os, shutil, tempfile
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

def _routing(detectors_root=None):
    cmd = [sys.executable, os.path.join(ROOT, "bench.py"), "--routing"]
    if detectors_root:
        cmd += ["--detectors-root", detectors_root]
    r = subprocess.run(cmd, capture_output=True, text=True)
    return r, json.loads(r.stdout)

def test_inventory_lists_every_corpus_cell():
    r = subprocess.run([sys.executable, os.path.join(ROOT, "bench.py"), "--inventory"],
                       capture_output=True, text=True)
    assert r.returncode == 0, r.stderr
    out = json.loads(r.stdout)
    ids = {c["id"] for c in out["cells"]}
    assert "S1-reset-token-reuse" in ids
    assert "S1-xfile-2fa-on-refresh" in ids
    # no-false-coverage-claim: the blind_spots mechanism must exist + be a list (it may legitimately be empty once
    # every class is measured — S11 left the list 2026-06-17 when band-3 deps+headers shipped).
    assert "blind_spots" in out and isinstance(out["blind_spots"], list)
    assert "S11" not in out["blind_spots"], "S11 is measured via band-3; must not be reported blind"
    # band-3 detector cells are now part of the single coverage ledger
    assert {"S11-deps-known-cve", "S11-headers-missing"} <= ids

def test_recall_marks_missing_canonical_as_regression():
    # a findings source that flags nothing must yield recall 0 and a non-zero exit (regression gate)
    empty = os.path.join(ROOT, "tests", "_empty_findings.json")
    with open(empty, "w") as f:
        json.dump({"S1-reset-token-reuse": []}, f)
    r = subprocess.run([sys.executable, os.path.join(ROOT, "bench.py"),
                        "--findings", empty, "--require", "S1-reset-token-reuse"],
                       capture_output=True, text=True)
    assert r.returncode != 0, "missing required canonical must fail the bench (regression gate)"
    os.remove(empty)

def test_routing_specialists_route_right_reason():
    # every cell with a specialist routes to THAT specialist (not baseline). hit ids must be the specialist.
    _, out = _routing()
    by_id = {r["id"]: r for r in out["rows"]}
    expect = {"S1-reset-token-reuse": "auth", "S2-role-privilege-escalation": "access-control",
              "S3-session-tenant-isolation-idor": "access-control", "S8-attachment-cross-tenant-idor": "access-control",
              "S9-xfile-c02-self-deal-commission": "oracle"}
    for cid, spec in expect.items():
        r = by_id[cid]
        assert r["status"] == "ROUTED", f"{cid} not routed: {r}"
        assert r["selected_intended"] == [spec], f"{cid} routed to {r['selected_intended']}, expected [{spec}] (NOT baseline)"
        assert "baseline" not in r["selected_intended"], "baseline is the floor, never the routed intended detector"

def test_routing_baseline_is_green_with_headers_xfailed():
    # build-order step 0: every per-file-routable cell routes (recall 1.0), gate exits 0. The dead headers cell
    # is an accepted XFAIL (cell-flagged routing_xfail), NOT a MISS — reported, non-fatal (no-false-coverage).
    r, out = _routing()
    assert r.returncode == 0, f"baseline routing must be green; misses={out['misses']}"
    assert out["routing_recall"] == 1.0, out
    assert not out["misses"], out["misses"]
    assert [x["id"] for x in out["xfail"]] == ["S11-headers-missing"], out["xfail"]

def test_routing_blind_spots_are_the_no_specialist_classes():
    # no-false-coverage: S4/S5/S6/S7/S10/S12/S13 have NO specialist (baseline-floor only) -> BLIND_SPOT, excluded from rate.
    _, out = _routing()
    blind = {b["class"] for b in out["blind_spots"]}
    assert blind == {"S4", "S5", "S6", "S7", "S10", "S12", "S13"}, f"blind-spot classes drifted: {blind}"

def test_routing_excludes_baseline_DISCRIMINATOR():
    # THE load-bearing gate property: keep a specialist registered but make its applies_to never match -> its class
    # MUST go MISS. If it stayed ROUTED the gate would be measuring baseline (the floor), not routing (the ledger
    # false-green failure mode, re-instantiated). Mirrors the empirical neuter-auth check.
    tmp = tempfile.mkdtemp(prefix="sg_routing_")
    try:
        dst = os.path.join(tmp, "domains", "security", "detectors")
        shutil.copytree(os.path.join(ROOT, "domains", "security", "detectors"), dst)
        ap = os.path.join(dst, "auth", "detector.json")
        d = json.load(open(ap))
        d["applies_to"] = {"kinds": [], "signal": "(?!x)x", "always": False}  # registered, never matches
        json.dump(d, open(ap, "w"))
        r, out = _routing(detectors_root=tmp)
        s1 = [row for row in out["rows"] if row["class"] == "S1"]
        assert s1 and all(row["status"] == "MISS" for row in s1), \
            f"neutered specialist must drive its class to MISS (baseline-isolated), got {[(x['id'], x['status']) for x in s1]}"
        assert r.returncode != 0, "a routing MISS must fail the gate (exit non-zero)"
    finally:
        shutil.rmtree(tmp, ignore_errors=True)
