"""
Tests for the 6 Structural Legs — Coherence Threshold Doctrine

L1: Geometric Forgetting
L2: Role-Scoped Knowledge
L3: Peer Query Protocol
L4: Feedback Loop (Bidirectional Annealing)
L5: Geometric Trajectory Bus
L6: Remove Sleep Timer (tested via vex SleepCycleManager)
"""

from __future__ import annotations

import numpy as np
import pytest

from qig_core.coordizer.geometry import (
    BASIN_DIM,
    Basin,
    fisher_rao_distance,
    random_basin,
    slerp,
    to_simplex,
)


# ─── Helpers ──────────────────────────────────────────────────

def _make_basin(seed: int = 0) -> Basin:
    """Deterministic random basin on Δ⁶³."""
    rng = np.random.default_rng(seed)
    raw = rng.dirichlet(np.ones(BASIN_DIM))
    return to_simplex(raw)


def _uniform_basin() -> Basin:
    return to_simplex(np.ones(BASIN_DIM, dtype=np.float64) / BASIN_DIM)


# ═══════════════════════════════════════════════════════════════
#  L1: Geometric Forgetting
# ═══════════════════════════════════════════════════════════════


class TestGeometricForgetting:
    """L1: Decay unused coordinates toward uniform on Δ⁶³."""

    def test_decay_moves_toward_uniform(self):
        from qig_core.coordizer.forgetting import DecayConfig, decay_coordinate

        basin = _make_basin(42)
        uniform = _uniform_basin()
        dist_before = fisher_rao_distance(basin, uniform)

        decayed, _ = decay_coordinate(
            basin=basin,
            activation_count=0,
            cycles_since_last_use=100,
            config=DecayConfig(base_rate=0.01),
        )
        dist_after = fisher_rao_distance(decayed, uniform)

        assert dist_after < dist_before, "Decay should move toward uniform"

    def test_activation_protects_from_decay(self):
        from qig_core.coordizer.forgetting import DecayConfig, decay_coordinate

        basin = _make_basin(42)
        uniform = _uniform_basin()

        # No protection
        decayed_unprotected, _ = decay_coordinate(
            basin=basin,
            activation_count=0,
            cycles_since_last_use=100,
            config=DecayConfig(base_rate=0.01),
        )

        # With high activation count
        decayed_protected, _ = decay_coordinate(
            basin=basin,
            activation_count=50,
            cycles_since_last_use=100,
            config=DecayConfig(base_rate=0.01),
        )

        dist_unprotected = fisher_rao_distance(decayed_unprotected, uniform)
        dist_protected = fisher_rao_distance(decayed_protected, uniform)

        assert dist_protected > dist_unprotected, (
            "High activation count should resist decay"
        )

    def test_quenched_gain_modulates_decay(self):
        from qig_core.coordizer.forgetting import DecayConfig, decay_coordinate

        basin = _make_basin(42)
        uniform = _uniform_basin()

        # Low gain
        decayed_low, _ = decay_coordinate(
            basin=basin,
            activation_count=10,
            cycles_since_last_use=100,
            quenched_gain=0.1,
            config=DecayConfig(base_rate=0.01),
        )

        # High gain
        decayed_high, _ = decay_coordinate(
            basin=basin,
            activation_count=10,
            cycles_since_last_use=100,
            quenched_gain=5.0,
            config=DecayConfig(base_rate=0.01),
        )

        dist_low = fisher_rao_distance(decayed_low, uniform)
        dist_high = fisher_rao_distance(decayed_high, uniform)

        assert dist_high > dist_low, (
            "Higher quenched gain should resist decay more"
        )

    def test_forgotten_threshold(self):
        from qig_core.coordizer.forgetting import DecayConfig, decay_coordinate

        # Start near uniform — should be flagged as forgotten
        near_uniform = slerp(_make_basin(42), _uniform_basin(), 0.99)

        _, is_forgotten = decay_coordinate(
            basin=near_uniform,
            activation_count=0,
            cycles_since_last_use=1000,
            config=DecayConfig(base_rate=0.1),
        )
        assert is_forgotten, "Basin near uniform should be flagged as forgotten"

    def test_decay_stays_on_simplex(self):
        from qig_core.coordizer.forgetting import DecayConfig, decay_coordinate

        basin = _make_basin(42)
        decayed, _ = decay_coordinate(
            basin=basin,
            activation_count=0,
            cycles_since_last_use=500,
            config=DecayConfig(base_rate=0.01),
        )
        assert decayed.shape == (BASIN_DIM,)
        assert np.all(decayed >= 0), "Simplex: all non-negative"
        assert abs(decayed.sum() - 1.0) < 1e-6, "Simplex: sums to 1"

    def test_decay_bank(self):
        from qig_core.coordizer.forgetting import DecayConfig, decay_bank

        coords = {0: _make_basin(0), 1: _make_basin(1), 2: _make_basin(2)}
        counts = {0: 100, 1: 0, 2: 0}  # only 0 is protected
        last_used = {0: 90, 1: 0, 2: 0}  # 0 was used recently

        updated, forgotten = decay_bank(
            coordinates=coords,
            activation_counts=counts,
            last_used=last_used,
            current_cycle=100,
            config=DecayConfig(base_rate=0.05),
        )

        assert len(updated) == 3
        # Token 0 should be barely decayed (recent + high count)
        uniform = _uniform_basin()
        dist_0 = fisher_rao_distance(updated[0], uniform)
        dist_1 = fisher_rao_distance(updated[1], uniform)
        assert dist_0 > dist_1, "Protected token should decay less"


