"""
tests/test_hayat_integration.py — Hayat Integration Layer Validation

/dusun Substrate BUILD-mode test scenario validating v3.0.0 Hayat layer:
  - dusun-field.instructions.md (always-on shaping directives)
  - dusun.prompt.md (universal substrate entry point)
  - dusun-check.prompt.md (manual /self-check diagnostic)

Design Principles:
  AX5:  Hayat is the integration prerequisite — without it, all other
        attributes become inert.  These tests verify the triad is ALIVE.
  AX2→AX3→AX4: Three-phase actualization — each test class represents
        Distinction → Selection → Actualization of a structural claim.
  KV₇:  Tests are independent — no shared state between classes or methods.
  AX52: Multiplicative gate — zero in ANY dimension = total failure.
  T17:  Coverage ≤ 6/7 — we acknowledge one dimension is always unmapped.
  AX56: Max grade = İlmelyakîn — never claim Hakkalyakîn.
  KV₄:  0 < composite < 1 — convergence is always bounded.

Usage:
    python -m pytest tests/test_hayat_integration.py -v
    python tests/test_hayat_integration.py   # standalone
"""

import json
import re
import unittest
from pathlib import Path

# ---------------------------------------------------------------------------
# Paths (KV₇ — independent of other test modules' path definitions)
# ---------------------------------------------------------------------------
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"


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


# ═══════════════════════════════════════════════════════════════════════
# CLASS 1: Hayat Layer File Existence (AX5 — integration prerequisite)
# ═══════════════════════════════════════════════════════════════════════


class TestHayatLayerExists(unittest.TestCase):
    """All three Hayat integration components must exist.

    AX5: ¬Hayat → ∀s' ∈ S_Sifat\\{Hayat}: Inert(s')
    If ANY component is missing, the integration layer is dead.
    AX52: Multiplicative gate — zero in one dimension = collapse.
    """

    def test_field_instructions_exist(self):
        """dusun-field.instructions.md (always-on shaping directives) must exist."""
        self.assertTrue(
            FIELD_INSTRUCTIONS.exists(),
            f"Missing: {FIELD_INSTRUCTIONS.relative_to(ROOT)}",
        )

    def test_self_check_prompt_exists(self):
        """dusun-check.prompt.md must exist on disk."""
        self.assertTrue(
            SELF_CHECK.exists(),
            f"Missing: {SELF_CHECK.relative_to(ROOT)}",
        )

    def test_fw_prompt_exists(self):
        """dusun.prompt.md (universal substrate entry point) must exist on disk."""
        self.assertTrue(
            FW_PROMPT.exists(),
            f"Missing: {FW_PROMPT.relative_to(ROOT)}",
        )

    def test_triad_completeness(self):
        """Document triad İlim + İrade + Kudret must all exist (AX5)."""
        for path, label in [
            (AGENTS, "İlim (DUSUN.md)"),
            (FIELD_INSTRUCTIONS, "İrade (dusun-field.instructions.md)"),
            (FW_PROMPT, "Kudret (dusun.prompt.md)"),
        ]:
            with self.subTest(label=label):
                self.assertTrue(path.exists(), f"Triad broken: {label}")


# ═══════════════════════════════════════════════════════════════════════
# CLASS 2: Field Instructions — Always-On Shaping Directives (AX5)
# ═══════════════════════════════════════════════════════════════════════


class TestFieldInstructionsContent(unittest.TestCase):
    """dusun-field.instructions.md must enforce AX5 always-on.

    This component turns the Hayat principle from a document-level claim
    into an always-injected context that every VS Code chat receives.
    v3.0: The code IS the router — Copilot follows computed output.
    """

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

    def test_apply_to_all(self):
        """Must have applyTo: '**' for universal injection."""
        self.assertRegex(self.text, r"applyTo:\s*['\"]?\*\*['\"]?")

    def test_shaping_directives_documented(self):
        """Must document the shaping_directives dict keys."""
        for key in ["path_shape", "path_guidance", "grade_ceiling",
                     "pattern_directives", "constraint_directives",
                     "anomaly_directives", "response_bounds", "lens_coverage"]:
            with self.subTest(key=key):
                self.assertIn(key, self.text)

    def test_grade_ceiling_rule(self):
        """Must declare grade as a ceiling, not a floor."""
        self.assertIn("ceiling", self.text.lower())

    def test_path_classification_reference(self):
        """Must reference P0–P4 paths."""
        for p in ["P0", "P1", "P2", "P3", "P4"]:
            with self.subTest(path=p):
                self.assertIn(p, self.text)

    def test_structural_bounds_present(self):
        """Must declare key structural bounds."""
        # Coverage ≤ 6/7
        self.assertIn("6/7", self.text)
        # Grade ceiling
        self.assertIn("AX56", self.text)
        # Composite bounded
        self.assertRegex(self.text, r"KV|0\.95")
        # Map ≠ territory
        self.assertIn("AX58", self.text)

    def test_core_rule_present(self):
        """Must state the core rule: response shaped by dusun() output."""
        self.assertIn("shaping_directives", self.text)
        self.assertIn("dusun()", self.text)

    def test_what_not_to_do_present(self):
        """Must list anti-patterns (what NOT to do)."""
        lower = self.text.lower()
        self.assertIn("do not", lower)


