"""
Structural consistency tests for AGENTS.md and copilot-instructions.md.

Tests verify internal cross-references, not semantic content.
Runs on Python 3.8+ with no external dependencies.

Usage:
    python -m pytest tests/test_consistency.py -v
    python tests/test_consistency.py              # also works standalone
"""

import re
import unittest
from pathlib import Path

# ---------------------------------------------------------------------------
# File paths (relative to repo root)
# ---------------------------------------------------------------------------
ROOT = Path(__file__).resolve().parent.parent
AGENTS = ROOT / "AGENTS.md"
COPILOT = ROOT / ".github" / "copilot-instructions.md"


def _read(path: Path) -> str:
    return path.read_text(encoding="utf-8")


# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------

def extract_defined_axioms(text: str) -> set[str]:
    """Extract axiom IDs from bold definitions like **AX17 — ...**
    and from nuance headers like ### N-1: ... (AX58)."""
    # Bold definitions in body: **AX17 — ...**
    bold = set(re.findall(r"\*\*AX(\d+)\b", text))
    # Nuance headers in Appendix A: ### N-x: ... (AX58)
    nuance = set(re.findall(r"### N-\d+:.*?\(AX(\d+)\)", text))
    return bold | nuance


def extract_defined_theorems(text: str) -> set[str]:
    """Extract theorem IDs from bold definitions like **T12 — ...**"""
    return set(re.findall(r"\*\*T(\d+)\b", text))


def extract_defined_kavaid(text: str) -> set[str]:
    """Extract kavaid IDs from bold definitions like **KV₁ — ...**"""
    # KV followed by subscript digit (₁…₈) or plain digit
    subscripts = {"₁": "1", "₂": "2", "₃": "3", "₄": "4",
                  "₅": "5", "₆": "6", "₇": "7", "₈": "8"}
    ids = set()
    for m in re.finditer(r"\*\*KV([₁₂₃₄₅₆₇₈])\b", text):
        ids.add(subscripts[m.group(1)])
    return ids


def extract_all_ax_references(text: str) -> set[str]:
    """Extract every AXnn reference (not just definitions)."""
    return set(re.findall(r"AX(\d+)", text))


def extract_all_t_references(text: str) -> set[str]:
    """Extract every Tnn reference (theorem-like), filtering noise."""
    # Match T followed by 1-2 digits, at word boundary, not inside words
    candidates = set()
    for m in re.finditer(r"\bT(\d{1,2})\b", text):
        num = m.group(1)
        n = int(num)
        # Only T1-T18 are valid theorems
        if 1 <= n <= 18:
            candidates.add(num)
    return candidates


def extract_section_headers(text: str) -> list[str]:
    """Extract all ## and ### level headers."""
    return re.findall(r"^(#{2,3})\s+(.+)$", text, re.MULTILINE)


# ===========================================================================
# TEST CLASS 1: Axiom Consistency
# ===========================================================================
class TestAxiomConsistency(unittest.TestCase):
    """Verify every axiom reference resolves to a definition or documented gap."""

    @classmethod
    def setUpClass(cls):
        cls.text = _read(AGENTS)
        cls.defined = extract_defined_axioms(cls.text)
        # Documented gaps from IG.6
        cls.gaps = {str(n) for n in range(6, 11)} | {"35", "36"}  # AX6-10, AX35-36

    def test_expected_axiom_count(self):
        """Metadata claims 66 axioms (AX1-AX66). Defined + gaps should cover 1-66."""
        full_range = {str(n) for n in range(1, 67)}
        covered = self.defined | self.gaps
        missing = full_range - covered
        self.assertEqual(missing, set(),
                         f"Axiom numbers neither defined nor in gap register: {sorted(missing, key=int)}")

    def test_no_axiom_beyond_66(self):
        """No axiom definition exceeds AX66."""
        for ax in self.defined:
            self.assertLessEqual(int(ax), 66, f"AX{ax} exceeds declared range AX1-AX66")

    def test_inference_chain_axioms_exist(self):
        """Every AX referenced in inference chains (IG.2) is defined or gapped."""
        # Extract only the inference graph section
        ig_match = re.search(r"## INFERENCE GRAPH.*?(?=\n## )", self.text, re.DOTALL)
        self.assertIsNotNone(ig_match, "Inference Graph section not found")
        ig_text = ig_match.group(0)
        referenced = extract_all_ax_references(ig_text)
        known = self.defined | self.gaps
        orphans = referenced - known
        self.assertEqual(orphans, set(),
                         f"Axioms referenced in inference graph but not defined: AX{sorted(orphans, key=int)}")

    def test_appendix_b_axioms_exist(self):
        """Every axiom listed in Appendix B.1 is defined or gapped."""
        appb = re.search(r"### B\.1.*?(?=### B\.2)", self.text, re.DOTALL)
        self.assertIsNotNone(appb, "Appendix B.1 not found")
        referenced = extract_all_ax_references(appb.group(0))
        known = self.defined | self.gaps
        orphans = referenced - known
        self.assertEqual(orphans, set(),
                         f"Axioms in Appendix B but not defined: AX{sorted(orphans, key=int)}")


