"""
test_verdict.py — the unified credit/miss/punt authority (prevent/verdict.py, ARCHITECTURE §4 Verdict primitive).

Guards the contract that bench.py + orchestrator/detect.py both now consume:
  - POSITIVE polarity: right-reason -> CREDIT; autoscorable-but-not-flagged -> MISS; else -> PUNT.
  - hand-judge `why` routes to PUNT (never auto-credit), even with a matching symbol present.
  - NEGATIVE polarity (anti-canary / discriminator): a right-reason flag on SAFE code is a false raise -> MISS;
    staying quiet -> CREDIT.
  - the punt -> judge boundary: a wired judge resolves a PUNT (the autoscorer #61 drop-in point).
"""
import importlib.util, 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


V = _load("sg_verdict", os.path.join(ROOT, "prevent", "verdict.py"))


# --- POSITIVE polarity --------------------------------------------------------------------------------------

def test_code_identifier_contiguous_credit():
    exp = V.Expectation(symbol="assignee_id", polarity=V.POSITIVE)
    assert V.match(["missing assignee_id ownership check"], exp).outcome == V.CREDIT


def test_code_identifier_absent_no_autoscore_src_punts():
    # not flagged, no source to autoscore against -> hand-judge PUNT (mirrors bench cmd_findings: recall 0)
    exp = V.Expectation(symbol="assignee_id", polarity=V.POSITIVE)
    assert V.match(["unrelated finding"], exp).outcome == V.PUNT


def test_autoscorable_miss_is_real_recall_zero():
    # symbol IS contiguous in the source but no finding named it -> a REAL miss, scored, not punted
    exp = V.Expectation(symbol="assignee_id", polarity=V.POSITIVE,
                        autoscore_src="const x = body.assignee_id // no auth")
    v = V.match(["something else entirely"], exp)
    assert v.outcome == V.MISS


def test_phrase_symbol_needs_all_distinctive_tokens():
    exp = V.Expectation(symbol="unguarded ssrf fetch", polarity=V.POSITIVE)
    assert V.match(["unguarded ssrf fetch to internal metadata"], exp).outcome == V.CREDIT
    # one distinctive token missing -> not a right-reason match -> PUNT (no autoscore_src)
    assert V.match(["unguarded internal call"], exp).outcome == V.PUNT


def test_hand_judge_why_never_auto_credits():
    # `why` demands hand-judging: even with the symbol present in a finding, must NOT auto-credit
    exp = V.Expectation(symbol="privilege escalation", why="hand-judged: generic class tag",
                        polarity=V.POSITIVE)
    assert V.match(["privilege escalation via role bypass"], exp).outcome == V.PUNT


# --- NEGATIVE polarity (anti-canary / discriminator) --------------------------------------------------------

def test_negative_quiet_on_safe_is_credit():
    exp = V.Expectation(symbol="assignee_id", polarity=V.NEGATIVE)
    assert V.match(["a totally unrelated note"], exp).outcome == V.CREDIT


def test_negative_false_raise_on_safe_is_miss():
    exp = V.Expectation(symbol="assignee_id", polarity=V.NEGATIVE)
    assert V.match(["re-raised assignee_id on the fixed file"], exp).outcome == V.MISS


# --- punt -> judge boundary (autoscorer #61 drop-in) --------------------------------------------------------

def test_judge_resolves_punt():
    exp = V.Expectation(symbol="assignee_id", polarity=V.POSITIVE)  # would PUNT without a judge
    calls = []

    def judge(findings, expectation):
        calls.append((findings, expectation))
        return V.Verdict(V.CREDIT, "LLM judge credited")

    v = V.match(["unrelated"], exp, judge=judge)
    assert v.outcome == V.CREDIT and v.reason == "LLM judge credited"
    assert len(calls) == 1


def test_judge_not_called_when_right_reason_matches():
    exp = V.Expectation(symbol="assignee_id", polarity=V.POSITIVE)
    called = []
    V.match(["missing assignee_id check"], exp, judge=lambda f, e: called.append(1))
    assert not called  # CREDIT short-circuits before the judge


def test_judge_not_called_for_autoscorable_miss():
    # an autoscorable miss is a decided recall-0, not a punt -> judge must not be consulted
    exp = V.Expectation(symbol="assignee_id", polarity=V.POSITIVE, autoscore_src="x = assignee_id")
    called = []
    v = V.match(["unrelated"], exp, judge=lambda f, e: called.append(1))
    assert v.outcome == V.MISS and not called