# ═══════════════════════════════════════════════════════════════════════
# CLASS 3: Dusun Prompt Content (Universal Substrate Entry Point)
# ═══════════════════════════════════════════════════════════════════════


class TestDusunPromptContent(unittest.TestCase):
    """dusun.prompt.md must define the /dusun invocation.

    v3.0: Single dusun() call replaces old 9-stage pipeline.
    The prompt must reference shaping_directives and key output fields.
    """

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

    def test_dusun_directive(self):
        """/dusun must be the main directive."""
        self.assertIn("/dusun", self.text)

    def test_dusun_tool_call(self):
        """Must show dusun() as the tool to call."""
        self.assertIn("dusun(", self.text)

    def test_shaping_directives_in_output(self):
        """Must list shaping_directives as a key output field."""
        self.assertIn("shaping_directives", self.text)

    def test_grade_ceiling_constraint(self):
        """Must state grade is a ceiling."""
        self.assertIn("ceiling", self.text.lower())

    def test_composite_bound(self):
        """Must state 0 < Composite < 1."""
        self.assertRegex(self.text, r"0 < .* < 1|0\.95|KV")

    def test_path_classification(self):
        """Must reference P0–P4."""
        for p in ["P0", "P1", "P2", "P3", "P4"]:
            with self.subTest(path=p):
                self.assertIn(p, self.text)


# ═══════════════════════════════════════════════════════════════════════
# CLASS 4: MCP Tool — bileshke Pipeline (T6, KV₄, AX56, AX57)
# ═══════════════════════════════════════════════════════════════════════


