"""
tests/test_kaskad.py — Comprehensive tests for the kaskad (Generative Cascade) module.

Test categories:
  1. Types & Enums             — EdgeType, NodeKind, ChainId, dataclass construction
  2. CascadeDAG basics         — add_node, add_edge, cycle rejection, counts
  3. Topological operations    — sort, reachability, depths, critical path
  4. T8 Neighborliness         — prediction, confidence decay, edge cases
  5. AX63 Tesanüd detection    — super-additive convergence, infirâdî isolation
  6. Hub & Bridge analysis     — §IG.3 bridge nodes, multi-chain hubs
  7. Chain analysis             — per-chain integrity, acyclicity, connectivity
  8. Full report (generate_report)  — structural completeness, KV₄ cap
  9. Self-diagnostic           — reflexive integrity of the engine itself
 10. Propagation               — T7+T8 cascade propagation engine
 11. Subgraph extraction       — partial DAG construction
 12. Factory functions          — build_framework_graph, build_beauty_cascade
 13. Verification layer         — all verify_* functions
 14. Integration                — full pipeline on the framework graph
"""

import unittest

from kaskad.types import (
    EdgeType,
    NodeKind,
    ChainId,
    CascadeNode,
    CascadeEdge,
    NeighborhoodPrediction,
    HubAnalysis,
    ChainIntegrity,
    TesanudResult,
    CascadeReport,
)
from kaskad.engine import (
    CascadeDAG,
    MAX_STRUCTURAL_COMPLETENESS,
    T8_BASE_CONFIDENCE,
    T8_HOP_DECAY,
    MIN_TESANUD_CHAINS,
)
from kaskad.chains import build_framework_graph, build_beauty_cascade
from kaskad.verification import (
    verify_acyclicity,
    verify_transitivity_sample,
    verify_convergence_bound,
    verify_chain_coverage,
    verify_chain_integrity,
    verify_bridge_nodes,
    verify_sources_and_sinks,
    verify_all,
    is_fully_verified,
)


# ========================================================================
#  Helpers
# ========================================================================

def _make_node(nid: str, kind: NodeKind = NodeKind.AXIOM,
               chains: frozenset | None = None) -> CascadeNode:
    return CascadeNode(
        id=nid,
        kind=kind,
        label=nid,
        chains=chains or frozenset(),
    )


def _make_edge(src: str, tgt: str,
               etype: EdgeType = EdgeType.PROVES) -> CascadeEdge:
    return CascadeEdge(source=src, target=tgt, edge_type=etype)


def _linear_dag(n: int = 5) -> CascadeDAG:
    """Build a simple linear chain: N0 → N1 → N2 → ... → N(n-1)."""
    dag = CascadeDAG()
    nodes = [_make_node(f"N{i}") for i in range(n)]
    for nd in nodes:
        dag.add_node(nd)
    for i in range(n - 1):
        dag.add_edge(_make_edge(f"N{i}", f"N{i+1}"))
    return dag


def _diamond_dag() -> CascadeDAG:
    """Build a diamond: A → B, A → C, B → D, C → D."""
    dag = CascadeDAG()
    for nid in ["A", "B", "C", "D"]:
        dag.add_node(_make_node(nid))
    dag.add_edge(_make_edge("A", "B"))
    dag.add_edge(_make_edge("A", "C"))
    dag.add_edge(_make_edge("B", "D"))
    dag.add_edge(_make_edge("C", "D"))
    return dag


# ========================================================================
#  1. Types & Enums
# ========================================================================

class TestEnums(unittest.TestCase):

    def test_edge_type_count(self):
        """§IG.1: exactly 6 typed morphisms."""
        self.assertEqual(len(EdgeType), 6)

    def test_node_kind_count(self):
        """9 node categories."""
        self.assertEqual(len(NodeKind), 9)

    def test_chain_id_count(self):
        """7 chains + BEAUTY_CASCADE = 8 total."""
        self.assertEqual(len(ChainId), 8)

    def test_edge_type_values(self):
        expected = {"proves", "grounds", "constrains", "gates", "calibrates", "combines"}
        self.assertEqual({e.value for e in EdgeType}, expected)