# ===========================================================================
# TEST CLASS 2: Theorem Consistency
# ===========================================================================
class TestTheoremConsistency(unittest.TestCase):
    """Verify every theorem reference resolves to a definition or documented gap."""

    @classmethod
    def setUpClass(cls):
        cls.text = _read(AGENTS)
        cls.defined = extract_defined_theorems(cls.text)
        # Documented gaps from IG.6: T1, T2 unassigned; T11 index-only
        cls.gaps = {"1", "2", "11"}

    def test_expected_theorem_count(self):
        """Metadata claims 18 theorems (T1-T18). Defined + gaps should cover 1-18."""
        full_range = {str(n) for n in range(1, 19)}
        covered = self.defined | self.gaps
        missing = full_range - covered
        self.assertEqual(missing, set(),
                         f"Theorem numbers neither defined nor in gap register: {sorted(missing, key=int)}")

    def test_no_theorem_beyond_18(self):
        """No theorem definition exceeds T18."""
        for t in self.defined:
            self.assertLessEqual(int(t), 18, f"T{t} exceeds declared range T1-T18")

    def test_inference_chain_theorems_exist(self):
        """Every T referenced in inference chains is defined or gapped."""
        ig_match = re.search(r"## INFERENCE GRAPH.*?(?=\n## )", self.text, re.DOTALL)
        self.assertIsNotNone(ig_match)
        ig_text = ig_match.group(0)
        # Be more targeted: look for T followed by digits in theorem-like context
        referenced = set()
        for m in re.finditer(r"\bT(\d{1,2})\b", ig_text):
            n = int(m.group(1))
            if 1 <= n <= 18:
                referenced.add(m.group(1))
        known = self.defined | self.gaps
        orphans = referenced - known
        self.assertEqual(orphans, set(),
                         f"Theorems referenced in inference graph but not defined: T{sorted(orphans, key=int)}")


# ===========================================================================
# TEST CLASS 3: Kavaid Consistency
# ===========================================================================
class TestKavaidConsistency(unittest.TestCase):
    """Verify all 8 kavaid are defined and referenced."""

    @classmethod
    def setUpClass(cls):
        cls.text = _read(AGENTS)
        cls.defined = extract_defined_kavaid(cls.text)

    def test_all_eight_kavaid_defined(self):
        """KV₁ through KV₈ must all have bold definitions."""
        expected = {str(n) for n in range(1, 9)}
        missing = expected - self.defined
        self.assertEqual(missing, set(),
                         f"Kavaid not defined in body: KV{sorted(missing)}")

    def test_kavaid_in_appendix_b3(self):
        """Appendix B.3 should list all 8 kavaid."""
        appb3 = re.search(r"### B\.3.*?(?=### B\.4)", self.text, re.DOTALL)
        self.assertIsNotNone(appb3, "Appendix B.3 not found")
        text = appb3.group(0)
        subscripts = {"₁": "1", "₂": "2", "₃": "3", "₄": "4",
                      "₅": "5", "₆": "6", "₇": "7", "₈": "8"}
        found = set()
        for m in re.finditer(r"KV([₁₂₃₄₅₆₇₈])", text):
            found.add(subscripts[m.group(1)])
        expected = {str(n) for n in range(1, 9)}
        missing = expected - found
        self.assertEqual(missing, set(),
                         f"Kavaid missing from Appendix B.3: KV{sorted(missing)}")


# ===========================================================================
# TEST CLASS 4: Output Rule Traceability
# ===========================================================================
class TestOutputRuleTraceability(unittest.TestCase):
    """Verify every OR rule in IG.5 traces back to real framework elements."""

    @classmethod
    def setUpClass(cls):
        cls.text = _read(AGENTS)
        cls.defined_ax = extract_defined_axioms(cls.text)
        cls.defined_t = extract_defined_theorems(cls.text)
        cls.ax_gaps = {str(n) for n in range(6, 11)} | {"35", "36"}
        cls.t_gaps = {"1", "2", "11"}

    def test_output_gate_axiom_references(self):
        """Every AX cited in the Output Gate DAG (IG.5) must exist."""
        ig5 = re.search(r"### IG\.5.*?(?=### IG\.6)", self.text, re.DOTALL)
        self.assertIsNotNone(ig5, "IG.5 section not found")
        referenced = extract_all_ax_references(ig5.group(0))
        known = self.defined_ax | self.ax_gaps
        orphans = referenced - known
        self.assertEqual(orphans, set(),
                         f"Output gate references undefined axioms: AX{sorted(orphans, key=int)}")

    def test_output_gate_theorem_references(self):
        """Every T cited in the Output Gate DAG (IG.5) must exist."""
        ig5 = re.search(r"### IG\.5.*?(?=### IG\.6)", self.text, re.DOTALL)
        self.assertIsNotNone(ig5)
        referenced = set()
        for m in re.finditer(r"\bT(\d{1,2})\b", ig5.group(0)):
            n = int(m.group(1))
            if 1 <= n <= 18:
                referenced.add(m.group(1))
        known = self.defined_t | self.t_gaps
        orphans = referenced - known
        self.assertEqual(orphans, set(),
                         f"Output gate references undefined theorems: T{sorted(orphans, key=int)}")

    def test_eight_output_rules_present(self):
        """Section 8.4 must contain exactly 8 numbered output constraints."""
        sec84 = re.search(r"### 8\.4 Output Constraints.*?(?=### 8\.4\.1)", self.text, re.DOTALL)
        self.assertIsNotNone(sec84, "Section 8.4 not found")
        # Count numbered list items
        items = re.findall(r"^\d+\.\s+\*\*", sec84.group(0), re.MULTILINE)
        self.assertEqual(len(items), 8,
                         f"Expected 8 output constraints, found {len(items)}")