class TestBileshkePipelineHayat(unittest.TestCase):
    """Exercise run_bileshke_pipeline as integration test.

    Validates:
      T6:  Convergence < 1.0
      KV₄: Composite ∈ (0, 1) — flag ≥ 0.95 as error
      AX56: Grade ≤ İlmelyakîn
      AX57: Disclosure block present
      T17: Completeness ≤ 6/7
      AX52: Multiplicative gate
    """

    @classmethod
    def setUpClass(cls):
        from fw_server.server import run_bileshke_pipeline
        # Supply meaningful text so lenses produce non-trivial scores.
        # Without content every lens correctly returns 0.0 — that is correct
        # behaviour but this test validates the T6/KV₄ bounds on *real* analysis.
        sample_text = (
            "Tecelli of Esmâ through Hayat demonstrates holographic "
            "structure. AX1 AX17 T6 KV4 convergence functor category."
        )
        lens_params = {
            lid: {"text": sample_text}
            for lid in (
                "Ontoloji", "Mereoloji", "FOL", "Bayes",
                "OyunTeorisi", "KategoriTeorisi", "Topoloji + Holografik",
            )
        }
        raw = run_bileshke_pipeline(json.dumps(lens_params), None, "[1,1,1]")
        cls.result = json.loads(raw)

    def test_composite_score_bounded(self):
        """T6 + KV₄: 0 < composite < 1.0."""
        if "error" in self.result:
            self.skipTest(f"Pipeline error: {self.result['error']}")
        score = self.result["composite_score"]
        self.assertGreater(score, 0.0, "Composite must be > 0")
        self.assertLess(score, 1.0, "T6: Composite must be < 1.0")

    def test_kv4_threshold_warning(self):
        """KV₄: If composite ≥ 0.95, kv4_status must be FAIL/WARNING."""
        if "error" in self.result:
            self.skipTest(f"Pipeline error: {self.result['error']}")
        score = self.result["composite_score"]
        disclosure = self.result.get("ax57_disclosure", {})
        if score >= 0.95:
            kv4 = disclosure.get("kv4_status", "")
            self.assertFalse(
                kv4.startswith("PASS"),
                f"KV₄ violation: score={score:.4f} but kv4_status={kv4}",
            )

    def test_ax57_disclosure_block_present(self):
        """AX57: Every pipeline response must carry disclosure."""
        if "error" in self.result:
            self.skipTest(f"Pipeline error: {self.result['error']}")
        disclosure = self.result.get("ax57_disclosure")
        self.assertIsNotNone(disclosure, "ax57_disclosure missing")
        for key in [
            "composite_score", "grade", "completeness",
            "kv4_status", "kv7_status", "ax56_status",
        ]:
            with self.subTest(key=key):
                self.assertIn(
                    key, disclosure,
                    f"ax57_disclosure missing key: {key}",
                )

    def test_ax56_grade_ceiling(self):
        """AX56: Grade must be ≤ İlmelyakîn, never Hakkalyakîn."""
        if "error" in self.result:
            self.skipTest(f"Pipeline error: {self.result['error']}")
        disclosure = self.result.get("ax57_disclosure", {})
        grade = disclosure.get("grade", "")
        self.assertNotIn("Hakkalyakîn", grade, "AX56 violation")

    def test_t17_completeness_bound(self):
        """T17: Completeness ≤ 6/7 ≈ 0.857."""
        if "error" in self.result:
            self.skipTest(f"Pipeline error: {self.result['error']}")
        disclosure = self.result.get("ax57_disclosure", {})
        completeness = disclosure.get("completeness", "")
        # completeness is formatted as "X/7" or a float
        if isinstance(completeness, str) and "/" in completeness:
            num, denom = completeness.split("/")
            ratio = float(num) / float(denom)
        elif isinstance(completeness, (int, float)):
            ratio = float(completeness)
        else:
            ratio = 0.0
        self.assertLessEqual(
            ratio, 6 / 7 + 0.001,
            f"T17: Completeness {completeness} exceeds 6/7",
        )

    def test_all_seven_lenses_produce_results(self):
        """T16 + AX50: All 7 lenses must produce results for full coverage."""
        if "error" in self.result:
            self.skipTest(f"Pipeline error: {self.result['error']}")
        lens_results = self.result.get("lens_results", [])
        self.assertEqual(
            len(lens_results), 7,
            f"Expected 7 lens results, got {len(lens_results)}",
        )

    def test_each_lens_score_below_one(self):
        """T6: Every individual lens score < 1.0."""
        if "error" in self.result:
            self.skipTest(f"Pipeline error: {self.result['error']}")
        for lr in self.result.get("lens_results", []):
            lid = lr.get("lens_id", "?")
            score = lr.get("score", 0.0)
            with self.subTest(lens=lid):
                self.assertLess(
                    score, 1.0,
                    f"T6: {lid} score={score} ≥ 1.0",
                )


# ═══════════════════════════════════════════════════════════════════════
# CLASS 5: MCP Tool — check_kavaid (KV₁–KV₈, AX52)
# ═══════════════════════════════════════════════════════════════════════


class TestKavaidEvaluation(unittest.TestCase):
    """Exercise check_kavaid tool with standard parameters.

    Validates:
      All 8 kavaid are evaluated (KV₁ through KV₈)
      Descriptions present for each
      KV₄ bound enforced at 0.95 threshold
      Multiplicative nature (AX52) — all_pass requires ALL 8
    """

    @classmethod
    def setUpClass(cls):
        from fw_server.server import check_kavaid
        # Standard parameters: composite=0.5, 6 of 7 latife active (Ahfâ=0)
        raw = check_kavaid(0.5, "[1,1,1,1,1,1,0]", None)
        cls.result = json.loads(raw)

    def test_all_eight_kavaid_present(self):
        """All 8 kavaid (KV1–KV8) must be evaluated."""
        if "error" in self.result:
            self.skipTest(f"Kavaid error: {self.result['error']}")
        descs = self.result.get("descriptions", {})
        for kv in ["KV1", "KV2", "KV3", "KV4", "KV5", "KV6", "KV7", "KV8"]:
            with self.subTest(kv=kv):
                self.assertIn(kv, descs, f"Missing kavaid: {kv}")

    def test_kv4_safe_at_half(self):
        """KV₄: composite=0.5 is safely below 0.95 threshold."""
        if "error" in self.result:
            self.skipTest(f"Kavaid error: {self.result['error']}")
        checks = self.result.get("checks", {})
        self.assertTrue(
            checks.get("KV4", False),
            "KV₄ should pass at composite=0.5",
        )

    def test_kv4_warns_at_threshold(self):
        """KV₄: composite=0.96 must trigger warning."""
        from fw_server.server import check_kavaid
        raw = check_kavaid(0.96, "[1,1,1,1,1,1,0]", None)
        result = json.loads(raw)
        if "error" in result:
            self.skipTest(f"Kavaid error: {result['error']}")
        kv4_warning = result.get("kv4_warning")
        self.assertIsNotNone(
            kv4_warning,
            "KV₄ must issue warning at composite=0.96",
        )

    def test_all_pass_is_multiplicative(self):
        """AX52: all_pass is True only if ALL 8 kavaid pass."""
        if "error" in self.result:
            self.skipTest(f"Kavaid error: {self.result['error']}")
        checks = self.result.get("checks", {})
        all_pass = self.result.get("all_pass", False)
        individual_all = all(checks.values())
        self.assertEqual(
            all_pass, individual_all,
            "AX52: all_pass must equal AND of all individual checks",
        )

    def test_kavaid_descriptions_non_empty(self):
        """Each kavaid must have a non-empty description."""
        if "error" in self.result:
            self.skipTest(f"Kavaid error: {self.result['error']}")
        descs = self.result.get("descriptions", {})
        for kv, desc in descs.items():
            with self.subTest(kv=kv):
                self.assertTrue(
                    len(desc) > 5,
                    f"{kv} description too short: '{desc}'",
                )