class TestDataclasses(unittest.TestCase):

    def test_cascade_node_frozen(self):
        node = _make_node("X")
        with self.assertRaises(AttributeError):
            node.id = "Y"  # type: ignore[misc]

    def test_cascade_edge_frozen(self):
        edge = _make_edge("A", "B")
        with self.assertRaises(AttributeError):
            edge.source = "C"  # type: ignore[misc]

    def test_cascade_node_chains(self):
        chains = frozenset({ChainId.C1, ChainId.C3})
        node = CascadeNode(id="X", kind=NodeKind.KAVAID, label="test", chains=chains)
        self.assertEqual(node.chains, chains)

    def test_neighborhood_prediction(self):
        pred = NeighborhoodPrediction(
            detected="A", predicted="B", confidence=0.7, chain_coverage=2,
        )
        self.assertEqual(pred.detected, "A")
        self.assertAlmostEqual(pred.confidence, 0.7)

    def test_cascade_report(self):
        rpt = CascadeReport(
            total_nodes=10, total_edges=15, chain_count=3,
            chain_integrity={}, hub_nodes=[], bridge_nodes=["B"],
            tesanud_nodes=[], topological_order=[],
            max_depth=4, structural_completeness=0.65,
        )
        self.assertLess(rpt.structural_completeness, 1.0)


# ========================================================================
#  2. CascadeDAG Basics
# ========================================================================

class TestDAGBasics(unittest.TestCase):

    def test_empty_dag(self):
        dag = CascadeDAG()
        self.assertEqual(dag.node_count, 0)
        self.assertEqual(dag.edge_count, 0)
        self.assertEqual(len(dag), 0)

    def test_add_node(self):
        dag = CascadeDAG()
        dag.add_node(_make_node("A"))
        self.assertEqual(dag.node_count, 1)
        self.assertIn("A", dag)

    def test_add_duplicate_node(self):
        """Duplicate IDs are silently updated (not an error)."""
        dag = CascadeDAG()
        dag.add_node(_make_node("A"))
        dag.add_node(_make_node("A"))  # silently updates
        self.assertEqual(dag.node_count, 1)

    def test_add_edge(self):
        dag = CascadeDAG()
        dag.add_node(_make_node("A"))
        dag.add_node(_make_node("B"))
        dag.add_edge(_make_edge("A", "B"))
        self.assertEqual(dag.edge_count, 1)

    def test_add_edge_missing_node(self):
        dag = CascadeDAG()
        dag.add_node(_make_node("A"))
        with self.assertRaises(ValueError):
            dag.add_edge(_make_edge("A", "MISSING"))

    def test_cycle_rejection_direct(self):
        """AX23: DAG — direct cycle A→B→A must be rejected."""
        dag = CascadeDAG()
        dag.add_node(_make_node("A"))
        dag.add_node(_make_node("B"))
        dag.add_edge(_make_edge("A", "B"))
        with self.assertRaises(ValueError):
            dag.add_edge(_make_edge("B", "A"))

    def test_cycle_rejection_transitive(self):
        """AX23: DAG — transitive cycle A→B→C→A must be rejected."""
        dag = _linear_dag(3)  # N0→N1→N2
        with self.assertRaises(ValueError):
            dag.add_edge(_make_edge("N2", "N0"))

    def test_self_loop_rejection(self):
        """AX23: self-loops are cycles."""
        dag = CascadeDAG()
        dag.add_node(_make_node("A"))
        with self.assertRaises(ValueError):
            dag.add_edge(_make_edge("A", "A"))

    def test_successors_and_predecessors(self):
        dag = _diamond_dag()
        self.assertIn("B", dag.successors("A"))
        self.assertIn("C", dag.successors("A"))
        self.assertIn("A", dag.predecessors("B"))

    def test_in_out_degree(self):
        dag = _diamond_dag()
        self.assertEqual(dag.in_degree("A"), 0)
        self.assertEqual(dag.out_degree("A"), 2)
        self.assertEqual(dag.in_degree("D"), 2)
        self.assertEqual(dag.out_degree("D"), 0)

    def test_get_node(self):
        dag = CascadeDAG()
        n = _make_node("X")
        dag.add_node(n)
        self.assertEqual(dag.get_node("X"), n)
        self.assertIsNone(dag.get_node("NONEXISTENT"))

    def test_get_edges_by_type(self):
        dag = CascadeDAG()
        dag.add_node(_make_node("A"))
        dag.add_node(_make_node("B"))
        dag.add_node(_make_node("C"))
        dag.add_edge(_make_edge("A", "B", EdgeType.PROVES))
        dag.add_edge(_make_edge("A", "C", EdgeType.GROUNDS))
        proves = dag.get_edges_by_type(EdgeType.PROVES)
        self.assertEqual(len(proves), 1)
        self.assertEqual(proves[0].target, "B")

    def test_repr(self):
        dag = _linear_dag(3)
        r = repr(dag)
        self.assertIn("CascadeDAG", r)


