"""test_recall_gate.py — deterministic conformance for the measured-recall RATE gate (#5).

No LLM: builds a synthetic corpus + detector prompt + records, then asserts recall_gate.run()'s
verdicts and exit code. Proves the load-bearing doctrine as a machine check — k>=3 is a HARD gate
(single-run claim forbidden), the prompt-hash binding reds STALE records, no-false-coverage flags
unknown/unmeasured cells, and the rate floor is advisory + per-cell waivable (never a default hard
red, because the project ships some cells below floor on no-regression). Path-load per convention."""
import hashlib
import importlib.util
import json
import os

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


rg = _load("sg_recall_gate", os.path.join(ROOT, "recall_gate.py"))

PROMPT_BODY = "you are a security reviewer. flag executing code only.\n"
PROMPT_HASH = hashlib.sha256(PROMPT_BODY.encode()).hexdigest()
STALE_HASH = "0" * 64


def _env(tmp, cells, records):
    """Build detectors/baseline + corpus cells + records.json; return (records, corpus, detectors)."""
    det = os.path.join(tmp, "detectors", "baseline")
    os.makedirs(det, exist_ok=True)
    with open(os.path.join(det, "detector.json"), "w", encoding="utf-8") as fh:
        json.dump({"id": "baseline", "prompt": "baseline.prompt.txt"}, fh)
    with open(os.path.join(det, "baseline.prompt.txt"), "w", encoding="utf-8") as fh:
        fh.write(PROMPT_BODY)

    corpus = os.path.join(tmp, "corpus")
    for cid, cls in cells.items():
        d = os.path.join(corpus, cid)
        os.makedirs(d, exist_ok=True)
        with open(os.path.join(d, "canonical.json"), "w", encoding="utf-8") as fh:
            json.dump({"id": cid, "class": cls}, fh)

    rp = os.path.join(tmp, "records.json")
    with open(rp, "w", encoding="utf-8") as fh:
        json.dump({"records": records}, fh)
    return rp, corpus, os.path.join(tmp, "detectors")


def _rec(cell, catches, k, **kw):
    r = {"cell_id": cell, "detector": "baseline", "catches": catches, "k": k,
         "metric": "per-roll-union", "measured_against": PROMPT_HASH}
    r.update(kw)
    return r


def _by_cell(res):
    return {v["cell_id"]: v for v in res["records"]}


def test_clean_k3_passes_and_is_rate_validated(tmp_path):
    rp, cr, dr = _env(str(tmp_path), {"S4-x": "S4"}, [_rec("S4-x", 3, 3)])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 0
    assert _by_cell(res)["S4-x"]["status"] == "OK"
    assert "S4-x" in res["rate_validated_cells"] and "S4" in res["rate_validated_classes"]
    assert res["unmeasured_cells"] == []


def test_single_run_k_below_3_is_hard_fail(tmp_path):
    rp, cr, dr = _env(str(tmp_path), {"S4-x": "S4"}, [_rec("S4-x", 1, 1)])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 1
    assert _by_cell(res)["S4-x"]["status"] == "SINGLE_RUN"
    assert "S4-x" not in res["rate_validated_cells"]


def test_stale_prompt_hash_is_hard_fail(tmp_path):
    rec = _rec("S4-x", 3, 3)
    rec["measured_against"] = STALE_HASH
    rp, cr, dr = _env(str(tmp_path), {"S4-x": "S4"}, [rec])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 1
    assert _by_cell(res)["S4-x"]["status"] == "STALE"
    assert "S4-x" not in res["rate_validated_cells"]


def test_unknown_cell_is_hard_fail(tmp_path):
    rp, cr, dr = _env(str(tmp_path), {"S4-x": "S4"}, [_rec("S9-ghost", 3, 3)])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 1
    assert _by_cell(res)["S9-ghost"]["status"] == "UNKNOWN_CELL"


def test_catches_gt_k_is_malformed(tmp_path):
    rp, cr, dr = _env(str(tmp_path), {"S4-x": "S4"}, [_rec("S4-x", 4, 3)])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 1
    assert _by_cell(res)["S4-x"]["status"] == "MALFORMED"