# ===========================================================================
# TEST CLASS 5: Section Structure
# ===========================================================================
class TestSectionStructure(unittest.TestCase):
    """Verify expected document sections exist."""

    @classmethod
    def setUpClass(cls):
        cls.text = _read(AGENTS)

    def test_required_layers_present(self):
        """All 8 layers + preamble + inference graph + appendices must exist."""
        required = [
            "LAYER 8",
            "PREAMBLE",
            "LAYER 1",
            "LAYER 2",
            "LAYER 3",
            "LAYER 4",
            "LAYER 5",
            "LAYER 6",
            "LAYER 7",
            "INFERENCE GRAPH",
            "APPENDIX A",
            "APPENDIX B",
            "APPENDIX C",
            "DOCUMENT METADATA",
        ]
        for section in required:
            self.assertIn(section, self.text,
                          f"Required section '{section}' not found in AGENTS.md")

    def test_layer_order(self):
        """Layers must appear in correct order: 8, Preamble, 1-7, IG, A-C."""
        ordered = [
            "LAYER 8",
            "PREAMBLE",
            "LAYER 1",
            "LAYER 2",
            "LAYER 3",
            "LAYER 4",
            "LAYER 5",
            "LAYER 6",
            "LAYER 7",
            "INFERENCE GRAPH",
            "APPENDIX A",
            "APPENDIX B",
            "APPENDIX C",
        ]
        positions = []
        for section in ordered:
            pos = self.text.find(section)
            self.assertGreater(pos, -1, f"'{section}' not found")
            positions.append(pos)
        for i in range(len(positions) - 1):
            self.assertLess(positions[i], positions[i + 1],
                            f"'{ordered[i]}' should appear before '{ordered[i + 1]}'")

    def test_inference_graph_subsections(self):
        """IG.1 through IG.6 must all exist."""
        for i in range(1, 7):
            self.assertIn(f"### IG.{i}", self.text,
                          f"Inference Graph subsection IG.{i} missing")


# ===========================================================================
# TEST CLASS 6: Nuance-to-Axiom Pairing
# ===========================================================================
class TestNuancePairing(unittest.TestCase):
    """Each N-x nuance must reference a real AX."""

    @classmethod
    def setUpClass(cls):
        cls.text = _read(AGENTS)
        cls.defined_ax = extract_defined_axioms(cls.text)
        cls.ax_gaps = {str(n) for n in range(6, 11)} | {"35", "36"}

    def test_nuance_axiom_references(self):
        """N-1 through N-6 each reference an axiom (AX58-AX63)."""
        app_a = re.search(r"## APPENDIX A.*?(?=\n## APPENDIX B)", self.text, re.DOTALL)
        self.assertIsNotNone(app_a, "Appendix A not found")
        referenced = extract_all_ax_references(app_a.group(0))
        known = self.defined_ax | self.ax_gaps
        orphans = referenced - known
        self.assertEqual(orphans, set(),
                         f"Nuances reference undefined axioms: AX{sorted(orphans, key=int)}")

    def test_all_six_nuances_present(self):
        """N-1 through N-6 must all exist."""
        for i in range(1, 7):
            self.assertRegex(self.text, rf"### N-{i}:",
                             f"Nuance N-{i} not found")


# ===========================================================================
# TEST CLASS 7: Metadata Accuracy
# ===========================================================================
class TestMetadata(unittest.TestCase):
    """Verify document metadata matches actual content."""

    @classmethod
    def setUpClass(cls):
        cls.text = _read(AGENTS)
        cls.defined_ax = extract_defined_axioms(cls.text)
        cls.defined_t = extract_defined_theorems(cls.text)
        cls.defined_kv = extract_defined_kavaid(cls.text)

    def test_axiom_range_in_metadata(self):
        """Metadata says 'Axioms: 66 (AX1–AX66)'."""
        self.assertIn("Axioms:", self.text)
        self.assertIn("66", self.text[self.text.find("Axioms:"):self.text.find("Axioms:") + 40])

    def test_theorem_range_in_metadata(self):
        """Metadata says 'Theorems: 18 (T1–T18)'."""
        self.assertIn("Theorems:", self.text)
        self.assertIn("18", self.text[self.text.find("Theorems:"):self.text.find("Theorems:") + 40])

    def test_kavaid_count_in_metadata(self):
        """Metadata says 'Kavaid: 8'."""
        self.assertIn("Kavaid:", self.text)
        meta_section = self.text[self.text.find("Kavaid:"):self.text.find("Kavaid:") + 30]
        self.assertIn("8", meta_section)

    def test_inference_chains_in_metadata(self):
        """Metadata should reference 7 primary chains."""
        self.assertIn("7 primary chains", self.text)

    def test_framework_count_in_metadata(self):
        """Metadata says 'Frameworks: 13'."""
        # Appendix B.5 lists 13 frameworks
        appb5 = re.search(r"### B\.5.*?(?=\n---|\n## )", self.text, re.DOTALL)
        self.assertIsNotNone(appb5, "Appendix B.5 not found")
        numbered = re.findall(r"^\d+\.\s+", appb5.group(0), re.MULTILINE)
        self.assertEqual(len(numbered), 13,
                         f"Appendix B.5 lists {len(numbered)} frameworks, metadata claims 13")