# ========================================================================
#  3. Topological Operations
# ========================================================================

class TestTopological(unittest.TestCase):

    def test_topo_sort_linear(self):
        dag = _linear_dag(5)
        topo = dag.topological_sort()
        self.assertEqual(topo, ["N0", "N1", "N2", "N3", "N4"])

    def test_topo_sort_diamond(self):
        dag = _diamond_dag()
        topo = dag.topological_sort()
        self.assertEqual(topo[0], "A")
        self.assertEqual(topo[-1], "D")

    def test_topo_sort_caching(self):
        dag = _linear_dag(3)
        t1 = dag.topological_sort()
        t2 = dag.topological_sort()
        self.assertEqual(t1, t2)  # same values (cache-backed)

    def test_reachable_from(self):
        dag = _linear_dag(4)
        r = dag.reachable_from("N0")
        self.assertEqual(r, frozenset({"N1", "N2", "N3"}))

    def test_reachable_from_leaf(self):
        dag = _linear_dag(3)
        r = dag.reachable_from("N2")
        self.assertEqual(r, frozenset())

    def test_reachable_to(self):
        dag = _linear_dag(4)
        r = dag.reachable_to("N3")
        self.assertEqual(r, frozenset({"N0", "N1", "N2"}))

    def test_is_reachable(self):
        dag = _diamond_dag()
        self.assertTrue(dag.is_reachable("A", "D"))
        self.assertFalse(dag.is_reachable("D", "A"))
        self.assertFalse(dag.is_reachable("B", "C"))

    def test_compute_depths(self):
        dag = _linear_dag(4)
        depths = dag.compute_depths()
        self.assertEqual(depths["N0"], 0)
        self.assertEqual(depths["N3"], 3)

    def test_max_depth(self):
        dag = _linear_dag(5)
        self.assertEqual(dag.max_depth(), 4)

    def test_critical_path_linear(self):
        dag = _linear_dag(5)
        cp = dag.critical_path()
        self.assertEqual(cp, ["N0", "N1", "N2", "N3", "N4"])

    def test_critical_path_diamond(self):
        dag = _diamond_dag()
        cp = dag.critical_path()
        self.assertEqual(len(cp), 3)
        self.assertEqual(cp[0], "A")
        self.assertEqual(cp[-1], "D")

    def test_source_nodes(self):
        dag = _diamond_dag()
        self.assertEqual(dag.source_nodes(), frozenset({"A"}))

    def test_sink_nodes(self):
        dag = _diamond_dag()
        self.assertEqual(dag.sink_nodes(), frozenset({"D"}))

    def test_source_sink_linear(self):
        dag = _linear_dag(5)
        self.assertEqual(dag.source_nodes(), frozenset({"N0"}))
        self.assertEqual(dag.sink_nodes(), frozenset({"N4"}))


# ========================================================================
#  4. T8 Neighborhood Prediction
# ========================================================================

class TestNeighborhood(unittest.TestCase):

    def test_direct_neighbor_predicted(self):
        """T8: direct neighbors are predicted from detected set."""
        dag = _linear_dag(3)
        result = dag.predict_neighbors(["N0"])
        self.assertIn("N1", result.predicted)
        self.assertGreater(result.confidence, 0.0)
        self.assertLess(result.confidence, 1.0)

    def test_hop_decay(self):
        """T8: 2-hop neighbors included with wider range."""
        dag = _linear_dag(3)
        result = dag.predict_neighbors(["N0"], max_hops=2)
        self.assertIn("N2", result.predicted)
        self.assertGreater(result.confidence, 0.0)

    def test_max_hops_default(self):
        """Default max_hops=2: includes 2-hop neighbors."""
        dag = _linear_dag(5)
        result = dag.predict_neighbors(["N0"])
        self.assertIn("N1", result.predicted)

    def test_nonexistent_node(self):
        dag = CascadeDAG()
        with self.assertRaises(ValueError):
            dag.predict_neighbors(["GHOST"])


