"""CRT-03 doc↔schema gate: the findings grammar has exactly one definition.

``docs/standards/REVIEW.md`` states the court findings grammar twice by
necessity — once as a **MUST** in the CRT-03 Rules-box row (the binding text)
and once as the §3 field table (the readable form) — and
``templates/findings-schema.json`` is its machine form. Three copies of one
field list is the federation's most reliable drift shape (the same class
``check_headline_coherence.py`` and ``check_testing_doc.py`` exist to kill), so
it is gated here: the schema's required fields, its enums, and the
``CONFIRMED ⇒ VERIFIED_BY_EXECUTION`` coupling must all agree with the document.

Stdlib-only on purpose — ``mgf-standard`` is a zero-dependency leaf (see the
pyproject note), so this compares structure rather than running a JSON Schema
validator.

**TS-29 (added by adoption wave S5) — and this file is the rule's own worked
failure.** When wave S2 shipped this gate it *was* sabotage-proved three ways
(drop a required field / weaken the ``CONFIRMED`` coupling / delete a CRT
anchor), each red then restored green — and the proof was recorded **in the
commit body of ``2cc0622`` and nowhere else**. Every test in the file was a
positive assertion over the live tree, so nothing in the suite could re-run it,
and a refactor that dissolved the failure mode would have gone unnoticed. That
is failure mode (a) in TS-29's own words, produced by the initiative that wrote
the rule. The decision is now the callable ``check(review, schema)`` and the
three sabotages stand as fixtures in ``TestTheGateBites``.
"""

from __future__ import annotations

import copy
import json
import re
from pathlib import Path
from typing import Any

_ROOT = Path(__file__).resolve().parents[2]
_REVIEW = _ROOT / "docs" / "standards" / "REVIEW.md"
_SCHEMA = _ROOT / "templates" / "findings-schema.json"

# A §3 field-table row: "| `finding_id` | yes | ... |"
_FIELD_ROW = re.compile(r"^\|\s*`([a-z_]+)`\s*\|")


def live_schema() -> dict[str, Any]:
    return json.loads(_SCHEMA.read_text(encoding="utf-8"))


def live_review() -> str:
    return _REVIEW.read_text(encoding="utf-8")


# ---------------------------------------------------------------------------
# Readers — every one takes its input, so the gate can be fed a seeded pair.
# ---------------------------------------------------------------------------


def _finding_def(schema: dict[str, Any]) -> dict[str, Any]:
    finding = schema["$defs"]["finding"]
    assert isinstance(finding, dict)
    return finding


def _field_table_names(review: str) -> list[str]:
    """The field names in REVIEW.md §3's grammar table, in document order."""
    names: list[str] = []
    in_block = False
    for line in review.splitlines():
        if line.startswith("## 3. The findings grammar"):
            in_block = True
            continue
        if in_block and line.startswith("## "):
            break
        if in_block:
            m = _FIELD_ROW.match(line)
            if m:
                names.append(m.group(1))
    return names


def _rule_row_text(review: str, rule_id: str) -> str | None:
    """The Rules-box row for ``rule_id`` — the BINDING text of that MUST."""
    anchor = f'<a id="{rule_id}"></a>'
    for line in review.splitlines():
        if line.startswith("|") and anchor in line:
            return line
    return None


def _enum(schema: dict[str, Any], field: str) -> list[str]:
    values = _finding_def(schema)["properties"][field]["enum"]
    assert isinstance(values, list)
    return [str(v) for v in values]


# ---------------------------------------------------------------------------
# The decision, as a callable over (review text, schema) — TS-29 §20.3.
# ---------------------------------------------------------------------------


