"""
tests/test_emergence.py — Comprehensive tests for the emergence module.

Test categories:
  1. Types & Enums         — EmergenceLevel, FlowDirection, InterfaceKind, VesselMode
  2. Dataclasses           — PovertyInterface, EmergenceNode, EmergenceEdge, etc.
  3. DAG Basics            — add_node, add_edge, counts, factory
  4. Provision Flow        — AX2–AX4 top-down flow computation
  5. Holographic Fidelity  — T12/AX37 fidelity pair computation
  6. Beauty Motor          — AX24/AX25/T7 beauty pressure
  7. Multiplicative Gate   — AX52 zero-collapse detection
  8. Verification          — All 9 checks + verify_all + yakinlasma
"""

import unittest

from emergence.emergence import (
    # Types
    EmergenceLevel,
    FlowDirection,
    InterfaceKind,
    VesselMode,
    PovertyInterface,
    EmergenceNode,
    EmergenceEdge,
    BeautyMotor,
    FidelityPair,
    EmergenceReport,
    clamp_score,
    # Engine
    EmergenceDAG,
    MAX_STRUCTURAL_COMPLETENESS,
    MIN_HOLOGRAPHIC_SEED,
    BEAUTY_BASE_PRESSURE,
    FIDELITY_DECAY_PER_LEVEL,
    POVERTY_TRANSPARENCY_THRESHOLD,
    # Verification
    verify_dag_acyclicity,
    verify_fidelity_bounds,
    verify_poverty_interfaces,
    verify_holographic_condensation,
    verify_transparent_vessels,
    verify_multiplicative_gate,
    verify_continuous_degrees,
    verify_beauty_motor,
    verify_top_down_flow,
    verify_all,
    yakinlasma,
    framework_summary,
)


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

def _make_interface(
    level: EmergenceLevel = EmergenceLevel.KAINAT,
    kind: InterfaceKind = InterfaceKind.FAKR,
    transparency: float = 0.7,
    capacity: float = 0.8,
) -> PovertyInterface:
    return PovertyInterface(
        level=level,
        interface_kind=kind,
        transparency=transparency,
        capacity=capacity,
    )


def _make_node(
    level: EmergenceLevel = EmergenceLevel.KAINAT,
    seed: float = 0.5,
    interfaces: tuple = (),
) -> EmergenceNode:
    if not interfaces:
        interfaces = (
            _make_interface(level=level),
        )
    return EmergenceNode(
        level=level,
        label=f"test_{level.value}",
        holographic_seed=seed,
        interfaces=interfaces,
    )


def _make_edge(
    src: EmergenceLevel = EmergenceLevel.HAYAT_MUHAMMEDIYE,
    tgt: EmergenceLevel = EmergenceLevel.RUH,
    fidelity: float = 0.8,
) -> EmergenceEdge:
    return EmergenceEdge(
        source_level=src,
        target_level=tgt,
        flow_direction=FlowDirection.TOP_DOWN,
        fidelity=fidelity,
    )


def _default_dag() -> EmergenceDAG:
    """Build the default 6-level dag via factory."""
    return EmergenceDAG.build_default_dag()


def _minimal_dag() -> EmergenceDAG:
    """Build a minimal 2-node, 1-edge dag."""
    dag = EmergenceDAG()
    dag.add_node(_make_node(EmergenceLevel.HAYAT_MUHAMMEDIYE, seed=0.9))
    dag.add_node(_make_node(EmergenceLevel.RUH, seed=0.7))
    dag.add_edge(_make_edge(
        EmergenceLevel.HAYAT_MUHAMMEDIYE,
        EmergenceLevel.RUH,
        fidelity=0.85,
    ))
    return dag


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