# ========================================================================
#  5. AX63 Tesanüd Detection
# ========================================================================

class TestTesanud(unittest.TestCase):

    def _multi_chain_dag(self) -> CascadeDAG:
        """Build a DAG with shared node: C1 and C2 both reach BRIDGE."""
        dag = CascadeDAG()
        dag.add_node(CascadeNode("A1", NodeKind.AXIOM, "A1",
                                  frozenset({ChainId.C1})))
        dag.add_node(CascadeNode("A2", NodeKind.AXIOM, "A2",
                                  frozenset({ChainId.C2})))
        dag.add_node(CascadeNode("BRIDGE", NodeKind.KAVAID, "Bridge",
                                  frozenset({ChainId.C1, ChainId.C2})))
        dag.add_node(CascadeNode("SINK", NodeKind.OUTPUT_RULE, "Sink",
                                  frozenset({ChainId.C1, ChainId.C2})))
        dag.add_edge(_make_edge("A1", "BRIDGE", EdgeType.PROVES))
        dag.add_edge(_make_edge("A2", "BRIDGE", EdgeType.GROUNDS))
        dag.add_edge(_make_edge("BRIDGE", "SINK", EdgeType.GATES))
        return dag

    def test_tesanud_detected(self):
        dag = self._multi_chain_dag()
        results = dag.analyze_tesanud()
        bridge_results = [r for r in results if r.node_id == "BRIDGE"]
        self.assertEqual(len(bridge_results), 1)
        self.assertTrue(bridge_results[0].is_tesanud)
        self.assertGreaterEqual(len(bridge_results[0].converging_chains), 2)

    def test_infiradi_single_chain(self):
        """Single-chain node should NOT be tesanüd."""
        dag = CascadeDAG()
        dag.add_node(CascadeNode("X", NodeKind.AXIOM, "X",
                                  frozenset({ChainId.C1})))
        results = dag.analyze_tesanud()
        x_results = [r for r in results if r.node_id == "X"]
        if x_results:
            self.assertFalse(x_results[0].is_tesanud)


# ========================================================================
#  6. Hub & Bridge Analysis
# ========================================================================

class TestHubBridge(unittest.TestCase):

    def test_hub_in_diamond(self):
        """D in diamond has in-degree 2 — qualifies as hub."""
        dag = _diamond_dag()
        hubs = dag.analyze_hubs(min_in_degree=2)
        hub_ids = {h.node_id for h in hubs}
        self.assertIn("D", hub_ids)

    def test_bridge_multi_chain(self):
        """A node in >1 chain is a bridge node."""
        dag = CascadeDAG()
        dag.add_node(CascadeNode("X", NodeKind.KAVAID, "X",
                                  frozenset({ChainId.C3, ChainId.C4, ChainId.C7})))
        bridges = dag.find_bridges()
        self.assertIn("X", bridges)

    def test_no_bridge_single_chain(self):
        """All nodes in 1 chain → no bridges."""
        dag = _linear_dag(3)
        bridges = dag.find_bridges()
        self.assertEqual(bridges, [])


# ========================================================================
#  7. Chain Analysis
# ========================================================================

class TestChainAnalysis(unittest.TestCase):

    def test_chain_with_no_nodes(self):
        dag = CascadeDAG()
        ci = dag.analyze_chain(ChainId.C1)
        self.assertEqual(ci.node_count, 0)
        self.assertEqual(ci.edge_count, 0)

    def test_chain_linear(self):
        """A chain with linearly connected nodes."""
        dag = CascadeDAG()
        for i in range(4):
            dag.add_node(CascadeNode(
                f"C1_{i}", NodeKind.AXIOM, f"C1_{i}",
                frozenset({ChainId.C1}),
            ))
        for i in range(3):
            dag.add_edge(_make_edge(f"C1_{i}", f"C1_{i+1}"))
        ci = dag.analyze_chain(ChainId.C1)
        self.assertTrue(ci.is_acyclic)
        self.assertTrue(ci.is_connected)
        self.assertEqual(ci.node_count, 4)
        self.assertEqual(ci.edge_count, 3)
        self.assertEqual(ci.max_depth, 3)

    def test_chain_disconnected(self):
        """A chain with disconnected components."""
        dag = CascadeDAG()
        dag.add_node(CascadeNode("A", NodeKind.AXIOM, "A",
                                  frozenset({ChainId.C1})))
        dag.add_node(CascadeNode("B", NodeKind.THEOREM, "B",
                                  frozenset({ChainId.C1})))
        # No edge between A and B
        ci = dag.analyze_chain(ChainId.C1)
        self.assertFalse(ci.is_connected)