# ═══════════════════════════════════════════════════════════════════════
# CLASS 6: MCP Tool — verify_chains (C1–C7, IG.2, IG.3)
# ═══════════════════════════════════════════════════════════════════════


class TestChainVerification(unittest.TestCase):
    """Exercise verify_chains tool for all 7 inference chains.

    Validates:
      C1–C7 all present and valid
      Hub nodes identified (KV₄ in 3 chains per IG.3)
      Kaskad verification integration
    """

    @classmethod
    def setUpClass(cls):
        from fw_server.server import verify_chains
        raw = verify_chains(None, None)
        cls.result = json.loads(raw)

    def test_all_seven_chains_present(self):
        """C1 through C7 must all be present."""
        if "error" in self.result:
            self.skipTest(f"Chain error: {self.result['error']}")
        chains = self.result.get("chains", {})
        for c in ["C1", "C2", "C3", "C4", "C5", "C6", "C7"]:
            with self.subTest(chain=c):
                self.assertIn(c, chains, f"Missing chain: {c}")

    def test_all_chains_valid(self):
        """Every chain must be structurally valid."""
        if "error" in self.result:
            self.skipTest(f"Chain error: {self.result['error']}")
        self.assertTrue(
            self.result.get("all_valid", False),
            "Not all chains are valid",
        )

    def test_hub_nodes_identified(self):
        """Hub nodes must be identified per IG.3."""
        if "error" in self.result:
            self.skipTest(f"Chain error: {self.result['error']}")
        hubs = self.result.get("hub_nodes", [])
        self.assertGreater(
            len(hubs), 0,
            "Hub nodes must be identified (IG.3)",
        )

    def test_kv4_is_hub(self):
        """KV₄ must be a hub node — it appears in C3, C4, C7 (IG.3)."""
        if "error" in self.result:
            self.skipTest(f"Chain error: {self.result['error']}")
        hubs = self.result.get("hub_nodes", [])
        hub_ids = set()
        for h in hubs:
            if isinstance(h, dict):
                hub_ids.add(h.get("node", ""))
            elif isinstance(h, str):
                hub_ids.add(h)
        # KV₄ or KV4 must be present
        found = any("KV" in hid and "4" in hid for hid in hub_ids)
        self.assertTrue(found, f"KV₄ not found among hubs: {hub_ids}")

    def test_kaskad_verification_present(self):
        """Kaskad verification data must be included."""
        if "error" in self.result:
            self.skipTest(f"Chain error: {self.result['error']}")
        kv = self.result.get("kaskad_verification")
        self.assertIsNotNone(
            kv, "kaskad_verification missing from verify_chains output",
        )


# ═══════════════════════════════════════════════════════════════════════
# CLASS 7: MCP Tool — run_kaskad verify action (AX23)
# ═══════════════════════════════════════════════════════════════════════


class TestKaskadVerify(unittest.TestCase):
    """Exercise run_kaskad with action='verify' for structural integrity.

    AX23: Names form a DAG with root in S_Seun.
    All 7 structural checks must pass.
    """

    @classmethod
    def setUpClass(cls):
        from fw_server.server import run_kaskad
        raw = run_kaskad("verify", None, None, 0)
        cls.result = json.loads(raw)

    def test_action_is_verify(self):
        self.assertEqual(self.result.get("action"), "verify")

    def test_all_checks_pass(self):
        """All structural checks must pass."""
        if "error" in self.result:
            self.skipTest(f"Kaskad error: {self.result['error']}")
        self.assertTrue(
            self.result.get("all_pass", False),
            "Kaskad structural verification failed",
        )

    def test_checks_are_present(self):
        """At least one check was run."""
        checks = self.result.get("checks", [])
        self.assertGreater(
            len(checks), 0,
            "No checks were run in kaskad verify",
        )

    def test_each_check_has_name_and_result(self):
        """Each check has name, passed, message fields."""
        for check in self.result.get("checks", []):
            with self.subTest(check=check.get("name", "?")):
                self.assertIn("name", check)
                self.assertIn("passed", check)
                self.assertIn("message", check)