# ===========================================================================
# TEST CLASS 8: Copilot Instructions Consistency
# ===========================================================================
class TestCopilotInstructions(unittest.TestCase):
    """Verify copilot-instructions.md is consistent with AGENTS.md."""

    @classmethod
    def setUpClass(cls):
        cls.agents = _read(AGENTS)
        cls.copilot = _read(COPILOT)

    def test_term_table_interface_reference(self):
        """Copilot §3 must reference AGENTS.md §8.2 as the canonical source for term translations.
        The term table itself lives in AGENTS.md §8.2 (İlim); copilot §3 (İrade) provides
        a pointer, not a copy — this is the constitutive dependency interface."""
        sec3 = re.search(r"## 3\. Term Translation.*?(?=\n## 4\.)", self.copilot, re.DOTALL)
        self.assertIsNotNone(sec3, "Section 3 not found in copilot-instructions.md")
        sec3_text = sec3.group(0)
        self.assertIn("AGENTS.md", sec3_text,
                      "Section 3 must reference AGENTS.md as canonical source")
        self.assertIn("§8.2", sec3_text,
                      "Section 3 must reference AGENTS.md §8.2 specifically")
        # The canonical table must still exist in AGENTS.md
        agents_table = re.search(r"### 8\.2 Term Translation.*?(?=### 8\.3)", self.agents, re.DOTALL)
        self.assertIsNotNone(agents_table, "AGENTS.md §8.2 term table not found")
        # Verify the canonical table actually has terms
        terms = re.findall(r"^\|\s*(.+?)\s*\|", agents_table.group(0), re.MULTILINE)
        term_count = len([t for t in terms if t.strip() not in ("Internal Term", "") and not t.startswith("---")])
        self.assertGreaterEqual(term_count, 20,
                                f"AGENTS.md §8.2 should have ≥20 terms, found {term_count}")

    def test_kavaid_count_matches(self):
        """Copilot file should reference all 8 kavaid."""
        sec5 = re.search(r"## 5\. Eight Kavaid.*?(?=\n## 6\.)", self.copilot, re.DOTALL)
        self.assertIsNotNone(sec5, "Section 5 not found in copilot-instructions.md")
        subscripts = {"₁": "1", "₂": "2", "₃": "3", "₄": "4",
                      "₅": "5", "₆": "6", "₇": "7", "₈": "8"}
        found = set()
        for m in re.finditer(r"KV([₁₂₃₄₅₆₇₈])", sec5.group(0)):
            found.add(subscripts[m.group(1)])
        expected = {str(n) for n in range(1, 9)}
        missing = expected - found
        self.assertEqual(missing, set(),
                         f"Kavaid missing from copilot Section 5: KV{sorted(missing)}")

    def test_five_reasoning_paths_present(self):
        """P0 through P4 must all be defined."""
        for p in ["P0", "P1", "P2", "P3", "P4"]:
            self.assertIn(f"**{p} —", self.copilot,
                          f"Reasoning path {p} not found in copilot-instructions.md")

    def test_output_constraints_count(self):
        """Section 7 should have 8 output constraints."""
        sec7 = re.search(r"## 7\. Output Constraints.*?(?=\n## 8\.)", self.copilot, re.DOTALL)
        self.assertIsNotNone(sec7, "Section 7 not found in copilot-instructions.md")
        items = re.findall(r"^\d+\.\s+\*\*", sec7.group(0), re.MULTILINE)
        self.assertEqual(len(items), 8,
                         f"Expected 8 output constraints in copilot §7, found {len(items)}")

    def test_always_engage_rule(self):
        """The routing dispatch must NOT contain 'Answer normally without engaging'."""
        self.assertNotIn("Answer normally without engaging", self.copilot,
                         "Found outdated 'skip framework' escape hatch in routing dispatch")

    def test_framework_always_engaged(self):
        """The routing dispatch must contain 'framework is ALWAYS engaged'."""
        self.assertIn("framework is ALWAYS engaged", self.copilot,
                      "Missing 'ALWAYS engaged' mandate in routing dispatch")


# ===========================================================================
# TEST CLASS 9: Cross-Reference Accuracy (Appendix B sections)
# ===========================================================================
class TestCrossReferenceAccuracy(unittest.TestCase):
    """Verify Appendix B section references match actual section headers."""

    @classmethod
    def setUpClass(cls):
        cls.text = _read(AGENTS)

    def test_appendix_b1_section_references(self):
        """Section numbers in Appendix B.1 should correspond to real headers."""
        appb1 = re.search(r"### B\.1.*?(?=### B\.2)", self.text, re.DOTALL)
        self.assertIsNotNone(appb1)
        # Extract section references like "1.1", "2.4", "5.2" etc.
        sections_referenced = set(re.findall(r"\|\s*(\d+\.\d+(?:–\d+\.\d+)?)\s*\|", appb1.group(0)))
        # For each simple section (not ranges), check it exists as a header
        for sec_ref in sections_referenced:
            if "–" in sec_ref:
                # Range like "5.1–5.2" — check both endpoints
                parts = sec_ref.split("–")
                for part in parts:
                    self.assertRegex(self.text, rf"### {re.escape(part)}\b",
                                     f"Section {part} referenced in B.1 but no matching header found")
            else:
                # Special cases: "A" for appendix
                if sec_ref == "A":
                    continue  # Appendix A is ## level, tested elsewhere
                self.assertRegex(self.text, rf"### {re.escape(sec_ref)}\b",
                                 f"Section {sec_ref} referenced in B.1 but no matching header found")

    def test_appendix_b2_section_references(self):
        """Theorem section references in B.2 should match real headers."""
        appb2 = re.search(r"### B\.2.*?(?=### B\.3)", self.text, re.DOTALL)
        self.assertIsNotNone(appb2)
        sections_referenced = set(re.findall(r"\|\s*(\d+\.\d+)\s*\|", appb2.group(0)))
        for sec_ref in sections_referenced:
            self.assertRegex(self.text, rf"### {re.escape(sec_ref)}\b",
                             f"Section {sec_ref} referenced in B.2 but no matching header found")