# ========================================================================
#  8. Full Report
# ========================================================================

class TestGenerateReport(unittest.TestCase):

    def test_report_linear(self):
        dag = _linear_dag(5)
        rpt = dag.generate_report()
        self.assertEqual(rpt.total_nodes, 5)
        self.assertEqual(rpt.total_edges, 4)
        self.assertEqual(rpt.max_depth, 4)
        self.assertLess(rpt.structural_completeness, 1.0)

    def test_report_kv4_cap(self):
        """KV₄: structural_completeness must NEVER reach 1.0."""
        dag = _linear_dag(100)
        rpt = dag.generate_report()
        self.assertLess(rpt.structural_completeness, MAX_STRUCTURAL_COMPLETENESS)

    def test_report_empty_dag(self):
        dag = CascadeDAG()
        rpt = dag.generate_report()
        self.assertEqual(rpt.total_nodes, 0)
        self.assertEqual(rpt.structural_completeness, 0.0)


# ========================================================================
#  9. Self-Diagnostic
# ========================================================================

class TestSelfDiagnostic(unittest.TestCase):

    def test_diagnostic_linear(self):
        dag = _linear_dag(5)
        diag = dag.self_diagnostic()
        self.assertIn("is_dag", diag)
        self.assertIn("max_depth", diag)
        self.assertTrue(diag["is_dag"])

    def test_diagnostic_diamond(self):
        dag = _diamond_dag()
        diag = dag.self_diagnostic()
        self.assertTrue(diag["is_dag"])
        self.assertIn("max_depth", diag)


# ========================================================================
# 10. Propagation
# ========================================================================

class TestPropagation(unittest.TestCase):

    def test_propagate_linear(self):
        """T7+T8: seeding N0 should reach all downstream nodes."""
        dag = _linear_dag(4)
        result = dict(dag.propagate(["N0"]))
        self.assertIn("N0", result)
        self.assertIn("N3", result)

    def test_propagate_hops(self):
        """Hop count should increase with distance."""
        dag = _linear_dag(5)
        result = dict(dag.propagate(["N0"]))
        self.assertEqual(result["N0"], 0)
        self.assertEqual(result["N1"], 1)
        self.assertEqual(result["N4"], 4)

    def test_propagate_diamond(self):
        """Diamond: multiple paths to D, shortest wins."""
        dag = _diamond_dag()
        result = dict(dag.propagate(["A"]))
        self.assertEqual(result["A"], 0)
        self.assertEqual(result["B"], 1)
        self.assertEqual(result["C"], 1)
        self.assertEqual(result["D"], 2)

    def test_propagate_max_hops(self):
        """max_hops limits propagation depth."""
        dag = _linear_dag(10)
        result = dict(dag.propagate(["N0"], max_hops=3))
        self.assertIn("N3", result)
        self.assertNotIn("N4", result)

    def test_propagate_nonexistent_seed(self):
        dag = _linear_dag(3)
        result = dag.propagate(["GHOST"])
        self.assertEqual(result, [])

    def test_propagate_empty_seeds(self):
        dag = _linear_dag(3)
        result = dag.propagate([])
        self.assertEqual(result, [])


# ========================================================================
# 11. Subgraph Extraction
# ========================================================================

class TestSubgraph(unittest.TestCase):

    def test_subgraph_preserves_edges(self):
        dag = _linear_dag(5)
        sub = dag.subgraph(["N1", "N2", "N3"])
        self.assertEqual(sub.node_count, 3)
        self.assertEqual(sub.edge_count, 2)

    def test_subgraph_drops_external_edges(self):
        dag = _linear_dag(5)
        sub = dag.subgraph(["N0", "N4"])
        self.assertEqual(sub.node_count, 2)
        self.assertEqual(sub.edge_count, 0)  # N0→N4 is not direct

    def test_subgraph_empty(self):
        dag = _linear_dag(5)
        sub = dag.subgraph([])
        self.assertEqual(sub.node_count, 0)