# ═══════════════════════════════════════════════════════════════════════
# CLASS 8: MCP Tool — get_framework_summary (9 modules)
# ═══════════════════════════════════════════════════════════════════════


class TestFrameworkSummaryIntegration(unittest.TestCase):
    """Exercise get_framework_summary — aggregates all 9 modules.

    Validates multi-module health. Each module providing framework_summary()
    must contribute valid metadata. This is the Hayat integration test
    at the module level — if ANY module fails, integration is broken (AX52).
    """

    @classmethod
    def setUpClass(cls):
        from fw_server.server import get_framework_summary
        raw = get_framework_summary()
        cls.result = json.loads(raw)

    def test_result_is_dict(self):
        self.assertIsInstance(self.result, dict)

    def test_all_nine_modules_present(self):
        """All 9 modules must contribute to framework summary."""
        expected = {
            "kavram_sozlugu", "mereoloji", "bileshke",
            "fol_formalizasyon", "bayes_analiz", "oyun_teorisi",
            "kategori_teorisi", "holografik", "kaskad",
        }
        for mod in expected:
            with self.subTest(module=mod):
                self.assertIn(
                    mod, self.result,
                    f"Module {mod} missing from framework summary",
                )

    def test_no_module_has_error(self):
        """No module should report an error in its summary."""
        for mod, summary in self.result.items():
            with self.subTest(module=mod):
                if isinstance(summary, dict):
                    self.assertNotIn(
                        "error", summary,
                        f"{mod} reported error: {summary.get('error')}",
                    )


# ═══════════════════════════════════════════════════════════════════════
# CLASS 9: MCP Tool — run_single_lens (KV₇, T6, AX56)
# ═══════════════════════════════════════════════════════════════════════


class TestSingleLensIndependence(unittest.TestCase):
    """Exercise run_single_lens for each of the 7 lenses.

    KV₇: Each lens operates independently (no shared state).
    T6:  Each score < 1.0.
    AX56: Grade ≤ İlmelyakîn.
    AX27: Lenses are transparent vessels — they mediate, don't originate.
    """

    LENS_IDS = [
        "Ontoloji", "Mereoloji", "FOL", "Bayes",
        "OyunTeorisi", "KategoriTeorisi", "Topoloji + Holografik",
    ]

    def _run_lens(self, lens_id, params=None):
        from fw_server.server import run_single_lens
        raw = run_single_lens(lens_id, params)
        return json.loads(raw)

    def test_all_seven_lenses_callable(self):
        """Every lens must be callable without error."""
        for lid in self.LENS_IDS:
            with self.subTest(lens=lid):
                result = self._run_lens(lid)
                self.assertNotIn(
                    "error", result,
                    f"{lid} returned error: {result.get('error')}",
                )

    def test_t6_all_scores_below_one(self):
        """T6: Every lens score must be < 1.0."""
        for lid in self.LENS_IDS:
            with self.subTest(lens=lid):
                result = self._run_lens(lid)
                if "error" not in result:
                    self.assertLess(
                        result.get("score", 0.0), 1.0,
                        f"T6: {lid} score ≥ 1.0",
                    )

    def test_ax56_no_hakkalyakin(self):
        """AX56: No lens may return Hakkalyakîn grade."""
        for lid in self.LENS_IDS:
            with self.subTest(lens=lid):
                result = self._run_lens(lid)
                if "error" not in result:
                    grade = result.get("grade", "")
                    self.assertNotIn(
                        "Hakkalyakîn", grade,
                        f"AX56: {lid} returned Hakkalyakîn",
                    )

    def test_kv7_fol_independence(self):
        """KV₇: Two FOL calls with different input must not share state."""
        r1 = self._run_lens(
            "FOL",
            json.dumps({"text": "AX1 states order. T3 propagates perfection."}),
        )
        r2 = self._run_lens("FOL", None)
        # Both must succeed
        if "error" in r1 or "error" in r2:
            self.skipTest("FOL lens error — cannot test independence")
        # They should produce different detail or scores
        # (mere non-crash is already a pass for independence)
        self.assertIsNotNone(r1.get("score"))
        self.assertIsNotNone(r2.get("score"))