def check(review: str, schema: dict[str, Any]) -> list[str]:
    """Return every CRT-03 doc↔schema disagreement (empty == GREEN)."""
    errors: list[str] = []
    documented = _field_table_names(review)

    # The vacuity pin (TS-29). If §3's heading is renamed or its table
    # reshaped, `documented` goes empty and the CRT-03-row check below passes
    # over nothing at all.
    if not documented:
        return [
            "REVIEW.md: §3's grammar table parsed ZERO field rows — the '## 3. "
            "The findings grammar' heading or the '| `field` |' row shape "
            "changed, and the field checks below would pass vacuously."
        ]

    if schema.get("$schema") != "https://json-schema.org/draft/2020-12/schema":
        errors.append(f"findings-schema.json: not draft 2020-12 ({schema.get('$schema')!r}).")
    if not schema.get("title"):
        errors.append("findings-schema.json: carries no title.")

    required = _finding_def(schema)["required"]
    if sorted(required) != sorted(documented):
        errors.append(
            "CRT-03 drift: templates/findings-schema.json's required finding "
            f"fields {sorted(required)} != REVIEW.md §3's grammar table "
            f"{sorted(documented)} — update both in the same commit (DOC-04)."
        )

    crt03 = _rule_row_text(review, "CRT-03")
    if crt03 is None:
        errors.append("REVIEW.md: no CRT-03 Rules-box row found — the binding text is gone.")
    else:
        missing = [f for f in documented if f"`{f}`" not in crt03]
        if missing:
            errors.append(
                "CRT-03 drift: field(s) in REVIEW.md §3's table but absent from the "
                f"binding CRT-03 Rules-box row: {missing}. The Rules box is the MUST; "
                "§3 may not carry a field the rule does not."
            )
        for token in ("CONFIRMED", "PLAUSIBLE", "CRITICAL", "LOW"):
            if token not in crt03:
                errors.append(f"CRT-03 row does not name the value {token!r}.")
        if "CONFIRMED` MUST be backed by `evidence_level=VERIFIED_BY_EXECUTION" not in crt03:
            errors.append(
                "CRT-03 row no longer states the CONFIRMED ⇒ VERIFIED_BY_EXECUTION coupling."
            )

    if _enum(schema, "lane") != ["implementer", "founder", "court"]:
        errors.append(f"lane enum is {_enum(schema, 'lane')}, not the three documented lanes.")
    # CRT-06 owns the fail-closed behaviour; the schema can express the enum
    # but not the default-deny, so the rule must carry it in prose.
    if 'id="CRT-06"' not in review or "fail closed" not in review.lower():
        errors.append(
            "REVIEW.md: the CRT-06 anchor or its 'fail closed' prose is missing — "
            "the lane enum alone cannot express default-deny."
        )

    if _enum(schema, "verdict") != ["CONFIRMED", "PLAUSIBLE"]:
        errors.append(f"verdict enum drifted: {_enum(schema, 'verdict')}.")
    levels = _enum(schema, "evidence_level")
    if levels != [
        "VERIFIED_BY_EXECUTION",
        "VERIFIED_AGAINST_CODE",
        "VERIFIED_AGAINST_DOCS",
        "INFERRED",
    ]:
        errors.append(f"evidence_level enum drifted: {levels}.")
    if _enum(schema, "severity") != ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
        errors.append(f"severity enum drifted: {_enum(schema, 'severity')}.")
    if _enum(schema, "confidence") != ["HIGH", "MEDIUM", "LOW"]:
        errors.append(f"confidence enum drifted: {_enum(schema, 'confidence')}.")

    # CRT-04 owns the evidence ranking, so its levels are checked against the
    # row that defines them, not CRT-03's.
    crt04 = _rule_row_text(review, "CRT-04")
    if crt04 is None:
        errors.append("REVIEW.md: no CRT-04 Rules-box row found.")
    else:
        for level in levels:
            if level not in crt04:
                errors.append(f"CRT-04 row does not name the level {level!r}.")

    conditionals = _finding_def(schema).get("allOf")
    if not isinstance(conditionals, list) or not any(
        block.get("if", {}).get("properties", {}).get("verdict", {}).get("const") == "CONFIRMED"
        and block.get("then", {}).get("properties", {}).get("evidence_level", {}).get("const")
        == "VERIFIED_BY_EXECUTION"
        for block in conditionals
        if isinstance(block, dict)
    ):
        errors.append("schema does not couple verdict=CONFIRMED to VERIFIED_BY_EXECUTION (CRT-03).")

    # CRT-05: could_not_verify is a mandatory round-level block, not optional.
    if "could_not_verify" not in schema.get("required", []):
        errors.append("schema: round-level `could_not_verify` is not required (CRT-05).")
    # CRT-09: exactly one holistic verdict, from the documented four.
    round_verdict = schema["properties"]["verdict"]["enum"]
    if round_verdict != [
        "reviewed_sound",
        "upheld_with_findings",
        "could_not_verify",
        "rejected",
    ]:
        errors.append(f"round-level verdict enum drifted: {round_verdict} (CRT-09).")

    return errors