class TestEnums(unittest.TestCase):
    """Test enum definitions and counts."""

    def test_emergence_level_count(self):
        """6 fixed axiomatic levels (AX23)."""
        self.assertEqual(len(EmergenceLevel), 6)

    def test_emergence_level_values(self):
        expected = {
            "hayat_muhammediye", "ruh", "akil", "suur", "hayat", "kainat",
        }
        self.assertEqual({lv.value for lv in EmergenceLevel}, expected)

    def test_emergence_level_ranks(self):
        """Ranks: 0 (highest) → 5 (lowest)."""
        self.assertEqual(EmergenceLevel.HAYAT_MUHAMMEDIYE.rank, 0)
        self.assertEqual(EmergenceLevel.KAINAT.rank, 5)

    def test_rank_ordering(self):
        """Ranks must be strictly sequential 0..5."""
        ranks = sorted(lv.rank for lv in EmergenceLevel)
        self.assertEqual(ranks, [0, 1, 2, 3, 4, 5])

    def test_flow_direction_count(self):
        self.assertEqual(len(FlowDirection), 2)

    def test_interface_kind_count(self):
        self.assertEqual(len(InterfaceKind), 2)

    def test_vessel_mode_count(self):
        self.assertEqual(len(VesselMode), 2)


class TestClampScore(unittest.TestCase):
    """Test the clamp_score utility (KV₄)."""

    def test_normal_value(self):
        self.assertAlmostEqual(clamp_score(0.5), 0.5)

    def test_negative_clamped_to_zero(self):
        self.assertEqual(clamp_score(-1.0), 0.0)

    def test_one_clamped_below(self):
        self.assertLess(clamp_score(1.0), 1.0)
        self.assertEqual(clamp_score(1.0), MAX_STRUCTURAL_COMPLETENESS)

    def test_high_value_clamped(self):
        self.assertEqual(clamp_score(999.0), MAX_STRUCTURAL_COMPLETENESS)

    def test_zero_preserved(self):
        self.assertEqual(clamp_score(0.0), 0.0)


# ========================================================================
#  2. Dataclasses
# ========================================================================

class TestDataclasses(unittest.TestCase):
    """Test dataclass construction and properties."""

    def test_poverty_interface_frozen(self):
        iface = _make_interface()
        with self.assertRaises(AttributeError):
            iface.transparency = 0.5  # type: ignore[misc]

    def test_vessel_mode_transparent(self):
        iface = _make_interface(transparency=0.8)
        self.assertEqual(iface.vessel_mode, VesselMode.TRANSPARENT)

    def test_vessel_mode_opaque(self):
        iface = _make_interface(transparency=0.2)
        self.assertEqual(iface.vessel_mode, VesselMode.OPAQUE)

    def test_effective_capacity(self):
        iface = _make_interface(transparency=0.5, capacity=0.8)
        self.assertAlmostEqual(iface.effective_capacity, 0.4)

    def test_emergence_node_frozen(self):
        node = _make_node()
        with self.assertRaises(AttributeError):
            node.holographic_seed = 0.1  # type: ignore[misc]

    def test_emergence_node_hash(self):
        """Nodes with same level hash equally."""
        n1 = _make_node(EmergenceLevel.AKIL, seed=0.3)
        n2 = _make_node(EmergenceLevel.AKIL, seed=0.9)
        self.assertEqual(hash(n1), hash(n2))
        self.assertEqual(n1, n2)

    def test_emergence_edge_frozen(self):
        edge = _make_edge()
        with self.assertRaises(AttributeError):
            edge.fidelity = 0.1  # type: ignore[misc]

    def test_beauty_motor_to_dict(self):
        motor = BeautyMotor(
            desire_to_show=0.85, mirror_quality=0.6,
            compassion_flow=0.7, beauty_pressure=0.357,
        )
        d = motor.to_dict()
        self.assertIn("desire_to_show", d)
        self.assertIn("beauty_pressure", d)

    def test_fidelity_pair_to_dict(self):
        fp = FidelityPair(
            source_level=EmergenceLevel.RUH,
            target_level=EmergenceLevel.AKIL,
            source_fidelity=0.8, mirror_fidelity=0.5,
            participation_index=0.63,
        )
        d = fp.to_dict()
        self.assertEqual(d["source_level"], "ruh")
        self.assertEqual(d["target_level"], "akil")

    def test_emergence_report_to_dict(self):
        report = EmergenceReport()
        d = report.to_dict()
        self.assertIn("composite_score", d)
        self.assertIn("structural_completeness", d)
        self.assertEqual(d["total_nodes"], 0)

    def test_emergence_report_mutable(self):
        report = EmergenceReport()
        report.total_nodes = 5
        self.assertEqual(report.total_nodes, 5)