# ═══════════════════════════════════════════════════════════════════════
# CLASS 10: Cross-Component Integration (AX5 — Triad Coherence)
# ═══════════════════════════════════════════════════════════════════════


class TestTriadCoherence(unittest.TestCase):
    """Cross-validate the three Hayat components for coherence.

    AX5: Integration prerequisite — components must work TOGETHER.
    T14: NeAynNeGayr — participation without identity.
    AX39: Edeb — methodological propriety.

    v3.0: Triad is field_instructions + dusun_prompt + self_check_prompt.
    All three must reference shaping_directives consistently.
    """

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

    def test_all_three_reference_shaping_directives(self):
        """All 3 Hayat components must reference shaping_directives."""
        self.assertIn("shaping_directives", self.field)
        self.assertIn("shaping_directives", self.prompt)
        self.assertIn("shaping_directives", self.check)

    def test_field_and_prompt_reference_paths(self):
        """Both field instructions and prompt must reference P0–P4."""
        for p in ["P0", "P1", "P2", "P3", "P4"]:
            with self.subTest(path=p):
                self.assertIn(p, self.field)
                self.assertIn(p, self.prompt)

    def test_grade_ceiling_consistent(self):
        """Both field and prompt must declare grade as a ceiling."""
        self.assertIn("ceiling", self.field.lower())
        self.assertIn("ceiling", self.prompt.lower())

    def test_ax5_formula_in_agents(self):
        """AX5 Hayat formula must be in DUSUN.md."""
        self.assertIn("¬Hayat", self.agents)
        self.assertIn("Inert", self.agents)

    def test_check_references_dusun_tool(self):
        """self-check must reference dusun() tool call."""
        self.assertIn("dusun(", self.check)

    def test_field_references_dusun_tool(self):
        """field instructions must reference dusun() tool."""
        self.assertIn("dusun()", self.field)


# ═══════════════════════════════════════════════════════════════════════
# CLASS 11: Structural Incompleteness Assertions (§7.2)
# ═══════════════════════════════════════════════════════════════════════


class TestStructuralIncompleteness(unittest.TestCase):
    """Explicitly validate the four structural incompleteness results.

    These are NOT bugs — they are boundaries to honor (§7.2).
    The test scenario must VERIFY they are enforced, not try to exceed them.
    """

    def test_t17_latife_vektor_max_six(self):
        """T17: latife_vektor maximum is 6 out of 7 (Ahfâ = 0)."""
        from bileshke.engine import compute_latife_vektor
        # ALL 7 lens IDs active
        from bileshke.types import LensId
        all_lenses = list(LensId)
        vektor = compute_latife_vektor(all_lenses)
        # Ahfâ bit (index 6) must be 0
        self.assertEqual(
            vektor.bits[6], 0,
            "T17: Ahfâ (index 6) must be permanently 0",
        )
        # Sum must be ≤ 6
        self.assertLessEqual(
            vektor.active_count, 6,
            "T17: Maximum active latifeler = 6",
        )

    def test_ax56_grade_enum_excludes_hakkalyakin_from_valid(self):
        """AX56: VALID_GRADES must not include Hakkalyakîn."""
        from fw_server.types import VALID_GRADES
        for g in VALID_GRADES:
            self.assertNotIn(
                "Hakkalyakîn", g,
                f"AX56: VALID_GRADES contains Hakkalyakîn: {g}",
            )

    def test_ax58_agents_declares_gap(self):
        """AX58: DUSUN.md must declare the şuhud–istidlal gap."""
        text = _read(AGENTS)
        self.assertIn("Şuhud", text)
        self.assertIn("İstidlal", text)
        # The gap must be in §7.2
        self.assertRegex(text, r"AX58.*gap|gap.*AX58")

    def test_kv4_bileshke_clamp(self):
        """KV₄: bileshke engine must enforce composite < 1.0."""
        from bileshke.engine import bileshke as compute_bileshke
        from bileshke.types import LensResult, LensId, EpistemicGrade

        # Create synthetic results with high scores
        results = [
            LensResult(
                lens_id=lid,
                score=0.99,
                grade=EpistemicGrade.ILMELYAKIN,
                checks_passed=10,
                checks_total=10,
                detail="synthetic high score",
            )
            for lid in LensId
        ]
        composite = compute_bileshke(results)
        self.assertLess(
            composite, 1.0,
            f"KV₄: bileshke composite must be < 1.0, got {composite}",
        )

    def test_kv4_warning_detected(self):
        """KV₄: detect_kv4_warning triggers at ≥ 0.95."""
        from bileshke.engine import detect_kv4_warning

        self.assertIsNone(
            detect_kv4_warning(0.85),
            "KV₄: 0.85 should NOT trigger warning",
        )
        warning = detect_kv4_warning(0.96)
        self.assertIsNotNone(
            warning,
            "KV₄: 0.96 MUST trigger warning",
        )


