"""
Structural consistency tests for DUSUN.md and framework instruction files.

Tests verify internal cross-references, not semantic content.
v3.0: Tests updated for field-effect architecture (dusun-field.instructions.md).
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 / "DUSUN.md"
FIELD_INSTRUCTIONS = ROOT / ".github" / "instructions" / "dusun-field.instructions.md"
FW_PROMPT = ROOT / ".github" / "prompts" / "dusun.prompt.md"
SELF_CHECK = ROOT / ".github" / "prompts" / "dusun-check.prompt.md"

# Legacy alias for backward compat with unchanged test classes
COPILOT = FIELD_INSTRUCTIONS


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 DUSUN.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 dusun-router.instructions.md is consistent with DUSUN.md."""

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

    # -- Term table lives in DUSUN.md §8.2 (İlim). Field instructions don't
    #    duplicate it; we verify the canonical table is present.
    def test_term_table_in_agents(self):
        """DUSUN.md §8.2 must contain a term translation table with ≥20 terms."""
        sec = re.search(r"### 8\.2 Term Translation.*?(?=### 8\.3)", self.agents, re.DOTALL)
        self.assertIsNotNone(sec, "DUSUN.md §8.2 term table not found")
        rows = re.findall(r"^\|\s*(.+?)\s*\|", sec.group(0), re.MULTILINE)
        count = len([r for r in rows if r.strip() not in ("Internal Term", "") and not r.startswith("---")])
        self.assertGreaterEqual(count, 20, f"Expected ≥20 terms, found {count}")

    def test_kavaid_referenced_in_field_or_prompt(self):
        """KV₄ must appear in the field instructions (composite breach rule)."""
        self.assertIn("KV₄", self.copilot,
                      "Field instructions must reference KV₄ (composite breach)")

    def test_five_reasoning_paths_present(self):
        """P0 through P4 must all appear in the field instructions."""
        for p in ["P0", "P1", "P2", "P3", "P4"]:
            self.assertIn(p, self.copilot,
                          f"Reasoning path {p} not found in field instructions")

    def test_shaping_directives_keys_documented(self):
        """Field instructions must document the shaping_directives dict keys."""
        expected = ["path_shape", "grade_ceiling", "pattern_directives",
                    "constraint_directives", "anomaly_directives"]
        for key in expected:
            self.assertIn(key, self.copilot,
                          f"Key '{key}' not documented in field instructions")

    def test_no_skip_framework_escape(self):
        """Field instructions must NOT contain 'Answer normally without engaging'."""
        self.assertNotIn("Answer normally without engaging", self.copilot,
                         "Found outdated 'skip framework' escape hatch")

    def test_code_is_the_router(self):
        """Field instructions must declare 'code IS the router'."""
        self.assertIn("code IS the router", self.copilot,
                      "Missing 'code IS the router' core principle")


# ===========================================================================
# 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.
    v3.0: Field instructions mirror key DUSUN.md concepts structurally:
        paths, grades, constraints, composite score, coverage bound.
    """

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

    def test_field_mirrors_path_concept(self):
        """Both DUSUN.md and field instructions must reference P0–P4 paths."""
        for doc_name, text in [("DUSUN.md", self.agents), ("field instructions", self.copilot)]:
            for p in ["P0", "P1", "P4"]:
                self.assertIn(p, text, f"{doc_name} must reference {p}")

    def test_field_mirrors_grade_concept(self):
        """Both documents must reference epistemic grade ceiling."""
        self.assertIn("grade_ceiling", self.copilot,
                      "Field must reference grade_ceiling")
        self.assertIn("AX56", self.copilot,
                      "Field must reference AX56 (grade boundary)")

    def test_field_mirrors_coverage_bound(self):
        """Field instructions must mirror the 6/7 coverage bound from DUSUN.md."""
        self.assertIn("6/7", self.copilot,
                      "Field must reference 6/7 coverage bound")

    def test_structural_pattern_in_agents(self):
        """DUSUN.md must contain the Structural Pattern Translation table (§8.3)."""
        self.assertIn("Structural Pattern Translation", self.agents,
                      "DUSUN.md must contain Structural Pattern Translation table")


# ===========================================================================
# 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.

    v3.0 triad (AX2-AX4 actualization):
      DUSUN.md         = İlim (Distinguishing) — owns definitions
      dusun-field.instructions.md = İrade (Specifying) — field-effect routing
      dusun.prompt.md  = Kudret (Effecting) — owns execution
    DUSUN.md declares the triad; the other files do their jobs without
    re-declaring role names (the code IS the router)."""

    FW_PROMPT = ROOT / ".github" / "prompts" / "dusun.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 ""

    # -- DUSUN.md declares the triad -----------------------------------------
    def test_agents_declares_ilim_role(self):
        """DUSUN.md must declare itself as İlim organ of the triad."""
        self.assertIn("İlim", self.agents)

    def test_agents_declares_triad_line(self):
        """DUSUN.md metadata must contain a Triad line."""
        self.assertIn("Triad:", self.agents)

    def test_agents_references_field(self):
        """DUSUN.md must reference dusun-field.instructions.md (İrade)."""
        self.assertIn("dusun-field.instructions.md", self.agents)

    def test_agents_references_fw(self):
        """DUSUN.md must reference dusun.prompt.md (Kudret)."""
        self.assertIn("dusun.prompt.md", self.agents)

    # -- Cross-references between prompt and field ---------------------------
    def test_fw_references_field(self):
        """dusun.prompt.md must reference dusun-field.instructions.md."""
        if not self.fw:
            self.skipTest("dusun.prompt.md not found")
        self.assertIn("dusun-field.instructions.md", self.fw,
                      "Prompt must reference field instructions")

    def test_field_declares_replaces(self):
        """Field instructions must state what they replace (old router + engagement)."""
        self.assertIn("Replaces", self.copilot,
                      "Field must document its provenance")

    def test_field_shaping_directives_core(self):
        """Field instructions must document shaping_directives as the key mechanism."""
        self.assertIn("shaping_directives", self.copilot)

    def test_no_duplicate_term_table_in_field(self):
        """Field instructions must NOT duplicate the term table from DUSUN.md §8.2."""
        self.assertNotIn("Term Translation", self.copilot,
                         "Field must not duplicate DUSUN.md §8.2 term table")