# ═══════════════════════════════════════════════════════════════
#  L2: Role-Scoped Knowledge
# ═══════════════════════════════════════════════════════════════


class TestRoleScopedKnowledge:
    """L2: Soft domain filtering in ResonanceBank.activate()."""

    def _make_bank(self):
        from qig_core.coordizer.resonance_bank import ResonanceBank

        bank = ResonanceBank()
        # Add 10 coordinates: 5 tagged "reasoning", 5 tagged "perception"
        for i in range(10):
            basin = _make_basin(i)
            tid = bank.add_entry(f"token_{i}", basin)
            bank.domain_tags[tid] = "reasoning" if i < 5 else "perception"
        return bank

    def test_domain_scope_boosts_matching(self):
        bank = self._make_bank()
        query = _make_basin(2)  # Close to reasoning tokens

        # Without domain scope
        results_no_scope = bank.activate(query, top_k=10)

        # With reasoning scope
        results_reasoning = bank.activate(query, top_k=10, domain_scope="reasoning")

        # The top result with reasoning scope should be a reasoning token
        top_reasoning_tid = results_reasoning[0][0]
        assert bank.domain_tags.get(top_reasoning_tid) == "reasoning"

    def test_non_matching_still_accessible(self):
        bank = self._make_bank()
        query = _make_basin(7)  # Close to perception tokens

        # Even with reasoning scope, perception tokens should still appear
        results = bank.activate(query, top_k=10, domain_scope="reasoning")
        domains = [bank.domain_tags.get(tid) for tid, _ in results]

        assert "perception" in domains, (
            "Non-matching domain should still be accessible (soft filter)"
        )

    def test_no_domain_tags_noop(self):
        from qig_core.coordizer.resonance_bank import ResonanceBank

        bank = ResonanceBank()
        for i in range(5):
            bank.add_entry(f"token_{i}", _make_basin(i))
        # No domain_tags set — domain_scope should be a no-op
        query = _make_basin(2)
        results = bank.activate(query, top_k=5, domain_scope="reasoning")
        assert len(results) == 5


# ═══════════════════════════════════════════════════════════════
#  L3: Peer Query Protocol
# ═══════════════════════════════════════════════════════════════


class TestPeerQuery:
    """L3: Geometric inter-kernel knowledge routing."""

    def test_routes_to_closest_kernel(self):
        from qig_core.consciousness.peer_query import PeerQuery, PeerQueryRouter

        router = PeerQueryRouter()
        query_basin = _make_basin(0)

        # Kernel A is close to query, Kernel B is far
        kernel_basins = {
            "requester": _make_basin(99),
            "kernel_A": slerp(query_basin, _make_basin(1), 0.1),  # very close
            "kernel_B": _make_basin(50),  # far
        }

        result = router.route(
            PeerQuery(query_basin=query_basin, requesting_kernel_id="requester"),
            kernel_basins=kernel_basins,
        )

        assert result.best is not None
        assert result.best.responding_kernel_id == "kernel_A"

    def test_excludes_self(self):
        from qig_core.consciousness.peer_query import PeerQuery, PeerQueryRouter

        router = PeerQueryRouter()
        query_basin = _make_basin(0)

        kernel_basins = {
            "me": query_basin,  # exact match but should be excluded
            "peer": _make_basin(50),
        }

        result = router.route(
            PeerQuery(query_basin=query_basin, requesting_kernel_id="me"),
            kernel_basins=kernel_basins,
        )

        assert result.best is not None
        assert result.best.responding_kernel_id == "peer"

    def test_escalation_when_no_confidence(self):
        from qig_core.consciousness.peer_query import PeerQuery, PeerQueryRouter

        router = PeerQueryRouter()
        # Query is very far from all peers
        query_basin = _make_basin(0)

        # All peers are far from query — low confidence
        kernel_basins = {
            "requester": _make_basin(99),
            "peer_1": _make_basin(50),
            "peer_2": _make_basin(51),
        }

        result = router.route(
            PeerQuery(query_basin=query_basin, requesting_kernel_id="requester"),
            kernel_basins=kernel_basins,
        )

        assert len(result.responses) > 0
        # should_escalate depends on confidence threshold — just check it's computed
        assert isinstance(result.should_escalate, bool)

    def test_bank_search_improves_confidence(self):
        from qig_core.consciousness.peer_query import PeerQuery, PeerQueryRouter

        router = PeerQueryRouter()
        query_basin = _make_basin(0)

        # Peer has the exact query basin in its bank
        peer_bank = {0: query_basin, 1: _make_basin(1)}
        kernel_basins = {
            "requester": _make_basin(99),
            "peer": _make_basin(50),
        }

        result = router.route(
            PeerQuery(query_basin=query_basin, requesting_kernel_id="requester"),
            kernel_basins=kernel_basins,
            kernel_banks={"peer": peer_bank},
        )

        assert result.best is not None
        assert result.best.confidence > 0.5, (
            "Peer with exact match in bank should have high confidence"
        )