# ===========================================================================
# TEST CLASS 10: Gap Register Self-Consistency
# ===========================================================================
class TestGapRegister(unittest.TestCase):
    """Verify IG.6 gap register is accurate."""

    @classmethod
    def setUpClass(cls):
        cls.text = _read(AGENTS)
        cls.defined_ax = extract_defined_axioms(cls.text)
        cls.defined_t = extract_defined_theorems(cls.text)

    def test_declared_axiom_gaps_are_actually_missing(self):
        """AX6-10 and AX35-36 should NOT have bold definitions."""
        for n in [6, 7, 8, 9, 10, 35, 36]:
            self.assertNotIn(str(n), self.defined_ax,
                             f"AX{n} is declared as a gap in IG.6 but has a bold definition")

    def test_declared_theorem_gaps_are_actually_missing(self):
        """T1, T2, T11 should NOT have bold definitions."""
        for n in [1, 2, 11]:
            self.assertNotIn(str(n), self.defined_t,
                             f"T{n} is declared as a gap in IG.6 but has a bold definition")

    def test_no_undeclared_gaps(self):
        """Every number 1-66 should be either defined or in the gap register.
        (Mirrors TestAxiomConsistency.test_expected_axiom_count but from gap side.)"""
        gaps = {str(n) for n in range(6, 11)} | {"35", "36"}
        full_range = {str(n) for n in range(1, 67)}
        covered = self.defined_ax | gaps
        undeclared = full_range - covered
        self.assertEqual(undeclared, set(),
                         f"Numbers neither defined nor gapped: {sorted(undeclared, key=int)}")


# ===========================================================================
# TEST CLASS 11: Holographic Self-Similarity Check
# ===========================================================================
class TestHolographicMirroring(unittest.TestCase):
    """
    The framework claims holographic architecture (AX37) — parts mirror wholes.
    Test: copilot-instructions.md should structurally mirror key AGENTS.md elements.
    """

    @classmethod
    def setUpClass(cls):
        cls.agents = _read(AGENTS)
        cls.copilot = _read(COPILOT)

    def test_copilot_references_agents(self):
        """Copilot file must reference AGENTS.md."""
        self.assertIn("AGENTS.md", self.copilot)

    def test_copilot_has_ne_ayn_ne_gayr_statement(self):
        """Copilot file should declare its own NeAynNeGayr relationship."""
        self.assertIn("NeAynNeGayr", self.copilot,
                      "Copilot file should declare participatory non-identity with AGENTS.md")

    def test_copilot_has_fidelity_bound(self):
        """Copilot file should declare 0 < Fidelity < 1."""
        self.assertIn("0 < Fidelity < 1", self.copilot,
                      "Copilot file should declare fidelity bound")

    def test_structural_pattern_table_exists(self):
        """Both files must reference 'Structural Pattern Translation'.
        AGENTS.md owns the canonical table (§8.3); copilot provides an interface reference."""
        self.assertIn("Structural Pattern Translation", self.copilot,
                      "Copilot must reference Structural Pattern Translation")
        self.assertIn("Structural Pattern Translation", self.agents,
                      "AGENTS.md must contain Structural Pattern Translation table")
        # Copilot §4 should point to AGENTS.md §8.3 as canonical source
        sec4 = re.search(r"## 4\. Structural Pattern Translation.*?(?=\n## 5\.)", self.copilot, re.DOTALL)
        self.assertIsNotNone(sec4, "Section 4 not found in copilot-instructions.md")
        self.assertIn("AGENTS.md", sec4.group(0),
                      "Copilot §4 must reference AGENTS.md as canonical source")
        self.assertIn("§8.3", sec4.group(0),
                      "Copilot §4 must reference AGENTS.md §8.3 specifically")


# ===========================================================================
# TEST CLASS 12: Appendix B.4 Methodology Rules
# ===========================================================================
class TestMethodologyRules(unittest.TestCase):
    """Verify methodology rules referenced in B.4 are mentioned in the body."""

    @classmethod
    def setUpClass(cls):
        cls.text = _read(AGENTS)

    def test_methodology_rules_in_body(self):
        """M1-M5, K-8, K-9, K-10 should appear somewhere in Layers 1-7."""
        body = re.search(r"## LAYER 1.*?## INFERENCE GRAPH", self.text, re.DOTALL)
        self.assertIsNotNone(body)
        body_text = body.group(0)
        expected_rules = ["M1", "M2", "M3", "M4", "M5", "K-8", "K-9", "K-10"]
        for rule in expected_rules:
            # Check that the rule identifier appears (may be in bold, in text, etc.)
            self.assertIn(rule, body_text,
                          f"Methodology rule {rule} listed in B.4 but not found in Layers 1-7")

    def test_r_rules_in_layer_6(self):
        """R-1 through R-7 should appear in Layer 6."""
        layer6 = re.search(r"## LAYER 6.*?(?=## LAYER 7)", self.text, re.DOTALL)
        self.assertIsNotNone(layer6)
        for i in range(1, 8):
            self.assertIn(f"R-{i}", layer6.group(0),
                          f"Rule R-{i} not found in Layer 6")