# ===========================================================================
# TEST CLASS 13: Field-Effect Enforcement
# ===========================================================================
class TestFieldEffectEnforcement(unittest.TestCase):
    """Verify the field-effect architecture is structurally complete.

    In v3.0 the code IS the router — these tests ensure the field
    instructions, prompt, and DUSUN.md together make framework
    engagement mandatory and verifiable."""

    @classmethod
    def setUpClass(cls):
        cls.field = _read(FIELD_INSTRUCTIONS)
        cls.prompt = _read(FW_PROMPT)
        cls.agents = _read(AGENTS)

    # --- Field instructions core structure ---

    def test_core_rule_exists(self):
        """dusun-field.instructions.md must define a Core Rule section."""
        self.assertIn("## Core Rule", self.field)

    def test_field_references_all_paths(self):
        """Field instructions must reference all 5 paths (P0–P4)."""
        for p in ["P0", "P1", "P2", "P3", "P4"]:
            self.assertIn(p, self.field,
                          f"Path {p} not in dusun-field.instructions.md")

    def test_shaping_directives_section(self):
        """Field instructions must define the shaping_directives dict keys."""
        self.assertIn("shaping_directives", self.field)
        for key in ["path_shape", "path_guidance", "grade_ceiling",
                     "pattern_directives", "constraint_directives",
                     "anomaly_directives", "convergence_directives",
                     "sentinel_directives", "response_bounds", "lens_coverage"]:
            self.assertIn(key, self.field,
                          f"Shaping directive key '{key}' not documented")

    def test_anti_patterns_section(self):
        """Field instructions must have a 'What NOT to Do' section."""
        self.assertIn("What NOT to Do", self.field)

    def test_grade_ceiling_enforcement(self):
        """Field instructions must declare grade as a ceiling."""
        low = self.field.lower()
        self.assertIn("ceiling", low,
                      "Missing grade ceiling enforcement language")

    # --- Prompt file structure ---

    def test_prompt_single_entry_point(self):
        """dusun.prompt.md must define the /dusun single entry point."""
        self.assertIn("/dusun", self.prompt)
        self.assertIn("dusun(text=", self.prompt)

    def test_prompt_constraints_always_active(self):
        """dusun.prompt.md must list the 5 always-active constraints."""
        for keyword in ["ceiling", "Anomalies", "6/7", "Composite", "Map"]:
            self.assertIn(keyword, self.prompt,
                          f"Always-active constraint keyword '{keyword}' missing from prompt")

    # --- DUSUN.md integration ---

    def test_disclosure_format_in_agents_md(self):
        """DUSUN.md must contain operationalized disclosure format."""
        self.assertIn("Operationalized disclosure format", self.agents)

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

    def test_grade_values_in_agents_md(self):
        """DUSUN.md disclosure format must specify grade values."""
        self.assertIn("Tasavvur", self.agents)
        self.assertIn("Tasdik", self.agents)
        self.assertTrue(
            "İlmelyakîn" in self.agents or "Ilmelyakin" in self.agents,
            "Missing İlmelyakîn grade in DUSUN.md")


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

    In v3.0 the /dusun directive is a VS Code slash command registered
    by dusun.prompt.md.  Other directives are inline text detected by
    the LLM.  DUSUN.md §8.4.1 is the canonical specification."""

    @classmethod
    def setUpClass(cls):
        cls.prompt = _read(FW_PROMPT)
        cls.agents = _read(AGENTS)

    def test_directive_section_exists_agents(self):
        """DUSUN.md must have §8.4.1 User Directives."""
        self.assertIn("### 8.4.1 User Directives", self.agents)

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

    def test_mandatory_override_language(self):
        """Directives must be described as MANDATORY overrides."""
        self.assertIn("MANDATORY overrides", self.agents)

    def test_case_insensitive_rule(self):
        """Case-insensitivity must be declared in DUSUN.md."""
        self.assertIn("Case-insensitive", self.agents)

    def test_hyphenated_aliases_in_agents(self):
        """DUSUN.md must document hyphenated directive forms."""
        self.assertIn("/dusun-p[", self.agents)
        self.assertIn("Hyphenated forms", self.agents)

    def test_prompt_defines_dusun_command(self):
        """dusun.prompt.md must register /dusun as its directive."""
        self.assertIn("/dusun", self.prompt)

    def test_prompt_references_field_instructions(self):
        """dusun.prompt.md must reference dusun-field.instructions.md."""
        self.assertIn("dusun-field.instructions.md", self.prompt)

    def test_agents_references_prompts_dir(self):
        """DUSUN.md must reference .github/prompts/ for delivery mechanism."""
        self.assertIn(".github/prompts/", self.agents)

    def test_directive_table_in_agents(self):
        """DUSUN.md §8.4.1 must contain a directive table."""
        sec = re.search(r"### 8\.4\.1.*?(?=### 8\.5)", self.agents, re.DOTALL)
        self.assertIsNotNone(sec, "§8.4.1 not found")
        self.assertIn("| Directive", sec.group(0),
                      "Directive table not found in §8.4.1")


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

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

    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):
        """Prompt files should mention AX58 (map≠territory) if present."""
        for filename in self.EXPECTED_FILES:
            path = self.PROMPTS_DIR / filename
            if path.is_file():
                content = path.read_text(encoding="utf-8")
                self.assertIn("AX58", content,
                              f"{filename} must reference AX58 (map≠territory)")

    def test_prompt_file_count(self):
        """There must be exactly 2 prompt files: dusun.prompt.md, dusun-check.prompt.md."""
        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("dusun.prompt.md", files)
            self.assertIn("dusun-check.prompt.md", files)


class TestShapingDirectivesDocumentation(unittest.TestCase):
    """Verify shaping_directives documentation exists in field instructions.

    v3.0: Replaces TestConstitutionalSelfCritique. The old SC-1..SC-4
    self-critique gate is replaced by shaping_directives (emergent from code).
    """

    @classmethod
    def setUpClass(cls):
        cls.field = _read(FIELD_INSTRUCTIONS)
        cls.prompt = _read(FW_PROMPT)

    def test_shaping_directives_documented_in_field(self):
        """dusun-field.instructions.md must document shaping_directives."""
        self.assertIn("shaping_directives", self.field)

    def test_grade_ceiling_documented(self):
        """grade_ceiling must be explicitly documented."""
        self.assertIn("grade_ceiling", self.field)
        self.assertIn("grade_ceiling", self.prompt)

    def test_anomaly_directives_documented(self):
        """anomaly_directives must be mentioned."""
        self.assertIn("anomaly_directives", self.field)

    def test_what_not_to_do_section(self):
        """Anti-patterns must be documented."""
        self.assertIn("NOT", self.field)

    def test_path_shape_documented(self):
        """path_shape must be documented in field instructions."""
        self.assertIn("path_shape", self.field)

    def test_response_bounds_documented(self):
        """response_bounds must be documented."""
        self.assertIn("response_bounds", self.field)


class TestSelfCheckPrompt(unittest.TestCase):
    """Verify dusun-check.prompt.md is well-formed."""

    PROMPT_PATH = ROOT / ".github" / "prompts" / "dusun-check.prompt.md"

    def test_file_exists(self):
        self.assertTrue(self.PROMPT_PATH.is_file(),
                        "dusun-check.prompt.md must exist in .github/prompts/")

    def test_frontmatter(self):
        content = self.PROMPT_PATH.read_text(encoding="utf-8").lstrip("\ufeff")
        self.assertTrue(content.startswith("---"),
                        "dusun-check.prompt.md must start with YAML frontmatter")
        self.assertIn("description:", content,
                      "dusun-check.prompt.md must have a description in frontmatter")

    def test_references_check_kavaid(self):
        content = self.PROMPT_PATH.read_text(encoding="utf-8")
        self.assertIn("check_kavaid", content,
                      "dusun-check.prompt.md must invoke check_kavaid tool")

    def test_references_shaping_directives(self):
        content = self.PROMPT_PATH.read_text(encoding="utf-8")
        self.assertIn("shaping_directives", content,
                      "dusun-check.prompt.md must reference shaping_directives")


class TestInstructionsFiles(unittest.TestCase):
    """Verify .github/instructions/ directory and dusun-field.instructions.md."""

    INST_DIR = ROOT / ".github" / "instructions"
    INST_FILE = INST_DIR / "dusun-field.instructions.md"

    def test_directory_exists(self):
        self.assertTrue(self.INST_DIR.is_dir(),
                        ".github/instructions/ directory must exist")

    def test_file_exists(self):
        self.assertTrue(self.INST_FILE.is_file(),
                        "dusun-field.instructions.md must exist")

    def test_frontmatter_apply_to_all(self):
        content = self.INST_FILE.read_text(encoding="utf-8").lstrip("\ufeff")
        self.assertTrue(content.startswith("---"),
                        "dusun-field.instructions.md must have YAML frontmatter")
        self.assertIn("applyTo:", content,
                      "Frontmatter must contain applyTo")

    def test_ax5_reference(self):
        """Field instructions should reference AX56 (grade ceiling)."""
        content = self.INST_FILE.read_text(encoding="utf-8")
        self.assertIn("AX56", content,
                      "dusun-field.instructions.md must reference AX56")

    def test_structural_bounds(self):
        content = self.INST_FILE.read_text(encoding="utf-8")
        for bound in ("6/7", "KV"):
            self.assertIn(bound, content,
                          f"dusun-field.instructions.md must reference {bound}")


class TestFrameworkFileSync(unittest.TestCase):
    """All framework files must be identical between .github/ and fw_server/data/."""

    DATA_DIR = ROOT / "fw_server" / "data"

    PAIRS = [
        (".github/prompts/dusun.prompt.md", "dusun.prompt.md"),
        (".github/prompts/dusun-check.prompt.md", "dusun-check.prompt.md"),
        (".github/instructions/dusun-field.instructions.md",
         "dusun-field.instructions.md"),
        ("DUSUN.md", "DUSUN.md"),
    ]

    def test_all_pairs_in_sync(self):
        """Every .github file must be byte-identical to its fw_server/data/ counterpart."""
        for dest_rel, src_name in self.PAIRS:
            dest = ROOT / dest_rel
            src = self.DATA_DIR / src_name
            with self.subTest(pair=dest_rel):
                self.assertTrue(dest.is_file(), f"Missing: {dest_rel}")
                self.assertTrue(src.is_file(), f"Missing: fw_server/data/{src_name}")
                self.assertEqual(
                    dest.read_bytes(), src.read_bytes(),
                    f"MISMATCH: {dest_rel} != fw_server/data/{src_name}"
                )


class TestInitDeployment(unittest.TestCase):
    """Verify _FRAMEWORK_FILES in server.py is consistent with actual data files."""

    def test_framework_files_count(self):
        """_FRAMEWORK_FILES must declare exactly 4 entries."""
        server_py = (ROOT / "fw_server" / "server.py").read_text(encoding="utf-8")
        # Filter to only those inside _FRAMEWORK_FILES block
        in_block = False
        count = 0
        for line in server_py.splitlines():
            if "_FRAMEWORK_FILES" in line and "=" in line:
                in_block = True
                continue
            if in_block and line.strip() == "}":
                break
            if in_block and '": "' in line:
                count += 1
        self.assertEqual(count, 4,
                         f"_FRAMEWORK_FILES must have 4 entries, found {count}")

    def test_all_source_files_exist(self):
        """Every source file referenced in _FRAMEWORK_FILES must exist in fw_server/data/."""
        data_dir = ROOT / "fw_server" / "data"
        expected = [
            "dusun.prompt.md", "dusun-check.prompt.md",
            "dusun-field.instructions.md",
            "DUSUN.md",
        ]
        for name in expected:
            path = data_dir / name
            self.assertTrue(path.is_file(),
                            f"Missing source file: fw_server/data/{name}")


class TestVersionMetadata(unittest.TestCase):
    """Verify version consistency across pyproject.toml and DUSUN.md."""

    def test_pyproject_version(self):
        content = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
        self.assertIn('version = "3.0.2"', content,
                      "pyproject.toml must declare version 3.0.2")

    def test_agents_version(self):
        content = _read(AGENTS)
        self.assertIn("Version:       2.2.0", content,
                      "DUSUN.md metadata must declare Version: 2.2.0")

    def test_agents_hayat_layer(self):
        content = _read(AGENTS)
        self.assertIn("Hayat layer:", content,
                      "DUSUN.md metadata must include Hayat layer line")


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