# ═══════════════════════════════════════════════════════════════════════
# CLASS 12: Run kaskad report + tesanud (AX23, AX63)
# ═══════════════════════════════════════════════════════════════════════


class TestKaskadReportAndTesanud(unittest.TestCase):
    """Exercise kaskad report and tesanüd analysis.

    AX23: Cascade structure (DAG).
    AX63: Affirmations compose super-additively (tesanüd);
          denials remain isolated (infirâdî).
    """

    def test_report_returns_valid_structure(self):
        """Kaskad report must return graph stats and chain integrity."""
        from fw_server.server import run_kaskad
        raw = run_kaskad("report", None, None, 0)
        result = json.loads(raw)
        self.assertEqual(result.get("action"), "report")
        # Report uses flat keys for graph stats
        self.assertIn("total_nodes", result)
        self.assertIn("total_edges", result)
        self.assertIn("chain_integrity", result)

    def test_tesanud_identifies_superadditive_nodes(self):
        """AX63: Tesanüd analysis must identify super-additive nodes."""
        from fw_server.server import run_kaskad
        raw = run_kaskad("tesanud", None, None, 0)
        result = json.loads(raw)
        self.assertEqual(result.get("action"), "tesanud")
        self.assertIn("tesanud_count", result)
        # At least some tesanüd nodes should exist per IG.3
        self.assertGreater(
            result.get("tesanud_count", 0), 0,
            "AX63: At least one tesanüd node expected",
        )

    def test_hubs_identifies_high_degree_nodes(self):
        """IG.3: Hub analysis must find high-degree nodes."""
        from fw_server.server import run_kaskad
        raw = run_kaskad("hubs", None, None, 0)
        result = json.loads(raw)
        self.assertEqual(result.get("action"), "hubs")
        hubs = result.get("hubs", [])
        self.assertGreater(len(hubs), 0, "Must find at least one hub node")


# ═══════════════════════════════════════════════════════════════════════
# CLASS 13: Hayat Layer Reporting in framework_summary
# ═══════════════════════════════════════════════════════════════════════