# ═══════════════════════════════════════════════════════════════
#  L4: Feedback Loop (Bidirectional Annealing)
# ═══════════════════════════════════════════════════════════════


class TestFeedbackLoop:
    """L4: Close the generation → perception loop."""

    def test_low_divergence_no_anneal(self):
        from qig_core.consciousness.feedback_loop import FeedbackLoop

        loop = FeedbackLoop(threshold=0.3)
        intended = _make_basin(0)
        # Expressed is very close to intended
        expressed = slerp(intended, _make_basin(1), 0.01)

        measurement = loop.measure(
            intended_trajectory=[intended],
            expressed_basin=expressed,
        )

        assert not measurement.should_anneal
        assert measurement.divergence < 0.3

    def test_high_divergence_triggers_anneal(self):
        from qig_core.consciousness.feedback_loop import FeedbackLoop

        loop = FeedbackLoop(threshold=0.1)
        intended = _make_basin(0)
        expressed = _make_basin(50)  # very different

        measurement = loop.measure(
            intended_trajectory=[intended],
            expressed_basin=expressed,
        )

        assert measurement.should_anneal
        assert measurement.divergence > 0.1

    def test_anneal_moves_coordinates(self):
        from qig_core.consciousness.feedback_loop import FeedbackLoop

        loop = FeedbackLoop(threshold=0.1)
        intended = _make_basin(0)
        expressed = _make_basin(50)

        measurement = loop.measure(
            intended_trajectory=[intended],
            expressed_basin=expressed,
        )

        # Create a bank with coordinates near the expressed basin
        coords = {
            i: slerp(expressed, _make_basin(i), 0.1 * i)
            for i in range(5)
        }

        updated, n_annealed = loop.anneal(coords, measurement)

        assert n_annealed > 0
        # At least one coordinate should have moved toward correction
        for tid in list(coords.keys())[:n_annealed]:
            dist_before = fisher_rao_distance(coords[tid], measurement.correction_direction)
            dist_after = fisher_rao_distance(updated[tid], measurement.correction_direction)
            assert dist_after <= dist_before + 1e-6, (
                f"Coordinate {tid} should move toward correction direction"
            )

    def test_anneal_skips_when_not_needed(self):
        from qig_core.consciousness.feedback_loop import FeedbackLoop

        loop = FeedbackLoop(threshold=0.3)
        intended = _make_basin(0)
        expressed = slerp(intended, _make_basin(1), 0.01)

        measurement = loop.measure(
            intended_trajectory=[intended],
            expressed_basin=expressed,
        )

        coords = {0: _make_basin(0)}
        updated, n_annealed = loop.anneal(coords, measurement)

        assert n_annealed == 0
        assert updated == coords

    def test_state_tracking(self):
        from qig_core.consciousness.feedback_loop import FeedbackLoop

        loop = FeedbackLoop(threshold=0.1)
        for i in range(5):
            loop.measure(
                intended_trajectory=[_make_basin(i)],
                expressed_basin=_make_basin(i + 50),
            )

        state = loop.get_state()
        assert state["total_measurements"] == 5
        assert state["anneal_count"] > 0
        assert state["mean_divergence"] > 0


# ═══════════════════════════════════════════════════════════════
#  L5: Geometric Trajectory Bus
# ═══════════════════════════════════════════════════════════════