def test_missing_field_is_malformed(tmp_path):
    rp, cr, dr = _env(str(tmp_path), {"S4-x": "S4"}, [{"cell_id": "S4-x", "detector": "baseline", "k": 3}])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 1
    assert _by_cell(res)["S4-x"]["status"] == "MALFORMED"


def test_missing_metric_is_malformed(tmp_path):
    # an UNLABELED rate is the soft false-coverage the gate exists to prevent (a per-roll-union 1.000
    # and a max-single 0.11 are the same cell) -> `metric` is REQUIRED, absence is a hard MALFORMED.
    rec = _rec("S4-x", 3, 3)
    del rec["metric"]
    rp, cr, dr = _env(str(tmp_path), {"S4-x": "S4"}, [rec])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 1
    v = _by_cell(res)["S4-x"]
    assert v["status"] == "MALFORMED" and "metric" in v["detail"]


def test_unresolvable_detector_is_malformed(tmp_path):
    # the prompt-hash binding's failure mode: a record naming a detector with no resolvable prompt
    # cannot be hash-checked, so it is MALFORMED (never silently treated as hash-current).
    rp, cr, dr = _env(str(tmp_path), {"S4-x": "S4"}, [_rec("S4-x", 3, 3, detector="ghost")])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 1
    v = _by_cell(res)["S4-x"]
    assert v["status"] == "MALFORMED" and "unresolvable" in v["detail"]


def test_metric_is_carried_in_rate_validated_detail(tmp_path):
    # downstream (bench:72 reconcile) must receive the metric label, not just the cell id / rate.
    rp, cr, dr = _env(str(tmp_path), {"S4-x": "S4"}, [_rec("S4-x", 3, 3)])
    _, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    detail = {d["cell_id"]: d for d in res["rate_validated"]}
    assert detail["S4-x"]["metric"] == "per-roll-union"
    assert detail["S4-x"]["rate"] == 1.0


def test_below_floor_unwaived_is_report_only(tmp_path):
    # 3/9 = 0.33 < floor; NO waiver -> reported, NOT a hard red, but still statistically validated (k>=3)
    rp, cr, dr = _env(str(tmp_path), {"S2-flaky": "S2"}, [_rec("S2-flaky", 3, 9)])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 0
    assert _by_cell(res)["S2-flaky"]["status"] == "BELOW_FLOOR"
    assert "S2-flaky" in res["rate_validated_cells"]
    assert _by_cell(res)["S2-flaky"]["cell_id"] in [v["cell_id"] for v in res["below_floor"]]


def test_below_floor_waived_is_ok(tmp_path):
    rp, cr, dr = _env(str(tmp_path), {"S2-flaky": "S2"},
                      [_rec("S2-flaky", 3, 9, waiver={"below_floor_ok": True, "reason": "pre-existing flaky"})])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 0
    assert _by_cell(res)["S2-flaky"]["status"] == "OK_WAIVED"
    assert res["below_floor"] == []
    assert "S2-flaky" in res["rate_validated_cells"]


def test_strict_floor_makes_unwaived_below_floor_hard(tmp_path):
    rp, cr, dr = _env(str(tmp_path), {"S2-flaky": "S2"}, [_rec("S2-flaky", 3, 9)])
    code, _ = rg.run(rp, cr, dr, 0.66, strict_floor=True)
    assert code == 1


def test_unmeasured_corpus_cell_is_reported_not_failed(tmp_path):
    rp, cr, dr = _env(str(tmp_path), {"S4-x": "S4", "S12-finance": "S12"}, [_rec("S4-x", 3, 3)])
    code, res = rg.run(rp, cr, dr, 0.66, strict_floor=False)
    assert code == 0
    assert res["unmeasured_cells"] == ["S12-finance"]


def test_missing_records_file_is_hard_fail(tmp_path):
    _, cr, dr = _env(str(tmp_path), {"S4-x": "S4"}, [])
    code, res = rg.run(os.path.join(str(tmp_path), "nope.json"), cr, dr, 0.66, strict_floor=False)
    assert code == 1 and "error" in res
