"""Tests for cortex.core.retrieval_policy — Policy Engine (#362)."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import pytest

from cortex.core.retrieval_policy import (
    ALL_CHANNELS,
    DEFAULT_POLICIES,
    FALLBACK_POLICY,
    ChannelPolicy,
    PolicyEngine,
    RetrievalPolicy,
)
from cortex.core.task_mode import TaskMode, TaskModeResult


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

@dataclass
class _FakeConfig:
    """Minimal stand-in for CortexConfig for testing."""
    retrieval_policy_enabled: bool = True
    retrieval_policy_confidence_threshold: float = 0.3
    retrieval_policy_default_mode: str = "recall_history"


class TestChannelPolicy:
    def test_defaults(self):
        cp = ChannelPolicy()
        assert cp.enabled is True
        assert cp.weight == 0.2
        assert cp.min_confidence == 0.0
        assert cp.max_results == 10


class TestRetrievalPolicy:
    def test_enabled_channels(self):
        policy = RetrievalPolicy(
            channels={
                "semantic": ChannelPolicy(enabled=True, weight=0.5),
                "keyword": ChannelPolicy(enabled=False, weight=0.3),
                "temporal": ChannelPolicy(enabled=True, weight=0.2),
            }
        )
        enabled = policy.enabled_channels()
        assert "keyword" not in enabled
        assert enabled == ["semantic", "temporal"]  # sorted by weight desc

    def test_normalized_weights(self):
        policy = RetrievalPolicy(
            channels={
                "a": ChannelPolicy(enabled=True, weight=0.6),
                "b": ChannelPolicy(enabled=True, weight=0.4),
                "c": ChannelPolicy(enabled=False, weight=0.5),
            }
        )
        weights = policy.normalized_weights()
        assert "c" not in weights
        assert abs(weights["a"] - 0.6) < 0.01
        assert abs(weights["b"] - 0.4) < 0.01
        assert abs(sum(weights.values()) - 1.0) < 0.001

    def test_empty_channels(self):
        policy = RetrievalPolicy(channels={})
        assert policy.enabled_channels() == []
        assert policy.normalized_weights() == {}


class TestDefaultPolicies:
    """Each mode has a correct default policy."""

    def test_all_modes_covered(self):
        for mode in TaskMode:
            assert mode in DEFAULT_POLICIES, f"Missing policy for {mode.value}"

    @pytest.mark.parametrize("mode", list(TaskMode))
    def test_policy_has_all_channels(self, mode):
        policy = DEFAULT_POLICIES[mode]
        for ch in ALL_CHANNELS:
            assert ch in policy.channels, f"{mode.value} missing channel {ch}"

    @pytest.mark.parametrize("mode", list(TaskMode))
    def test_policy_mode_set(self, mode):
        policy = DEFAULT_POLICIES[mode]
        assert policy.mode == mode

    @pytest.mark.parametrize("mode", list(TaskMode))
    def test_policy_has_description(self, mode):
        policy = DEFAULT_POLICIES[mode]
        assert len(policy.description) > 0

    @pytest.mark.parametrize("mode", list(TaskMode))
    def test_weights_reasonable(self, mode):
        """All weights positive, sum > 0."""
        policy = DEFAULT_POLICIES[mode]
        total = sum(ch.weight for ch in policy.channels.values() if ch.enabled)
        assert total > 0.5, f"{mode.value}: total weight {total} seems too low"
        for name, ch in policy.channels.items():
            assert ch.weight >= 0.0, f"{mode.value}/{name}: negative weight"

    def test_debug_prioritizes_working_memory(self):
        policy = DEFAULT_POLICIES[TaskMode.DEBUG]
        assert policy.channels["working_memory"].weight >= policy.channels["semantic"].weight

    def test_design_prioritizes_semantic(self):
        policy = DEFAULT_POLICIES[TaskMode.DESIGN]
        assert policy.channels["semantic"].weight >= policy.channels["working_memory"].weight

    def test_future_commitment_prioritizes_temporal(self):
        policy = DEFAULT_POLICIES[TaskMode.FUTURE_COMMITMENT]
        assert policy.channels["temporal"].weight >= policy.channels["semantic"].weight


class TestFallbackPolicy:
    def test_has_all_channels(self):
        for ch in ALL_CHANNELS:
            assert ch in FALLBACK_POLICY.channels

    def test_mode_is_none(self):
        assert FALLBACK_POLICY.mode is None

    def test_balanced_weights(self):
        weights = [ch.weight for ch in FALLBACK_POLICY.channels.values()]
        max_w = max(weights)
        min_w = min(weights)
        assert max_w - min_w <= 0.2, "Fallback should be relatively balanced"


class TestPolicyEngine:
    @pytest.fixture
    def engine(self):
        return PolicyEngine(config=_FakeConfig())

    def test_high_confidence_returns_mode_policy(self, engine):
        mode_result = TaskModeResult(mode=TaskMode.DEBUG, confidence=0.8)
        policy = engine.get_policy(mode_result)
        assert policy.mode == TaskMode.DEBUG

    def test_low_confidence_returns_fallback(self, engine):
        mode_result = TaskModeResult(mode=TaskMode.DEBUG, confidence=0.1)
        policy = engine.get_policy(mode_result)
        assert policy.mode is None  # fallback

    def test_threshold_boundary_below(self, engine):
        mode_result = TaskModeResult(mode=TaskMode.PLAN, confidence=0.29)
        policy = engine.get_policy(mode_result)
        assert policy is FALLBACK_POLICY

    def test_threshold_boundary_at(self, engine):
        mode_result = TaskModeResult(mode=TaskMode.PLAN, confidence=0.3)
        policy = engine.get_policy(mode_result)
        assert policy.mode == TaskMode.PLAN

    def test_disabled_engine_returns_fallback(self):
        config = _FakeConfig(retrieval_policy_enabled=False)
        engine = PolicyEngine(config=config)
        mode_result = TaskModeResult(mode=TaskMode.DEBUG, confidence=0.9)
        policy = engine.get_policy(mode_result)
        assert policy is FALLBACK_POLICY

    def test_custom_threshold(self):
        config = _FakeConfig(retrieval_policy_confidence_threshold=0.7)
        engine = PolicyEngine(config=config)
        # 0.5 is above default 0.3 but below custom 0.7
        mode_result = TaskModeResult(mode=TaskMode.DEBUG, confidence=0.5)
        policy = engine.get_policy(mode_result)
        assert policy is FALLBACK_POLICY

    @pytest.mark.parametrize("mode", list(TaskMode))
    def test_each_mode_returns_correct_policy(self, mode, engine):
        mode_result = TaskModeResult(mode=mode, confidence=0.8)
        policy = engine.get_policy(mode_result)
        assert policy.mode == mode

    def test_no_config_uses_defaults(self):
        engine = PolicyEngine(config=None)
        mode_result = TaskModeResult(mode=TaskMode.DEBUG, confidence=0.5)
        policy = engine.get_policy(mode_result)
        assert policy.mode == TaskMode.DEBUG

    def test_enabled_property(self):
        engine_on = PolicyEngine(config=_FakeConfig(retrieval_policy_enabled=True))
        engine_off = PolicyEngine(config=_FakeConfig(retrieval_policy_enabled=False))
        assert engine_on.enabled is True
        assert engine_off.enabled is False


class TestBackwardCompat:
    """When task_mode_context is not provided, existing behavior is unchanged."""

    def test_no_task_mode_context_means_no_policy(self):
        """PolicyEngine is never invoked when task_mode_context is None.

        This is tested at the integration level — RetrievalPipeline.retrieve()
        only instantiates the classifier/engine when task_mode_context is not None.
        Here we just verify the engine handles all modes cleanly.
        """
        engine = PolicyEngine(config=_FakeConfig())
        # Even with zero confidence, engine returns fallback (not crash)
        result = TaskModeResult(mode=TaskMode.RECALL_HISTORY, confidence=0.0)
        policy = engine.get_policy(result)
        assert policy is FALLBACK_POLICY