# ========================================================================
#  3. DAG Basics
# ========================================================================

class TestDAGBasics(unittest.TestCase):
    """Test EmergenceDAG construction and basic operations."""

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

    def test_add_node(self):
        dag = EmergenceDAG()
        dag.add_node(_make_node(EmergenceLevel.AKIL))
        self.assertEqual(dag.node_count, 1)

    def test_add_duplicate_node_replaces(self):
        dag = EmergenceDAG()
        dag.add_node(_make_node(EmergenceLevel.AKIL, seed=0.3))
        dag.add_node(_make_node(EmergenceLevel.AKIL, seed=0.9))
        self.assertEqual(dag.node_count, 1)
        self.assertAlmostEqual(
            dag.get_node(EmergenceLevel.AKIL).holographic_seed, 0.9,
        )

    def test_add_edge(self):
        dag = EmergenceDAG()
        dag.add_node(_make_node(EmergenceLevel.HAYAT_MUHAMMEDIYE))
        dag.add_node(_make_node(EmergenceLevel.RUH))
        dag.add_edge(_make_edge(
            EmergenceLevel.HAYAT_MUHAMMEDIYE,
            EmergenceLevel.RUH,
        ))
        self.assertEqual(dag.edge_count, 1)

    def test_edge_rejects_wrong_direction(self):
        """Top-down edge with source.rank >= target.rank must fail."""
        dag = EmergenceDAG()
        dag.add_node(_make_node(EmergenceLevel.RUH))         # rank=1
        dag.add_node(_make_node(EmergenceLevel.HAYAT_MUHAMMEDIYE))  # rank=0
        with self.assertRaises(ValueError):
            dag.add_edge(_make_edge(
                EmergenceLevel.RUH,   # rank=1 (source)
                EmergenceLevel.HAYAT_MUHAMMEDIYE,  # rank=0 (target)
            ))

    def test_edge_rejects_missing_node(self):
        dag = EmergenceDAG()
        dag.add_node(_make_node(EmergenceLevel.HAYAT_MUHAMMEDIYE))
        with self.assertRaises(ValueError):
            dag.add_edge(_make_edge(
                EmergenceLevel.HAYAT_MUHAMMEDIYE,
                EmergenceLevel.RUH,
            ))

    def test_topological_sort(self):
        dag = _default_dag()
        topo = dag.topological_sort()
        self.assertEqual(len(topo), 6)
        # Ranks must be ascending
        ranks = [lv.rank for lv in topo]
        self.assertEqual(ranks, sorted(ranks))

    def test_default_dag_factory(self):
        dag = _default_dag()
        self.assertEqual(dag.node_count, 6)
        # Adjacent levels → 5 edges
        self.assertEqual(dag.edge_count, 5)

    def test_reachability(self):
        dag = _default_dag()
        self.assertTrue(dag.is_reachable(
            EmergenceLevel.HAYAT_MUHAMMEDIYE,
            EmergenceLevel.KAINAT,
        ))
        self.assertFalse(dag.is_reachable(
            EmergenceLevel.KAINAT,
            EmergenceLevel.HAYAT_MUHAMMEDIYE,
        ))

    def test_get_nodes_sorted(self):
        dag = _default_dag()
        nodes = dag.get_nodes()
        self.assertEqual(nodes[0].level, EmergenceLevel.HAYAT_MUHAMMEDIYE)
        self.assertEqual(nodes[-1].level, EmergenceLevel.KAINAT)


# ========================================================================
#  4. Provision Flow
# ========================================================================

