"""test_detect_bench.py — run_bench no-false-clean + teaching-to-test guard. detect_target stubbed (no gate/LLM).
- autoscore: literal-symbol cell whose titles MISS the canonical → scorable recall-0 (true miss), NOT incomplete.
- phrase cell (symbol absent from vuln source) whose titles MISS → coverage_incomplete['autoscore-unscorable'].
- routing-blindness: kind handed to detect_target comes from mapper.kind_of, NEVER from canonical['class']."""
import importlib.util, os, tempfile

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


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


detect = _load("sg_detect", os.path.join(ROOT, "orchestrator", "detect.py"))
BASELINE = {"id": "baseline", "kind": "llm", "applies_to": {"always": True}}


def _cell(cid, klass, symbol, vuln_src):
    d = tempfile.mkdtemp()
    open(os.path.join(d, "vuln.ts"), "w").write(vuln_src)
    return {"id": cid, "class": klass, "canonical_symbol": symbol, "_dir": d, "file": "apps/api/x.ts"}


def test_run_bench_baseline_absent_hard_stop():
    fo, meta = detect.run_bench([], [{"id": "auth", "kind": "llm"}], [], out_dir=tempfile.mkdtemp())
    assert meta["status"] == "COVERAGE-INCOMPLETE" and fo == {}


def test_autoscore_literal_symbol_miss_is_scorable_recall0(monkeypatch):
    # canonical symbol IS a literal token in the vuln source → autoscorable; titles miss → real recall-0
    cell = _cell("S3-x", "S3", "getSchedulingConnectionById",
                 "export function getSchedulingConnectionById(id){return db.x(id)}\n")
    monkeypatch.setattr(detect, "detect_target",
                        lambda t, k, d, *a, **kw: {"status": "clean", "findings": [{"title": "unrelated note"}]})
    fo, meta = detect.run_bench([cell], [BASELINE], [], out_dir=tempfile.mkdtemp())
    assert "S3-x" in fo                              # scorable: bench will score it recall-0 (true miss)
    assert "S3-x" not in meta["coverage_incomplete"]
    assert meta["recall"] == 0.0 and meta["scored"] == 1


def test_phrase_canonical_miss_is_unscorable_not_recall0(monkeypatch):
    # canonical symbol is a PHRASE not present in vuln source → not autoscorable; titles miss → unscorable
    cell = _cell("S5-y", "S5", "escape email interpolation",
                 "export function render(t){return `<b>${t}</b>`}\n")
    monkeypatch.setattr(detect, "detect_target",
                        lambda t, k, d, *a, **kw: {"status": "clean", "findings": [{"title": "unrelated note"}]})
    fo, meta = detect.run_bench([cell], [BASELINE], [], out_dir=tempfile.mkdtemp())
    assert "S5-y" not in fo                          # NOT fed to bench (would be a false recall-0)
    assert meta["coverage_incomplete"]["S5-y"] == "autoscore-unscorable"


def test_scan_degraded_cell_is_incomplete(monkeypatch):
    cell = _cell("S3-z", "S3", "touchSession", "export function touchSession(id){return db.u(id)}\n")
    monkeypatch.setattr(detect, "detect_target",
                        lambda t, k, d, *a, **kw: {"status": "degraded", "findings": []})
    fo, meta = detect.run_bench([cell], [BASELINE], [], out_dir=tempfile.mkdtemp())
    assert "S3-z" not in fo and meta["coverage_incomplete"]["S3-z"] == "scan-degraded"


def test_routing_kind_from_mapper_not_canonical_class(monkeypatch):
    cell = _cell("S1-r", "S1", "reset token single-use", "export function reset(){}\n")
    captured = {}
    monkeypatch.setattr(detect.mapper, "kind_of", lambda rel, body=None: "edge-function")
    def _spy(target, kind, dets, *a, **kw):
        captured["kind"] = kind
        return {"status": "clean", "findings": []}
    monkeypatch.setattr(detect, "detect_target", _spy)
    detect.run_bench([cell], [BASELINE], [], out_dir=tempfile.mkdtemp())
    assert captured["kind"] == "edge-function"       # mapper-derived
    assert captured["kind"] != cell["class"]         # NOT the canonical class (teaching-to-test guard)
