"""Brief 11B-step-2 — pin the lexicon additions and verify wrong=0 + hazard.

Hard invariants:

* `wrong == 0` — no admissions, no risky frame-opener miscategorisation.
* The hazard canary case ``gsm8k-train-sample-v1-0050`` MUST remain refused
  at sentence_index 0 (pre-frame), the same site as before this PR.
* Every new lemma is classified as ``drain_token`` (the only category that
  cannot open a frame and therefore cannot create wrong admissions).
* The compiled lexicon manifest checksum is unchanged (additions live in
  the per-category source file; the compiled ``lexicon.jsonl`` is not
  regenerated by this PR — see ``generate/comprehension/lexicon.py`` for
  the load contract).
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from generate.comprehension import lexicon as comprehension_lexicon
from generate.comprehension.audit import audit_problem
from generate.comprehension.state import ReaderRefusal

REPO_ROOT = Path(__file__).resolve().parents[1]
PACK_DIR = REPO_ROOT / "language_packs/data/en_core_math_v1"
DRAIN_PATH = PACK_DIR / "lexicon/drain_token.jsonl"
CASES_PATH = REPO_ROOT / "evals/gsm8k_math/train_sample/v1/cases.jsonl"

NEW_LEMMAS: tuple[str, ...] = (
    "along",
    "animals",
    "decrease",
    "jacks",
    "length",
    "previous",
    "reach",
    "stray",
    "too",
    "uphill",
    "which",
    "your",
)
# Aliases added to existing lemmas in this PR (lemma → new alias)
NEW_ALIASES: dict[str, str] = {
    "weight": "weights",
}
PROVENANCE = "phase_2_reader_brief_11b_step2_2026-05-27"


@pytest.fixture(scope="module")
def drain_entries() -> list[dict]:
    return [
        json.loads(line)
        for line in DRAIN_PATH.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]


@pytest.fixture(scope="module")
def cases() -> list[dict]:
    with CASES_PATH.open() as f:
        return [json.loads(line) for line in f]


@pytest.fixture(scope="module", autouse=True)
def _clear_lex_cache():
    comprehension_lexicon._CACHE.clear()
    yield
    comprehension_lexicon._CACHE.clear()


# ---------------------------------------------------------------------------
# Pack-additions pinning
# ---------------------------------------------------------------------------


def test_every_new_lemma_is_drain_token(drain_entries):
    by_lemma = {e["lemma"]: e for e in drain_entries}
    for lemma in NEW_LEMMAS:
        assert lemma in by_lemma, f"missing new lemma {lemma!r}"
        assert by_lemma[lemma]["category"] == "drain_token"


def test_every_new_lemma_carries_brief_provenance(drain_entries):
    by_lemma = {e["lemma"]: e for e in drain_entries}
    for lemma in NEW_LEMMAS:
        assert by_lemma[lemma]["provenance"] == PROVENANCE, (
            f"{lemma!r} provenance mismatch (got {by_lemma[lemma]['provenance']!r})"
        )


def test_new_aliases_attached_to_existing_lemmas(drain_entries):
    by_lemma = {e["lemma"]: e for e in drain_entries}
    for lemma, alias in NEW_ALIASES.items():
        entry = by_lemma.get(lemma)
        if entry is None:
            # Some aliases attach to lemmas in OTHER per-category files; skip
            # silently if the lemma isn't in drain_token.
            continue
        assert alias in entry["aliases"], (
            f"{alias!r} not aliased on {lemma!r}: {entry['aliases']!r}"
        )


def test_drain_entries_alphabetically_sorted(drain_entries):
    lemmas = [e["lemma"] for e in drain_entries]
    assert lemmas == sorted(lemmas), "drain_token.jsonl not alphabetically sorted"


# ---------------------------------------------------------------------------
# Reader-level effect — wrong=0 + hazard canary
# ---------------------------------------------------------------------------


def test_no_case_admits_after_lexicon_closure(cases):
    """``wrong == 0`` is preserved: lexicon additions must not produce any
    admission. Drain_token cannot open a frame, so no PartialOperation /
    PartialInitialPossession can be contributed by these additions."""
    for c in cases:
        result, _ = audit_problem(c["question"], case_id=c["case_id"])
        if not (isinstance(result, ReaderRefusal) or result is None):
            pytest.fail(
                f"unexpected admission on {c['case_id']!r} — "
                f"lexicon closure must not lift cases without runtime work; "
                f"got graph entities={result.entities!r}"
            )


def test_hazard_case_0050_remains_refused_pre_frame(cases):
    """The documented wrong>0 hazard from Brief 11B audit_brief_11.md must
    remain refused. Any closure work that admits this case is rejected per
    Brief 11 §"correct-count greed."
    """
    case = next(c for c in cases if c["case_id"] == "gsm8k-train-sample-v1-0050")
    result, _ = audit_problem(case["question"], case_id=case["case_id"])
    assert isinstance(result, ReaderRefusal), (
        f"case 0050 admitted: {result!r} — this would create wrong>0"
    )
    # Refusal site must remain at sentence 0 (pre-frame filler), not slip
    # forward into the partial Operation(mark, add, 3, songs) zone.
    assert result.sentence_index == 0, (
        f"case 0050 refusal site moved to sentence {result.sentence_index} "
        f"(was 0). Verify no partial graph reaches finalize."
    )


def test_unknown_word_row_strictly_decreased(cases):
    """Brief 11B-step-2 exit condition: the unknown_word row in the audit
    taxonomy must strictly decrease (Brief 11 §Gate 1)."""
    from collections import Counter
    reasons = Counter()
    for c in cases:
        result, _ = audit_problem(c["question"], case_id=c["case_id"])
        if isinstance(result, ReaderRefusal):
            reasons[result.reason] += 1
    # Pre-step-2 baseline (pinned by test_brief_11b_audit_artifact): 11
    assert reasons["unknown_word"] < 11, (
        f"unknown_word row did not decrease: {reasons['unknown_word']} vs 11 baseline"
    )


def test_manifest_checksum_unchanged_by_source_edits():
    """The manifest checksum verifies the compiled ``lexicon.jsonl``; this PR
    edits only the per-category source files, so the compiled file (and
    therefore the checksum) is intentionally not regenerated."""
    manifest = json.loads((PACK_DIR / "manifest.json").read_text())
    compiled_path = PACK_DIR / "lexicon.jsonl"
    import hashlib
    actual = hashlib.sha256(compiled_path.read_bytes()).hexdigest()
    assert manifest["checksum"] == actual, (
        f"manifest checksum mismatch: declared={manifest['checksum']!r}, "
        f"actual={actual!r}. If you regenerated lexicon.jsonl, update manifest."
    )


# ---------------------------------------------------------------------------
# Loader integrity — additions are reachable through the live lexicon.
# ---------------------------------------------------------------------------


def test_new_lemmas_resolve_through_load_lexicon():
    lex = comprehension_lexicon.load_lexicon()
    for lemma in NEW_LEMMAS:
        entry = comprehension_lexicon.lookup(lex,lemma)
        assert entry is not None, f"{lemma!r} not found by lexicon loader"
        assert entry.category == "drain_token"


def test_new_aliases_resolve_through_load_lexicon():
    lex = comprehension_lexicon.load_lexicon()
    for lemma, alias in NEW_ALIASES.items():
        entry = comprehension_lexicon.lookup(lex,alias)
        if entry is None:
            continue  # alias may belong to a different category file
        assert entry.lemma == lemma, (
            f"alias {alias!r} resolved to {entry.lemma!r}, expected {lemma!r}"
        )