# ========================================================================
# 12. Factory Functions (chains.py)
# ========================================================================

class TestFactories(unittest.TestCase):

    def test_framework_graph_builds(self):
        dag = build_framework_graph()
        self.assertGreater(dag.node_count, 30)
        self.assertGreater(dag.edge_count, 40)

    def test_framework_graph_is_dag(self):
        """AX23: framework graph MUST be acyclic."""
        dag = build_framework_graph()
        topo = dag.topological_sort()
        self.assertEqual(len(topo), dag.node_count)

    def test_framework_graph_has_sources_sinks(self):
        dag = build_framework_graph()
        self.assertGreater(len(dag.source_nodes()), 0)
        self.assertGreater(len(dag.sink_nodes()), 0)

    def test_framework_graph_bridge_nodes(self):
        """§IG.3: KV₄ should be a bridge node."""
        dag = build_framework_graph()
        bridges = dag.find_bridges()
        self.assertIn("KV4", bridges)

    def test_beauty_cascade_builds(self):
        dag = build_beauty_cascade()
        self.assertEqual(dag.node_count, 9)
        self.assertEqual(dag.edge_count, 8)

    def test_beauty_cascade_linear(self):
        """9-level cascade is a simple linear chain."""
        dag = build_beauty_cascade()
        sources = dag.source_nodes()
        sinks = dag.sink_nodes()
        self.assertEqual(len(sources), 1)
        self.assertEqual(len(sinks), 1)
        self.assertIn("Cemal_Kemal", sources)
        self.assertIn("Tanzim_Takdir", sinks)

    def test_beauty_critical_path(self):
        dag = build_beauty_cascade()
        cp = dag.critical_path()
        self.assertEqual(len(cp), 9)
        self.assertEqual(cp[0], "Cemal_Kemal")
        self.assertEqual(cp[-1], "Tanzim_Takdir")


# ========================================================================
# 13. Verification Layer
# ========================================================================

class TestVerification(unittest.TestCase):

    def test_verify_acyclicity_pass(self):
        dag = _linear_dag(5)
        result = verify_acyclicity(dag)
        self.assertTrue(result.passed)

    def test_verify_transitivity_pass(self):
        dag = _linear_dag(5)
        result = verify_transitivity_sample(dag)
        self.assertTrue(result.passed)

    def test_verify_convergence_bound_pass(self):
        dag = _linear_dag(5)
        result = verify_convergence_bound(dag)
        self.assertTrue(result.passed)

    def test_verify_sources_sinks_pass(self):
        dag = _linear_dag(5)
        result = verify_sources_and_sinks(dag)
        self.assertTrue(result.passed)

    def test_verify_sources_sinks_empty(self):
        dag = CascadeDAG()
        result = verify_sources_and_sinks(dag)
        self.assertFalse(result.passed)

    def test_verify_bridge_nodes_on_framework(self):
        dag = build_framework_graph()
        result = verify_bridge_nodes(dag)
        self.assertTrue(result.passed)

    def test_verify_chain_integrity_on_framework(self):
        dag = build_framework_graph()
        result = verify_chain_integrity(dag)
        self.assertTrue(result.passed)


# ========================================================================
# 14. Integration: full pipeline on framework graph
# ========================================================================