class TestProvisionFlow(unittest.TestCase):
    """Test provision flow computation (AX2–AX4)."""

    def test_flow_non_empty(self):
        dag = _default_dag()
        flows = dag.compute_provision_flow()
        self.assertEqual(len(flows), 5)  # 5 edges → 5 flow scores

    def test_flow_values_bounded(self):
        """KV₄: all flow scores must be in [0, MAX_STRUCTURAL_COMPLETENESS]."""
        dag = _default_dag()
        flows = dag.compute_provision_flow()
        for key, score in flows.items():
            self.assertGreaterEqual(score, 0.0, f"{key}: score < 0")
            self.assertLess(score, 1.0, f"{key}: score ≥ 1")

    def test_flow_decreases_with_depth(self):
        """Provision flow should generally decrease at deeper levels."""
        dag = _default_dag()
        flows = dag.compute_provision_flow()
        values = list(flows.values())
        # Not strictly monotonic but generally decreasing
        self.assertGreater(values[0], values[-1])

    def test_empty_dag_flow(self):
        dag = EmergenceDAG()
        flows = dag.compute_provision_flow()
        self.assertEqual(len(flows), 0)

    def test_minimal_dag_flow(self):
        dag = _minimal_dag()
        flows = dag.compute_provision_flow()
        self.assertEqual(len(flows), 1)
        key = list(flows.keys())[0]
        self.assertGreater(flows[key], 0.0)


# ========================================================================
#  5. Holographic Fidelity
# ========================================================================

class TestHolographicFidelity(unittest.TestCase):
    """Test holographic fidelity computation (T12/AX37)."""

    def test_fidelity_pairs_count(self):
        dag = _default_dag()
        pairs = dag.compute_holographic_fidelity()
        self.assertEqual(len(pairs), 5)  # One per edge

    def test_fidelity_pairs_bounded(self):
        """KV₄: all fidelity values in (0, 1)."""
        dag = _default_dag()
        pairs = dag.compute_holographic_fidelity()
        for p in pairs:
            self.assertGreater(p.source_fidelity, 0.0)
            self.assertLess(p.source_fidelity, 1.0)
            self.assertGreater(p.mirror_fidelity, 0.0)
            self.assertLess(p.mirror_fidelity, 1.0)
            self.assertGreater(p.participation_index, 0.0)
            self.assertLess(p.participation_index, 1.0)

    def test_participation_is_geometric_mean(self):
        """pi ≈ sqrt(source × mirror), clamped."""
        dag = _minimal_dag()
        pairs = dag.compute_holographic_fidelity()
        self.assertEqual(len(pairs), 1)
        p = pairs[0]
        import math
        expected = math.sqrt(p.source_fidelity * p.mirror_fidelity)
        self.assertAlmostEqual(p.participation_index, clamp_score(expected), places=4)


# ========================================================================
#  6. Beauty Motor
# ========================================================================

class TestBeautyMotor(unittest.TestCase):
    """Test beauty motor computation (AX24/AX25/T7)."""

    def test_motor_non_zero(self):
        """T7: Beauty motor is always active on a valid DAG."""
        dag = _default_dag()
        motor = dag.compute_beauty_motor()
        self.assertGreater(motor.beauty_pressure, 0.0)
        self.assertGreater(motor.desire_to_show, 0.0)
        self.assertGreater(motor.mirror_quality, 0.0)
        self.assertGreater(motor.compassion_flow, 0.0)

    def test_beauty_pressure_bounded(self):
        """KV₄: beauty pressure < 1."""
        dag = _default_dag()
        motor = dag.compute_beauty_motor()
        self.assertLess(motor.beauty_pressure, 1.0)

    def test_empty_dag_zero_motor(self):
        dag = EmergenceDAG()
        motor = dag.compute_beauty_motor()
        self.assertEqual(motor.beauty_pressure, 0.0)

    def test_desire_is_base_pressure(self):
        dag = _default_dag()
        motor = dag.compute_beauty_motor()
        self.assertAlmostEqual(motor.desire_to_show, BEAUTY_BASE_PRESSURE)


# ========================================================================
#  7. Multiplicative Gate & Tesanüd
# ========================================================================

