"""
test_s11_band3.py — deterministic RED/GREEN regression gate for the S11 band-3 detectors (deps + headers).

Hermetic: the deps leg reads the PINNED captured audit JSON (no network); the headers leg reads the config cells.
A cell's canonical_symbol MUST appear in a RED finding and MUST NOT appear in any GREEN finding (RED-on-vuln /
GREEN-on-safe — the same discipline as the LLM corpus, applied to band-3).
"""
import importlib.util, json, os

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DET = os.path.join(ROOT, "domains", "security", "detectors")


def _load(path, name):
    spec = importlib.util.spec_from_file_location(name, path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def _canonical(leg):
    with open(os.path.join(DET, leg, "cells", "canonical.json")) as f:
        return json.load(f)


def test_deps_red_flags_canonical_green_silent():
    mod = _load(os.path.join(DET, "deps", "deps_audit.py"), "deps_audit")
    c = _canonical("deps")
    sym = c["canonical_symbol"].lower()
    cells = os.path.join(DET, "deps", "cells")
    red = mod.audit_findings(json.load(open(os.path.join(cells, "audit_vuln.json"))), "high")
    green = mod.audit_findings(json.load(open(os.path.join(cells, "audit_safe.json"))), "high")
    assert any(sym in f.lower() for f in red), f"RED must flag canonical {sym!r}; got {red}"
    assert not any(sym in f.lower() for f in green), f"GREEN must be silent on {sym!r}; got {green}"


def test_headers_red_flags_canonical_green_silent():
    mod = _load(os.path.join(DET, "headers", "headers_scan.py"), "headers_scan")
    c = _canonical("headers")
    sym = c["canonical_symbol"].lower()
    cells = os.path.join(DET, "headers", "cells")
    red = mod.missing_headers(open(os.path.join(cells, "headers_vuln.ts")).read())
    green = mod.missing_headers(open(os.path.join(cells, "headers_safe.ts")).read())
    assert any(sym in f.lower() for f in red), f"RED must flag canonical {sym!r}; got {red}"
    assert green == [], f"GREEN must be silent; got {green}"


def test_deps_severity_threshold_filters_below_high():
    mod = _load(os.path.join(DET, "deps", "deps_audit.py"), "deps_audit")
    audit = {"advisories": {"x": {"severity": "moderate", "module_name": "m", "title": "t",
                                  "vulnerable_versions": "*", "cwe": [], "cves": [], "url": ""}}}
    assert mod.audit_findings(audit, "high") == []          # moderate filtered at high
    assert mod.audit_findings(audit, "moderate") != []      # surfaced at moderate


def test_deps_unknown_or_missing_severity_always_surfaces():
    # no-false-clean: a null / new / garbage severity label must NEVER be dropped under any floor — it ranks
    # ABOVE critical so it clears even the strictest threshold (the reverse would be a silent false-clean).
    mod = _load(os.path.join(DET, "deps", "deps_audit.py"), "deps_audit")
    for sev in (None, "apocalyptic", ""):
        audit = {"advisories": {"x": {"severity": sev, "module_name": "m", "title": "t",
                                      "vulnerable_versions": "*", "cwe": [], "cves": [], "url": ""}}}
        assert mod.audit_findings(audit, "critical") != [], f"severity {sev!r} must surface at the strictest floor"


def test_headers_csp_frame_ancestors_satisfies_x_frame_options():
    mod = _load(os.path.join(DET, "headers", "headers_scan.py"), "headers_scan")
    # CSP with frame-ancestors and no explicit X-Frame-Options → X-Frame-Options must NOT be reported missing
    text = ('"Content-Security-Policy": "default-src \'self\'; frame-ancestors \'none\'",'
            '"X-Content-Type-Options":"nosniff","Strict-Transport-Security":"max-age=1",'
            '"Referrer-Policy":"no-referrer","Permissions-Policy":"geolocation=()"')
    missing = mod.missing_headers(text)
    assert not any("X-Frame-Options" in m for m in missing), f"frame-ancestors should satisfy X-Frame-Options; {missing}"