class TestHayatLayerReporting(unittest.TestCase):
    """Validate the hayat_layer section added to get_framework_summary().

    AX5:  ¬Hayat → Inert — without integration, modules are dead data.
    AX52: Multiplicative gate — zero in any dimension collapses the whole.
    AX57: Transparency — must disclose integration health alongside modules.

    v3.0 — The 3 Hayat components:
      1. dusun-field.instructions.md  (always-on field, applyTo: '**')
      2. dusun.prompt.md              (/dusun invocation prompt)
      3. dusun-check.prompt.md        (manual /self-check diagnostic)
    """

    @classmethod
    def setUpClass(cls):
        from fw_server.server import get_framework_summary
        raw = get_framework_summary()
        cls.result = json.loads(raw)
        cls.hayat = cls.result.get("hayat_layer", {})
        cls.integration = cls.result.get("system_integration", {})

    # ── hayat_layer top-level ──────────────────────────────────────

    def test_hayat_layer_present(self):
        """framework_summary must include hayat_layer key."""
        self.assertIn("hayat_layer", self.result)

    def test_hayat_status_active(self):
        """With all data files shipped, Hayat layer should be active."""
        self.assertEqual(self.hayat.get("status"), "active")

    def test_hayat_axiom_reference(self):
        """Must cite AX5 axiom."""
        self.assertIn("AX5", self.hayat.get("axiom", ""))

    # ── hayat_layer.components ─────────────────────────────────────

    def test_all_three_components_present(self):
        """All 3 Hayat components must appear in components dict."""
        components = self.hayat.get("components", {})
        for comp_id in ("field_instructions", "dusun_prompt",
                        "self_check_prompt"):
            with self.subTest(component=comp_id):
                self.assertIn(comp_id, components)

    def test_field_instructions_healthy(self):
        """dusun-field.instructions.md must be present and healthy."""
        comp = self.hayat.get("components", {}).get("field_instructions", {})
        self.assertTrue(comp.get("present"), "File must exist")
        self.assertTrue(comp.get("healthy"), "All markers must be found")

    def test_dusun_prompt_healthy(self):
        """dusun.prompt.md must be present and healthy."""
        comp = self.hayat.get("components", {}).get("dusun_prompt", {})
        self.assertTrue(comp.get("present"), "File must exist")
        self.assertTrue(comp.get("healthy"), "All markers must be found")

    def test_self_check_prompt_healthy(self):
        """dusun-check.prompt.md must be present and healthy."""
        comp = self.hayat.get("components", {}).get("self_check_prompt", {})
        self.assertTrue(comp.get("present"), "File must exist")
        self.assertTrue(comp.get("healthy"), "All markers must be found")

    def test_each_component_has_role(self):
        """Each component must describe its role."""
        for comp_id, comp in self.hayat.get("components", {}).items():
            with self.subTest(component=comp_id):
                self.assertIsInstance(comp.get("role"), str)
                self.assertGreater(len(comp.get("role", "")), 10)

    def test_each_component_has_path(self):
        """Each healthy component must report its path."""
        for comp_id, comp in self.hayat.get("components", {}).items():
            with self.subTest(component=comp_id):
                if comp.get("present"):
                    self.assertIsNotNone(comp.get("path"))

    # ── hayat_layer.integration_health ─────────────────────────────

    def test_integration_health_present(self):
        health = self.hayat.get("integration_health", {})
        self.assertIn("all_components_present", health)
        self.assertIn("always_on_confirmed", health)
        self.assertIn("shaping_refs_found", health)
        self.assertIn("shaping_refs_expected", health)

    def test_all_components_present_flag(self):
        health = self.hayat.get("integration_health", {})
        self.assertTrue(health.get("all_components_present"))

    def test_always_on_confirmed(self):
        """applyTo: '**' must be detected in field-instructions."""
        health = self.hayat.get("integration_health", {})
        self.assertTrue(health.get("always_on_confirmed"))

    def test_shaping_refs_all_three(self):
        """All 3 components must reference shaping_directives."""
        health = self.hayat.get("integration_health", {})
        self.assertEqual(health.get("shaping_refs_found"), 3)
        self.assertEqual(health.get("shaping_refs_expected"), 3)

    # ── system_integration ─────────────────────────────────────────

    def test_system_integration_present(self):
        """framework_summary must include system_integration key."""
        self.assertIn("system_integration", self.result)

    def test_total_modules_count(self):
        """Must report 9 analytical modules."""
        self.assertEqual(self.integration.get("total_modules"), 9)

    def test_healthy_modules_count(self):
        """All 9 modules should be healthy."""
        self.assertEqual(self.integration.get("healthy_modules"), 9)

    def test_hayat_active_flag(self):
        """system_integration.hayat_active must be true."""
        self.assertTrue(self.integration.get("hayat_active"))

    def test_coverage_bound(self):
        """Must cite T17 — 6/7 structural maximum."""
        self.assertIn("6/7", self.integration.get("coverage", ""))
        self.assertIn("T17", self.integration.get("coverage", ""))

    def test_max_grade_bound(self):
        """Must cite AX56 — İlmelyakîn maximum."""
        self.assertIn("AX56", self.integration.get("max_grade", ""))

    def test_ax52_gate_pass(self):
        """AX52 multiplicative gate should PASS with all modules healthy."""
        self.assertEqual(self.integration.get("ax52_gate"), "PASS")


# ═══════════════════════════════════════════════════════════════════════
# CLASS 14: _check_hayat_layer edge cases
# ═══════════════════════════════════════════════════════════════════════


class TestCheckHayatLayerEdgeCases(unittest.TestCase):
    """Test _check_hayat_layer internals for degraded/inactive scenarios."""

    def test_direct_call_returns_dict(self):
        """_check_hayat_layer() must return a dict with status key."""
        from fw_server.server import _check_hayat_layer
        result = _check_hayat_layer()
        self.assertIsInstance(result, dict)
        self.assertIn("status", result)

    def test_status_is_valid_value(self):
        """Status must be one of active/degraded/inactive."""
        from fw_server.server import _check_hayat_layer
        result = _check_hayat_layer()
        self.assertIn(result["status"], ("active", "degraded", "inactive"))

    def test_components_dict_has_three_keys(self):
        """Must report exactly 3 components."""
        from fw_server.server import _check_hayat_layer
        result = _check_hayat_layer()
        self.assertEqual(len(result["components"]), 3)

    def test_markers_expected_ge_markers_found(self):
        """markers_found <= markers_expected for each component."""
        from fw_server.server import _check_hayat_layer
        result = _check_hayat_layer()
        for comp_id, comp in result["components"].items():
            with self.subTest(component=comp_id):
                self.assertLessEqual(
                    comp["markers_found"], comp["markers_expected"])


# ═══════════════════════════════════════════════════════════════════════
# Runner
# ═══════════════════════════════════════════════════════════════════════


if __name__ == "__main__":
    unittest.main()