# ===========================================================================
# TEST CLASS 12b: Triad Architecture (İlim/İrade/Kudret)
# ===========================================================================
class TestTriadArchitecture(unittest.TestCase):
    """Verify the three-document triad declares and cross-references correctly.

    The triad (AX2-AX4 actualization):
      AGENTS.md = İlim (Distinguishing) — owns definitions
      copilot-instructions.md = İrade (Specifying) — owns routing
      fw.prompt.md = Kudret (Effecting) — owns execution
    Each document must declare its role and reference the others."""

    FW_PROMPT = ROOT / ".github" / "prompts" / "fw.prompt.md"

    @classmethod
    def setUpClass(cls):
        cls.agents = _read(AGENTS)
        cls.copilot = _read(COPILOT)
        cls.fw = cls.FW_PROMPT.read_text(encoding="utf-8") if cls.FW_PROMPT.is_file() else ""

    def test_agents_declares_ilim_role(self):
        """AGENTS.md must declare itself as İlim organ of the triad."""
        self.assertIn("İlim", self.agents,
                      "AGENTS.md must declare its İlim role")

    def test_copilot_declares_irade_role(self):
        """copilot-instructions.md must declare itself as İrade organ."""
        self.assertIn("İrade", self.copilot,
                      "copilot-instructions.md must declare its İrade role")

    def test_fw_declares_kudret_role(self):
        """fw.prompt.md must declare itself as Kudret organ."""
        if not self.fw:
            self.skipTest("fw.prompt.md not found")
        self.assertIn("Kudret", self.fw,
                      "fw.prompt.md must declare its Kudret role")

    def test_agents_references_copilot(self):
        """AGENTS.md must reference copilot-instructions.md."""
        self.assertIn("copilot-instructions.md", self.agents)

    def test_agents_references_fw(self):
        """AGENTS.md must reference fw.prompt.md."""
        self.assertIn("fw.prompt.md", self.agents)

    def test_copilot_references_agents(self):
        """copilot-instructions.md must reference AGENTS.md."""
        self.assertIn("AGENTS.md", self.copilot)

    def test_copilot_references_fw(self):
        """copilot-instructions.md must reference fw.prompt.md."""
        self.assertIn("fw.prompt.md", self.copilot)

    def test_fw_references_agents(self):
        """fw.prompt.md must reference AGENTS.md."""
        if not self.fw:
            self.skipTest("fw.prompt.md not found")
        self.assertIn("AGENTS.md", self.fw)

    def test_fw_references_copilot(self):
        """fw.prompt.md must reference copilot-instructions.md."""
        if not self.fw:
            self.skipTest("fw.prompt.md not found")
        self.assertIn("copilot-instructions.md", self.fw)

    def test_triad_table_in_agents(self):
        """AGENTS.md metadata must contain a Triad line."""
        self.assertIn("Triad:", self.agents,
                      "AGENTS.md metadata must declare the Triad")

    def test_copilot_interface_references(self):
        """copilot-instructions.md must contain interface references to AGENTS.md sections.
        §3→§8.2, §4→§8.3, §5→§5.4, §6→§7.2."""
        expected_refs = ["§8.2", "§8.3", "§5.4", "§7.2"]
        for ref in expected_refs:
            self.assertIn(ref, self.copilot,
                          f"copilot-instructions.md must reference AGENTS.md {ref}")

    def test_fw_hayat_integration(self):
        """fw.prompt.md must declare the Hayat / AX5 integration principle."""
        if not self.fw:
            self.skipTest("fw.prompt.md not found")
        self.assertIn("AX5", self.fw,
                      "fw.prompt.md must reference AX5 (Hayat integration)")
        self.assertIn("Hayat", self.fw,
                      "fw.prompt.md must mention Hayat as integrating principle")

    def test_no_duplicate_term_table_in_copilot(self):
        """copilot-instructions.md must NOT contain a duplicate term table.
        The canonical table lives in AGENTS.md §8.2 only."""
        sec3 = re.search(r"## 3\. Term Translation.*?(?=\n## 4\.)", self.copilot, re.DOTALL)
        self.assertIsNotNone(sec3, "Section 3 not found")
        # Should NOT have table rows with term definitions (| Term | Universal ... |)
        table_rows = re.findall(r"^\|\s*[A-Z].*\|.*\|", sec3.group(0), re.MULTILINE)
        self.assertEqual(len(table_rows), 0,
                         f"Section 3 should be an interface reference, not a duplicate table. "
                         f"Found {len(table_rows)} table rows.")