class TestIntegration(unittest.TestCase):

    def test_full_verification_pipeline(self):
        """Run verify_all on framework graph — all checks must pass."""
        dag = build_framework_graph()
        results = verify_all(dag)
        self.assertTrue(is_fully_verified(results),
                        msg=f"Failed checks: {[r for r in results if not r.passed]}")

    def test_framework_report(self):
        """generate_report on framework graph respects KV₄."""
        dag = build_framework_graph()
        rpt = dag.generate_report()
        self.assertLess(rpt.structural_completeness, 1.0)
        self.assertGreater(rpt.structural_completeness, 0.0)

    def test_framework_self_diagnostic(self):
        """self_diagnostic on framework graph."""
        dag = build_framework_graph()
        diag = dag.self_diagnostic()
        self.assertTrue(diag["is_dag"])

    def test_beauty_verification(self):
        """verify_all on beauty cascade (may have chain coverage gaps, that's OK)."""
        dag = build_beauty_cascade()
        # Only run the checks that make sense for this small graph
        self.assertTrue(verify_acyclicity(dag).passed)
        self.assertTrue(verify_sources_and_sinks(dag).passed)
        self.assertTrue(verify_convergence_bound(dag).passed)

    def test_propagate_from_framework_source(self):
        """T7+T8: propagate from framework source nodes."""
        dag = build_framework_graph()
        sources = list(dag.source_nodes())
        if sources:
            result = dict(dag.propagate(sources[:1], max_hops=5))
            self.assertGreater(len(result), 1)

    def test_tesanud_in_framework(self):
        """AX63: framework should have tesanüd convergence points."""
        dag = build_framework_graph()
        tesanud = dag.analyze_tesanud()
        has_tesanud = any(t.is_tesanud for t in tesanud)
        self.assertTrue(has_tesanud,
                        msg="Framework graph should have super-additive convergence points")


# ---------------------------------------------------------------------------
# 15. framework_summary — kaskad.verification.framework_summary
# ---------------------------------------------------------------------------

class TestFrameworkSummary(unittest.TestCase):
    """Tests for the framework_summary() function added to kaskad.verification."""

    @classmethod
    def setUpClass(cls):
        from kaskad.verification import framework_summary
        cls.summary = framework_summary()

    # -- structure ----------------------------------------------------------

    def test_returns_dict(self):
        self.assertIsInstance(self.summary, dict)

    def test_top_level_keys(self):
        expected = {
            "module", "governing_axioms", "capabilities",
            "framework_graph", "self_diagnostic", "verification",
        }
        self.assertTrue(expected.issubset(self.summary.keys()),
                        msg=f"Missing keys: {expected - self.summary.keys()}")

    def test_module_identity(self):
        self.assertEqual(self.summary["module"], "kaskad")

    def test_governing_axioms_non_empty(self):
        axioms = self.summary["governing_axioms"]
        self.assertIsInstance(axioms, list)
        self.assertGreater(len(axioms), 0)
        # Core axioms must be present
        for ax in ("AX23", "T7", "T8", "AX63"):
            self.assertIn(ax, axioms, msg=f"{ax} missing from governing_axioms")

    def test_capabilities_non_empty(self):
        caps = self.summary["capabilities"]
        self.assertIsInstance(caps, list)
        self.assertGreater(len(caps), 0)

    # -- framework_graph ----------------------------------------------------

    def test_framework_graph_keys(self):
        fg = self.summary["framework_graph"]
        for key in ("nodes", "edges", "chains", "max_depth",
                     "structural_completeness", "bridge_nodes", "tesanud_count"):
            self.assertIn(key, fg, msg=f"framework_graph missing '{key}'")

    def test_framework_graph_positive(self):
        fg = self.summary["framework_graph"]
        self.assertGreater(fg["nodes"], 0)
        self.assertGreater(fg["edges"], 0)
        self.assertGreater(fg["chains"], 0)

    def test_structural_completeness_bounded(self):
        sc = self.summary["framework_graph"]["structural_completeness"]
        self.assertGreater(sc, 0.0)
        self.assertLessEqual(sc, MAX_STRUCTURAL_COMPLETENESS)

    def test_bridge_nodes_present(self):
        bn = self.summary["framework_graph"]["bridge_nodes"]
        self.assertIsInstance(bn, list)
        self.assertGreater(len(bn), 0, msg="Framework should have bridge nodes")

    def test_tesanud_count_positive(self):
        tc = self.summary["framework_graph"]["tesanud_count"]
        self.assertGreater(tc, 0, msg="Framework should have tesanüd points")

    # -- self_diagnostic ----------------------------------------------------

    def test_self_diagnostic_keys(self):
        sd = self.summary["self_diagnostic"]
        for key in ("is_dag", "all_nodes_classified", "all_nodes_grounded",
                     "kv4_satisfied"):
            self.assertIn(key, sd, msg=f"self_diagnostic missing '{key}'")

    def test_self_diagnostic_core_pass(self):
        sd = self.summary["self_diagnostic"]
        self.assertTrue(sd["is_dag"], msg="self_diagnostic: is_dag should be True")
        self.assertTrue(sd["kv4_satisfied"], msg="self_diagnostic: kv4_satisfied should be True")

    # -- verification -------------------------------------------------------

    def test_verification_keys(self):
        v = self.summary["verification"]
        self.assertIn("checks_run", v)
        self.assertIn("checks_passed", v)
        self.assertIn("all_pass", v)

    def test_verification_all_pass(self):
        self.assertTrue(self.summary["verification"]["all_pass"],
                        msg="Framework verification should pass all checks")

    def test_verification_check_count(self):
        v = self.summary["verification"]
        self.assertEqual(v["checks_run"], 7,
                         msg="Should have exactly 7 verification checks")
        self.assertEqual(v["checks_passed"], 7,
                         msg="All 7 checks should pass")