# ---------------------------------------------------------------------------
# Green path.
# ---------------------------------------------------------------------------


class TestFindingsSchemaMatchesReviewDoc:
    def test_live_pair_agrees(self) -> None:
        assert check(live_review(), live_schema()) == []

    def test_the_grammar_table_is_not_empty(self) -> None:
        # Guard the guard: the green run above is meaningless over an empty
        # field list, which is why `check` refuses one outright.
        assert len(_field_table_names(live_review())) >= 10


# ---------------------------------------------------------------------------
# TS-29 — the three sabotages S2 ran once, now standing.
# ---------------------------------------------------------------------------


class TestTheGateBites:
    """The exact three defects wave S2 seeded at fold time (`2cc0622`)."""

    def test_dropping_a_required_field_is_caught(self) -> None:
        schema = copy.deepcopy(live_schema())
        dropped = schema["$defs"]["finding"]["required"].pop()
        errors = check(live_review(), schema)
        assert any("CRT-03 drift" in e and "required finding" in e for e in errors), errors
        # ... and restoring it clears (the green half of the proof).
        schema["$defs"]["finding"]["required"].append(dropped)
        assert check(live_review(), schema) == []

    def test_weakening_the_confirmed_coupling_is_caught(self) -> None:
        schema = copy.deepcopy(live_schema())
        for block in schema["$defs"]["finding"]["allOf"]:
            if block.get("if", {}).get("properties", {}).get("verdict", {}).get("const") == (
                "CONFIRMED"
            ):
                block["then"]["properties"]["evidence_level"]["const"] = "INFERRED"
        errors = check(live_review(), schema)
        assert any("does not couple verdict=CONFIRMED" in e for e in errors), errors

    def test_deleting_a_crt_anchor_is_caught(self) -> None:
        review = live_review().replace('<a id="CRT-06"></a>', "")
        errors = check(review, live_schema())
        assert any("CRT-06 anchor" in e for e in errors), errors

    def test_a_field_the_binding_row_does_not_carry_is_caught(self) -> None:
        # The defect this gate found on its very first run in wave S2:
        # `evidence` sat in the §3 table and was missing from the CRT-03 row.
        review = live_review().replace("`evidence`", "`evidence_omitted`", 1)
        errors = check(review, live_schema())
        assert errors, "removing a field from the binding row went unnoticed"

    def test_an_enum_drift_is_caught(self) -> None:
        schema = copy.deepcopy(live_schema())
        schema["$defs"]["finding"]["properties"]["severity"]["enum"] = ["MEDIUM-HIGH"]
        errors = check(live_review(), schema)
        assert any("severity enum drifted" in e for e in errors), errors

    def test_a_renamed_section_three_is_a_failure_not_a_pass(self) -> None:
        # The vacuity mode: rename §3 and every field comparison would compare
        # against nothing.
        review = live_review().replace("## 3. The findings grammar", "## 3. The grammar")
        errors = check(review, live_schema())
        assert len(errors) == 1
        assert "vacuously" in errors[0]