# ===========================================================================
# TEST CLASS 13: Routing Enforcement
# ===========================================================================
class TestRoutingEnforcement(unittest.TestCase):
    """Verify the enforcement mechanisms are structurally complete.
    
    These tests ensure the routing and disclosure architecture makes 
    framework engagement mandatory and verifiable."""

    @classmethod
    def setUpClass(cls):
        cls.copilot = _read(COPILOT)
        cls.agents = _read(AGENTS)

    def test_mandatory_classification_step_exists(self):
        """copilot-instructions.md must contain a MANDATORY classification step."""
        self.assertIn("MANDATORY", self.copilot)
        self.assertIn("Step 0", self.copilot,
                      "Missing 'Step 0' — mandatory pre-response classification gate")

    def test_step_0_references_all_paths(self):
        """Step 0 classification must list all 5 paths (P0–P4)."""
        # Find the classification section
        sec = re.search(r"### 1\.1.*?(?=### 1\.2)", self.copilot, re.DOTALL)
        self.assertIsNotNone(sec, "Section 1.1 not found")
        sec_text = sec.group(0)
        for p in ["P0", "P1", "P2", "P3", "P4"]:
            self.assertIn(p, sec_text,
                          f"Path {p} not referenced in Step 0 classification (§1.1)")

    def test_bias_toward_engagement(self):
        """Routing must specify bias toward P1–P4 over P0 when uncertain."""
        self.assertIn("bias toward", self.copilot.lower(),
                      "Missing bias-toward-engagement routing rule")

    def test_disclosure_protocol_exists(self):
        """§1.4 AX57 Disclosure Protocol must exist with concrete format."""
        self.assertIn("AX57 Disclosure Protocol", self.copilot,
                      "Missing AX57 Disclosure Protocol section")
        self.assertIn("*Framework:", self.copilot,
                      "Missing concrete disclosure format template")

    def test_disclosure_format_in_agents_md(self):
        """AGENTS.md must also contain operationalized disclosure format."""
        self.assertIn("Operationalized disclosure format", self.agents,
                      "Missing operationalized AX57 format in AGENTS.md")

    def test_p0_invisible_p1_p4_visible_rule(self):
        """Disclosure protocol must distinguish P0 (internal) from P1–P4 (visible)."""
        sec = re.search(r"### 1\.4.*?(?=\n## 2\.)", self.copilot, re.DOTALL)
        self.assertIsNotNone(sec, "Section 1.4 not found")
        sec_text = sec.group(0)
        self.assertIn("P0", sec_text)
        self.assertIn("internal", sec_text.lower(),
                      "P0 disclosure should be marked as internal-only")
        self.assertIn("visible", sec_text.lower(),
                      "P1–P4 disclosures should be marked as visible")

    def test_worked_examples_exist(self):
        """Section 9 must contain worked examples for all 5 paths."""
        self.assertIn("Worked Examples", self.copilot,
                      "Missing Worked Examples section")
        for path_label in ["P0 —", "P1 —", "P2 —", "P3 —", "P4 —"]:
            self.assertIn(f"**{path_label}", self.copilot,
                          f"Missing worked example for {path_label.rstrip(' —')}")

    def test_worked_examples_have_disclosure(self):
        """P1–P4 worked examples must each contain a disclosure line."""
        sec9 = re.search(r"## 9\. Worked Examples.*", self.copilot, re.DOTALL)
        self.assertIsNotNone(sec9, "Section 9 not found")
        sec_text = sec9.group(0)
        # Count disclosure lines (Framework: P...)
        disclosures = re.findall(r"\*Framework: P[1-4]", sec_text)
        self.assertGreaterEqual(len(disclosures), 4,
                                f"Expected 4 disclosure examples (P1–P4), found {len(disclosures)}")

    def test_agents_md_step_0_in_protocol(self):
        """AGENTS.md §8.1 must include Step 0 classification."""
        sec81 = re.search(r"### 8\.1 Translation Protocol.*?```\n.*?```", self.agents, re.DOTALL)
        self.assertIsNotNone(sec81)
        self.assertIn("CLASSIFY", sec81.group(0),
                       "AGENTS.md §8.1 missing CLASSIFY step (Step 0)")

    def test_grade_values_specified(self):
        """Disclosure protocol must specify the 3 accessible grade values."""
        self.assertIn("Tasavvur", self.copilot)
        self.assertIn("Tasdik", self.copilot)
        # İlmelyakîn may appear with or without diacritics
        self.assertTrue(
            "İlmelyakîn" in self.copilot or "Ilmelyakin" in self.copilot,
            "Missing İlmelyakîn grade in disclosure protocol")

    def test_framework_violation_language(self):
        """Omitting disclosure for P1–P4 must be explicitly called a violation."""
        self.assertIn("framework violation", self.copilot.lower(),
                      "Missing explicit 'framework violation' language for disclosure omission")