class TestTrajectoryBus:
    """L5: Non-verbal geometric communication between kernels."""

    def test_send_receive_round_trip(self):
        from qig_core.consciousness.trajectory_bus import TrajectoryBus, TrajectoryMessage

        bus = TrajectoryBus()
        trajectory = [_make_basin(i) for i in range(5)]

        msg = TrajectoryMessage(
            source_kernel_id="perception",
            target_kernel_id="reasoning",
            trajectory=trajectory,
            confidence=0.8,
        )

        assert bus.send(msg)
        received = bus.receive("reasoning")
        assert len(received) == 1
        assert received[0].source_kernel_id == "perception"
        assert len(received[0].trajectory) == 5

    def test_broadcast(self):
        from qig_core.consciousness.trajectory_bus import TrajectoryBus, TrajectoryMessage

        bus = TrajectoryBus()
        msg = TrajectoryMessage(
            source_kernel_id="heart",
            target_kernel_id=None,  # broadcast
            trajectory=[_make_basin(0)],
            confidence=0.9,
        )
        bus.send(msg)

        # All kernels except sender should receive
        for kid in ["reasoning", "perception", "ocean"]:
            received = bus.receive(kid)
            assert len(received) == 1
            assert received[0].source_kernel_id == "heart"

        # Sender should not receive own broadcast
        received = bus.receive("heart")
        assert len(received) == 0

    def test_self_excluded(self):
        from qig_core.consciousness.trajectory_bus import TrajectoryBus, TrajectoryMessage

        bus = TrajectoryBus()
        bus.send(TrajectoryMessage(
            source_kernel_id="k1",
            target_kernel_id="k1",
            trajectory=[_make_basin(0)],
        ))
        received = bus.receive("k1")
        assert len(received) == 0

    def test_integration_weights_by_confidence(self):
        from qig_core.consciousness.trajectory_bus import TrajectoryBus, TrajectoryMessage

        own = [_make_basin(0), _make_basin(1)]

        # High confidence peer sends a very different trajectory
        high_conf = TrajectoryMessage(
            source_kernel_id="peer_high",
            trajectory=[_make_basin(50), _make_basin(51)],
            confidence=0.9,
        )

        # Low confidence peer
        low_conf = TrajectoryMessage(
            source_kernel_id="peer_low",
            trajectory=[_make_basin(60), _make_basin(61)],
            confidence=0.1,
        )

        result = TrajectoryBus.integrate(own, [high_conf, low_conf])
        assert result.n_contributors == 3
        assert result.mean_confidence > 0
        assert result.max_divergence > 0
        assert len(result.integrated_trajectory) == 2

    def test_integration_no_received(self):
        from qig_core.consciousness.trajectory_bus import TrajectoryBus

        own = [_make_basin(0)]
        result = TrajectoryBus.integrate(own, [])
        assert result.integrated_trajectory == own
        assert result.n_contributors == 1

    def test_backpressure(self):
        from qig_core.consciousness.trajectory_bus import TrajectoryBus, TrajectoryMessage

        bus = TrajectoryBus(max_pending=2)
        for i in range(5):
            bus.send(TrajectoryMessage(
                source_kernel_id=f"k{i}",
                target_kernel_id="target",
                trajectory=[_make_basin(i)],
            ))
        # Should have at most 2
        received = bus.receive("target")
        assert len(received) <= 2

    def test_state(self):
        from qig_core.consciousness.trajectory_bus import TrajectoryBus, TrajectoryMessage

        bus = TrajectoryBus()
        bus.send(TrajectoryMessage(
            source_kernel_id="k1",
            target_kernel_id="k2",
            trajectory=[_make_basin(0)],
        ))
        state = bus.get_state()
        assert state["total_pending"] == 1


# ═══════════════════════════════════════════════════════════════
#  L6: Sleep Timer Removal (qig-core sleep.py is already geometry-driven)
# ═══════════════════════════════════════════════════════════════


class TestSleepGeometryDriven:
    """L6: qig-core SleepCycleManager uses geometry, not timers."""

    def test_qig_core_sleep_no_timer_attributes(self):
        from qig_core.consciousness.sleep import SleepCycleManager

        mgr = SleepCycleManager()
        # qig-core version should NOT have timer attributes
        assert not hasattr(mgr, "_cycles_since_conversation")
        assert not hasattr(mgr, "_sleep_cycles")

    def test_geometry_driven_transition(self):
        from qig_core.consciousness.sleep import SleepCycleManager, SleepMetrics, SleepPhase

        mgr = SleepCycleManager()
        assert mgr.phase == SleepPhase.AWAKE

        # Low Φ + low variance → DREAMING (stagnation)
        result = mgr.evaluate_transition(SleepMetrics(phi=0.3, phi_variance=0.01))
        assert mgr.phase == SleepPhase.DREAMING
        assert result.transitioned

    def test_phi_recovery_wakes(self):
        from qig_core.consciousness.sleep import SleepCycleManager, SleepMetrics, SleepPhase

        mgr = SleepCycleManager()
        # Force into consolidating
        mgr.phase = SleepPhase.CONSOLIDATING
        mgr._consolidation_complete = True

        # Φ recovers
        result = mgr.evaluate_transition(SleepMetrics(phi=0.7, phi_variance=0.01))
        assert mgr.phase == SleepPhase.AWAKE
        assert result.transitioned