class TestMultiplicativeGate(unittest.TestCase):
    """Test AX52 multiplicative gate."""

    def test_default_dag_passes(self):
        dag = _default_dag()
        passed, msg = dag.check_multiplicative_gate()
        self.assertTrue(passed, msg)

    def test_empty_dag_fails(self):
        dag = EmergenceDAG()
        passed, msg = dag.check_multiplicative_gate()
        self.assertFalse(passed)
        self.assertIn("AX52", msg)

    def test_zero_seed_fails(self):
        """KV₆: zero holographic seed → gate collapse."""
        dag = EmergenceDAG()
        dag.add_node(_make_node(EmergenceLevel.HAYAT_MUHAMMEDIYE, seed=0.0))
        dag.add_node(_make_node(EmergenceLevel.RUH, seed=0.5))
        dag.add_edge(_make_edge(
            EmergenceLevel.HAYAT_MUHAMMEDIYE,
            EmergenceLevel.RUH,
        ))
        passed, msg = dag.check_multiplicative_gate()
        self.assertFalse(passed)
        self.assertIn("zero", msg.lower())


class TestTesanud(unittest.TestCase):
    """Test AX63 tesanüd detection."""

    def test_no_tesanud_in_linear(self):
        dag = _minimal_dag()
        detected, _ = dag.detect_tesanud()
        self.assertFalse(detected)

    def test_tesanud_with_convergence(self):
        """If two edges converge on the same target → tesanüd."""
        dag = EmergenceDAG()
        dag.add_node(_make_node(EmergenceLevel.HAYAT_MUHAMMEDIYE, seed=0.9))
        dag.add_node(_make_node(EmergenceLevel.RUH, seed=0.8))
        dag.add_node(_make_node(EmergenceLevel.AKIL, seed=0.6))
        # Two edges converge on AKIL
        dag.add_edge(_make_edge(
            EmergenceLevel.HAYAT_MUHAMMEDIYE,
            EmergenceLevel.AKIL,
        ))
        dag.add_edge(_make_edge(
            EmergenceLevel.RUH,
            EmergenceLevel.AKIL,
        ))
        detected, msg = dag.detect_tesanud()
        self.assertTrue(detected)
        self.assertIn("AX63", msg)


# ========================================================================
#  8. Verification Layer
# ========================================================================

class TestVerification(unittest.TestCase):
    """Test all 9 verification checks + orchestrators."""

    def test_default_dag_acyclicity(self):
        dag = _default_dag()
        passed, msg = verify_dag_acyclicity(dag)
        self.assertTrue(passed, msg)

    def test_default_dag_fidelity_bounds(self):
        dag = _default_dag()
        passed, msg = verify_fidelity_bounds(dag)
        self.assertTrue(passed, msg)

    def test_default_dag_poverty_interfaces(self):
        dag = _default_dag()
        passed, msg = verify_poverty_interfaces(dag)
        self.assertTrue(passed, msg)

    def test_default_dag_holographic_condensation(self):
        dag = _default_dag()
        passed, msg = verify_holographic_condensation(dag)
        self.assertTrue(passed, msg)

    def test_default_dag_transparent_vessels(self):
        dag = _default_dag()
        passed, msg = verify_transparent_vessels(dag)
        # May WARN about opaque interfaces at deep levels
        # but should not crash
        self.assertIsInstance(passed, bool)

    def test_default_dag_multiplicative_gate(self):
        dag = _default_dag()
        passed, msg = verify_multiplicative_gate(dag)
        self.assertTrue(passed, msg)

    def test_default_dag_continuous_degrees(self):
        dag = _default_dag()
        passed, msg = verify_continuous_degrees(dag)
        self.assertTrue(passed, msg)

    def test_default_dag_beauty_motor(self):
        dag = _default_dag()
        passed, msg = verify_beauty_motor(dag)
        self.assertTrue(passed, msg)

    def test_default_dag_top_down_flow(self):
        dag = _default_dag()
        passed, msg = verify_top_down_flow(dag)
        self.assertTrue(passed, msg)

    def test_verify_all_returns_all_checks(self):
        dag = _default_dag()
        results = verify_all(dag)
        self.assertEqual(len(results), 9)
        expected_keys = {
            "dag_acyclicity", "fidelity_bounds", "poverty_interfaces",
            "holographic_condensation", "transparent_vessels",
            "multiplicative_gate", "continuous_degrees",
            "beauty_motor", "top_down_flow",
        }
        self.assertEqual(set(results.keys()), expected_keys)

    def test_verify_all_values_are_tuples(self):
        dag = _default_dag()
        results = verify_all(dag)
        for key, val in results.items():
            self.assertIsInstance(val, tuple, f"{key}: not a tuple")
            self.assertEqual(len(val), 2, f"{key}: not (bool, str)")
            self.assertIsInstance(val[0], bool, f"{key}: first element not bool")
            self.assertIsInstance(val[1], str, f"{key}: second element not str")

    def test_yakinlasma_default_dag(self):
        dag = _default_dag()
        score = yakinlasma(dag)
        self.assertGreater(score, 0.0)
        self.assertLess(score, 1.0)

    def test_yakinlasma_none_uses_default_dag(self):
        """When dag=None, yakinlasma builds the default DAG (harmony contract)."""
        score = yakinlasma(None)
        self.assertGreater(score, 0.0, "None should default to the default DAG, not 0.0")
        self.assertLess(score, 1.0)

    def test_yakinlasma_is_clamped(self):
        """KV₄: yakinlasma must never reach 1.0."""
        dag = _default_dag()
        score = yakinlasma(dag)
        self.assertLess(score, 1.0)

    def test_node_missing_interfaces_fails(self):
        """AX34: nodes without interfaces should fail poverty check."""
        dag = EmergenceDAG()
        dag.add_node(EmergenceNode(
            level=EmergenceLevel.KAINAT,
            label="test",
            holographic_seed=0.5,
            interfaces=(),  # No interfaces!
        ))
        passed, msg = verify_poverty_interfaces(dag)
        self.assertFalse(passed)

    def test_node_zero_seed_fails_holographic(self):
        """KV₆: zero seed fails holographic condensation check."""
        dag = EmergenceDAG()
        dag.add_node(EmergenceNode(
            level=EmergenceLevel.KAINAT,
            label="test",
            holographic_seed=0.0,
            interfaces=(_make_interface(),),
        ))
        passed, msg = verify_holographic_condensation(dag)
        self.assertFalse(passed)