# ---------------------------------------------------------------------------
# 16. MCP Server Integration — kaskad wiring in fw_server.server
# ---------------------------------------------------------------------------

class TestKaskadMCPIntegration(unittest.TestCase):
    """Integration tests ensuring kaskad is wired into the MCP server functions."""

    # -- framework_summary integration --------------------------------------

    def test_framework_summary_includes_kaskad(self):
        """get_framework_summary should include a 'kaskad' key."""
        import json
        from fw_server.server import get_framework_summary
        result = get_framework_summary()
        data = json.loads(result)
        self.assertIn("kaskad", data,
                       msg="get_framework_summary() must include kaskad module")
        ks = data["kaskad"]
        self.assertEqual(ks["module"], "kaskad")

    # -- verify_chains integration ------------------------------------------

    def test_verify_chains_includes_kaskad(self):
        """verify_chains should include kaskad_verification in output."""
        import json
        from fw_server.server import verify_chains
        result = verify_chains()
        data = json.loads(result)
        self.assertIn("kaskad_verification", data,
                       msg="verify_chains() must include kaskad_verification")
        kv = data["kaskad_verification"]
        self.assertIn("checks", kv)
        self.assertIn("all_pass", kv)
        self.assertTrue(kv["all_pass"])

    # -- run_kaskad tool actions --------------------------------------------

    def _run_kaskad(self, action, **kwargs):
        import json
        from fw_server.server import run_kaskad
        result = run_kaskad(action=action, **kwargs)
        data = json.loads(result)
        self.assertNotIn("error", data,
                         msg=f"run_kaskad('{action}') returned error: {data.get('error')}")
        return data

    def test_action_report(self):
        data = self._run_kaskad("report")
        self.assertIn("total_nodes", data)
        self.assertIn("total_edges", data)
        self.assertGreater(data["total_nodes"], 0)

    def test_action_verify(self):
        data = self._run_kaskad("verify")
        self.assertIn("all_pass", data)
        self.assertTrue(data["all_pass"])

    def test_action_diagnostic(self):
        data = self._run_kaskad("diagnostic")
        self.assertIn("self_diagnostic", data)
        sd = data["self_diagnostic"]
        self.assertTrue(sd["is_dag"])

    def test_action_hubs(self):
        data = self._run_kaskad("hubs")
        self.assertIn("hubs", data)
        self.assertIsInstance(data["hubs"], list)
        self.assertGreater(len(data["hubs"]), 0)

    def test_action_tesanud(self):
        data = self._run_kaskad("tesanud")
        self.assertIn("results", data)
        self.assertIsInstance(data["results"], list)

    def test_action_predict(self):
        data = self._run_kaskad("predict", seeds='["AX24", "AX25"]')
        self.assertIn("detected", data)
        self.assertIn("predicted", data)

    def test_action_propagate(self):
        data = self._run_kaskad("propagate", seeds='["AX1"]', max_hops=3)
        self.assertIn("reached", data)
        self.assertIsInstance(data["reached"], list)

    def test_action_chain(self):
        data = self._run_kaskad("chain", chain_id="C2_beauty_creation")
        self.assertIn("chain_id", data)
        self.assertEqual(data["chain_id"], "C2_beauty_creation")
        self.assertIn("nodes", data)

    def test_invalid_action_returns_error(self):
        import json
        from fw_server.server import run_kaskad
        result = run_kaskad(action="nonexistent_action")
        data = json.loads(result)
        self.assertIn("error", data)


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