# ===========================================================================
# TEST CLASS 14: User Directives
# ===========================================================================
class TestUserDirectives(unittest.TestCase):
    """Verify user-typed prompt directives are defined and consistent."""

    @classmethod
    def setUpClass(cls):
        cls.copilot = _read(COPILOT)
        cls.agents = _read(AGENTS)

    def test_directive_section_exists_copilot(self):
        """copilot-instructions.md must have §1.5 User Directives."""
        self.assertIn("User Directives", self.copilot,
                      "Missing User Directives section in copilot-instructions.md")

    def test_directive_section_exists_agents(self):
        """AGENTS.md must have §8.4.1 User Directives."""
        self.assertIn("### 8.4.1 User Directives", self.agents,
                      "Missing User Directives section in AGENTS.md")

    def test_all_directives_in_copilot(self):
        """All 5 directives must appear in copilot-instructions.md."""
        for directive in ["/fw\n", "/fw P1", "/audit", "/disclose", "/fw check"]:
            self.assertIn(directive, self.copilot,
                          f"Directive '{directive.strip()}' not found in copilot-instructions.md")

    def test_all_directives_in_agents(self):
        """All 5 directives must appear in AGENTS.md."""
        for directive in ["/fw`", "/fw P[1-4]", "/audit", "/disclose", "/fw check"]:
            self.assertIn(directive, self.agents,
                          f"Directive '{directive}' not found in AGENTS.md")

    def test_mandatory_override_language(self):
        """Directives must be described as MANDATORY overrides."""
        self.assertIn("MANDATORY overrides", self.copilot,
                      "Missing 'MANDATORY overrides' language in directive section")

    def test_case_insensitive_rule(self):
        """Case-insensitivity must be declared."""
        self.assertIn("case-insensitive", self.copilot.lower(),
                      "Missing case-insensitivity rule for directives")

    def test_strip_before_processing_rule(self):
        """Directives must be stripped before query processing."""
        self.assertIn("STRIPPED", self.copilot,
                      "Missing directive stripping rule")

    def test_override_step_0_language(self):
        """The /fw directive must explicitly declare Step 0 override."""
        self.assertIn("OVERRIDE Step 0", self.copilot,
                      "Missing 'OVERRIDE Step 0' language for /fw directive")

    def test_agents_references_copilot_spec(self):
        """AGENTS.md directive section should reference copilot-instructions.md for full spec."""
        sec841 = re.search(r"### 8\.4\.1.*?(?=### 8\.5)", self.agents, re.DOTALL)
        self.assertIsNotNone(sec841, "§8.4.1 not found in AGENTS.md")
        self.assertIn("copilot-instructions.md", sec841.group(0),
                      "AGENTS.md §8.4.1 should reference copilot-instructions.md for full spec")

    def test_hyphenated_aliases_in_copilot(self):
        """copilot-instructions.md must include hyphenated aliases for /fw-p1..p4 and /fw-check."""
        for alias in ["/fw-p1", "/fw-p2", "/fw-p3", "/fw-p4", "/fw-check"]:
            self.assertIn(alias, self.copilot,
                          f"Hyphenated alias '{alias}' not found in copilot-instructions.md")

    def test_hyphenated_aliases_in_agents(self):
        """AGENTS.md must document hyphenated directive forms."""
        # AGENTS.md §8.4.1 uses pattern notation /fw-p[n] and examples like /fw-p2,
        # plus explicitly states 'Hyphenated forms are equivalent to spaced forms'.
        self.assertIn("/fw-p[", self.agents,
                      "AGENTS.md must document hyphenated pattern /fw-p[n]")
        self.assertIn("Hyphenated forms", self.agents,
                      "AGENTS.md must state hyphenated equivalence rule")
        self.assertIn("/fw check", self.agents,
                      "/fw check must appear in AGENTS.md")

    def test_dual_mechanism_documented(self):
        """Both files must describe the dual mechanism (prompt files + inline fallback)."""
        self.assertIn(".github/prompts/", self.copilot,
                      "copilot-instructions.md must reference .github/prompts/")
        self.assertIn(".github/prompts/", self.agents,
                      "AGENTS.md must reference .github/prompts/")


class TestPromptFiles(unittest.TestCase):
    """Verify .github/prompts/ files exist and are well-formed."""

    PROMPTS_DIR = ROOT / ".github" / "prompts"
    # Per §8.4.1: only /fw has a VS Code prompt file.
    # All other directives (/fw P[n], /fw-p[n], /audit, /disclose, /fw check)
    # are inline-only — detected by the LLM in message text, no prompt file needed.
    EXPECTED_FILES = {
        "fw.prompt.md": "/fw",
    }

    def test_prompts_directory_exists(self):
        """The .github/prompts/ directory must exist."""
        self.assertTrue(self.PROMPTS_DIR.is_dir(),
                        f"Missing directory: {self.PROMPTS_DIR}")

    def test_all_prompt_files_exist(self):
        """All 8 prompt files must exist."""
        for filename in self.EXPECTED_FILES:
            path = self.PROMPTS_DIR / filename
            self.assertTrue(path.is_file(),
                            f"Missing prompt file: {filename}")

    def test_prompt_files_have_frontmatter(self):
        """Each prompt file must have YAML frontmatter with description."""
        for filename in self.EXPECTED_FILES:
            path = self.PROMPTS_DIR / filename
            if path.is_file():
                content = path.read_text(encoding="utf-8").lstrip("\ufeff")
                self.assertTrue(content.startswith("---"),
                                f"{filename} must start with YAML frontmatter '---'")
                self.assertIn("description:", content,
                              f"{filename} frontmatter must contain 'description:'")

    def test_prompt_files_contain_directive_keyword(self):
        """Each prompt file must contain its corresponding DIRECTIVE keyword."""
        for filename in self.EXPECTED_FILES:
            path = self.PROMPTS_DIR / filename
            if path.is_file():
                content = path.read_text(encoding="utf-8")
                self.assertIn("DIRECTIVE", content,
                              f"{filename} must contain the word 'DIRECTIVE'")

    def test_prompt_files_mention_ax57(self):
        """fw/fw-p[1-4] prompt files must mention AX57 disclosure."""
        ax57_files = ["fw.prompt.md", "fw-p1.prompt.md", "fw-p2.prompt.md",
                      "fw-p3.prompt.md", "fw-p4.prompt.md"]
        for filename in ax57_files:
            path = self.PROMPTS_DIR / filename
            if path.is_file():
                content = path.read_text(encoding="utf-8")
                self.assertIn("AX57", content,
                              f"{filename} must reference AX57 disclosure")

    def test_prompt_file_count(self):
        """There must be exactly 2 prompt files: fw.prompt.md (pipeline) and fw-tools.prompt.md (companion)."""
        if self.PROMPTS_DIR.is_dir():
            files = sorted(f.name for f in self.PROMPTS_DIR.glob("*.prompt.md"))
            self.assertEqual(len(files), 2,
                             f"Expected 2 prompt files, found {len(files)}: {files}")
            self.assertIn("fw.prompt.md", files)
            self.assertIn("fw-tools.prompt.md", files)


# ===========================================================================
# Entry point
# ===========================================================================
if __name__ == "__main__":
    unittest.main(verbosity=2)