# ========================================================================
#  9. Integration — Full Report
# ========================================================================

class TestIntegration(unittest.TestCase):
    """Full pipeline integration on the default DAG."""

    def test_generate_report(self):
        dag = _default_dag()
        report = dag.generate_report()
        self.assertEqual(report.total_nodes, 6)
        self.assertEqual(report.total_edges, 5)
        self.assertIsNotNone(report.beauty_motor)
        self.assertGreater(report.composite_score, 0.0)
        self.assertLess(report.composite_score, 1.0)
        self.assertTrue(report.multiplicative_gate_pass)

    def test_report_to_dict(self):
        dag = _default_dag()
        report = dag.generate_report()
        d = report.to_dict()
        self.assertIsInstance(d, dict)
        self.assertIn("beauty_motor", d)
        self.assertIn("fidelity_pairs", d)
        self.assertIsInstance(d["fidelity_pairs"], list)

    def test_emergence_score_bounded(self):
        """KV₄: composite score must be in (0, 1)."""
        dag = _default_dag()
        score = dag.compute_emergence_score()
        self.assertGreater(score, 0.0)
        self.assertLess(score, 1.0)

    def test_framework_summary_structure(self):
        summary = framework_summary()
        self.assertEqual(summary["module"], "emergence")
        self.assertIn("governing_axioms", summary)
        self.assertIn("capabilities", summary)
        self.assertIn("default_dag", summary)
        self.assertIn("verification", summary)
        self.assertIn("max_score", summary)

    def test_framework_summary_default_dag_passes(self):
        summary = framework_summary()
        dag_info = summary["default_dag"]
        self.assertEqual(dag_info["nodes"], 6)
        self.assertEqual(dag_info["edges"], 5)
        self.assertTrue(dag_info["gate_pass"])

    def test_full_pipeline_consistency(self):
        """All paths: report.composite ≈ compute_emergence_score."""
        dag = _default_dag()
        report = dag.generate_report()
        direct = dag.compute_emergence_score()
        self.assertAlmostEqual(report.composite_score, direct, places=4)


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