"""
tests/test_plugin_wiring.py — Integration tests for Phase A/B/C wiring into CortexPlugin.

Verifies:
    1. Working memory cache is created when config.working_memory_enabled=True
    2. Working memory cache is NOT created when config.working_memory_enabled=False
    3. RetrievalPipeline receives the working memory reference
    4. Working memory is cleared on sleep()
    5. TokenBudgetManager receives fraction config fields from config
    6. PromotionScorer is created and connected
    7. Full init with all features enabled does not crash
"""
from __future__ import annotations

import asyncio
import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from cortex.config import CortexConfig


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

def _make_config(**overrides) -> CortexConfig:
    """Create a minimal CortexConfig with test-friendly defaults."""
    cfg = CortexConfig()
    cfg.storage_db_path = ":memory:"
    cfg.llm_provider_priority = []  # no LLM needed for wiring tests
    for k, v in overrides.items():
        setattr(cfg, k, v)
    return cfg


def _make_plugin(config: CortexConfig):
    """Build a CortexPlugin with the given config, mocking heavy deps."""
    with patch("cortex.api.plugin.build_provider_cascade", return_value=None):
        from cortex.api.plugin import CortexPlugin
        plugin = CortexPlugin.__new__(CortexPlugin)
        # Manually run __init__ with our config
        plugin._init_errors = []
        plugin._session_id = None
        plugin._llm_call_count = 0
        plugin.config = config

        from cortex.logging_utils import install_redaction_filter
        install_redaction_filter()

        plugin._extraction_semaphore = asyncio.Semaphore(4)
        plugin._zero_claim_outcomes = 0

        # Storage — use None for simplicity (wiring tests don't need DB)
        plugin.storage = None
        plugin.llm = None
        plugin.extractor = None
        plugin.entity_resolver = None
        plugin.reconciler = None
        plugin.cornerstone_guardian = None
        plugin._circuit_breaker = None
        plugin._embedding_model_changed = False
        plugin._embedding_model_warning_emitted = False

        # Token budget manager
        from cortex.core.token_budget import TokenBudgetManager
        plugin.token_budget_manager = TokenBudgetManager(config=config)

        # Working memory cache
        plugin.working_memory = None
        if config.working_memory_enabled:
            try:
                from cortex.core.working_memory import WorkingMemoryCache
                plugin.working_memory = WorkingMemoryCache(
                    max_items=config.working_memory_max_items,
                    default_ttl_seconds=config.working_memory_default_ttl_seconds,
                    enabled=True,
                )
            except Exception:
                pass

        # Retrieval pipeline — mock it but track working_memory arg
        plugin.retrieval = MagicMock()
        plugin.retrieval._working_memory = plugin.working_memory

        # Feedback system
        plugin.feedback_system = None

        # Promotion scorer
        plugin.promotion_scorer = None
        try:
            from cortex.core.promotion import PromotionScorer
            plugin.promotion_scorer = PromotionScorer(
                config=config,
                feedback_system=plugin.feedback_system,
            )
        except Exception:
            pass

        # Event emitter
        plugin.event_emitter = None
        plugin._webhook_dispatcher = None

        # Deprecation pipeline
        plugin.deprecation_pipeline = None

        return plugin


# ---------------------------------------------------------------------------
# Working Memory Wiring (Phase A)
# ---------------------------------------------------------------------------

class TestWorkingMemoryWiring:
    """Verify WorkingMemoryCache is wired into CortexPlugin."""

    def test_working_memory_created_when_enabled(self):
        config = _make_config(working_memory_enabled=True, working_memory_max_items=50)
        plugin = _make_plugin(config)
        assert plugin.working_memory is not None
        assert plugin.working_memory.enabled is True

    def test_working_memory_not_created_when_disabled(self):
        config = _make_config(working_memory_enabled=False)
        plugin = _make_plugin(config)
        assert plugin.working_memory is None

    def test_working_memory_respects_config_max_items(self):
        config = _make_config(
            working_memory_enabled=True,
            working_memory_max_items=42,
        )
        plugin = _make_plugin(config)
        assert plugin.working_memory is not None
        assert plugin.working_memory._max_items == 42

    def test_working_memory_respects_config_ttl(self):
        config = _make_config(
            working_memory_enabled=True,
            working_memory_default_ttl_seconds=600,
        )
        plugin = _make_plugin(config)
        assert plugin.working_memory is not None
        assert plugin.working_memory._default_ttl == 600

    @pytest.mark.asyncio
    async def test_sleep_clears_working_memory(self):
        """sleep() should clear the working memory for the ended session."""
        config = _make_config(working_memory_enabled=True)
        plugin = _make_plugin(config)
        plugin._session_id = "test-session"

        # Add an entry to working memory
        await plugin.working_memory.put(
            "key1", "value1", "task_state", session_id="test-session"
        )
        stats = await plugin.working_memory.stats(session_id="test-session")
        assert stats["session_items"] == 1

        # Call sleep
        report = await plugin.sleep(session_id="test-session")

        # Working memory should be cleared
        stats = await plugin.working_memory.stats(session_id="test-session")
        assert stats["session_items"] == 0

    def test_retrieval_pipeline_receives_working_memory(self):
        """RetrievalPipeline should be constructed with working_memory parameter."""
        config = _make_config(working_memory_enabled=True)

        # Use actual plugin init to verify the real wiring
        with patch("cortex.api.plugin.build_provider_cascade", return_value=None):
            from cortex.api.plugin import CortexPlugin
            plugin = CortexPlugin(db_path=":memory:")

        # The real RetrievalPipeline may fail to init (no storage), but if it
        # did init, it should have the working_memory reference
        if plugin.retrieval is not None:
            assert hasattr(plugin.retrieval, "working_memory")
            if plugin.working_memory is not None:
                assert plugin.retrieval.working_memory is plugin.working_memory


# ---------------------------------------------------------------------------
# Token Budget Wiring (Phase B)
# ---------------------------------------------------------------------------

class TestTokenBudgetWiring:
    """Verify TokenBudgetManager receives fraction config from CortexConfig."""

    def test_token_budget_manager_reads_memory_fraction(self):
        config = _make_config(token_budget_memory_fraction=0.30)
        plugin = _make_plugin(config)
        assert plugin.token_budget_manager.memory_fraction == 0.30

    def test_token_budget_manager_reads_user_fraction(self):
        config = _make_config(token_budget_user_fraction=0.50)
        plugin = _make_plugin(config)
        assert plugin.token_budget_manager.user_fraction == 0.50

    def test_token_budget_manager_reads_reserve_fraction(self):
        config = _make_config(token_budget_reserve_fraction=0.15)
        plugin = _make_plugin(config)
        assert plugin.token_budget_manager.reserve_fraction == 0.15

    def test_token_budget_default_fractions(self):
        """Default fractions should match the spec defaults."""
        config = _make_config()
        plugin = _make_plugin(config)
        assert plugin.token_budget_manager.memory_fraction == 0.25
        assert plugin.token_budget_manager.user_fraction == 0.55
        assert plugin.token_budget_manager.reserve_fraction == 0.20


# ---------------------------------------------------------------------------
# Promotion Scorer Wiring (Phase C)
# ---------------------------------------------------------------------------

class TestPromotionScorerWiring:
    """Verify PromotionScorer is wired into CortexPlugin."""

    def test_promotion_scorer_created(self):
        config = _make_config()
        plugin = _make_plugin(config)
        assert plugin.promotion_scorer is not None

    def test_promotion_scorer_reads_config_threshold(self):
        config = _make_config(promotion_threshold=0.75)
        plugin = _make_plugin(config)
        assert plugin.promotion_scorer.threshold == 0.75

    def test_promotion_scorer_reads_config_weights(self):
        config = _make_config(
            promotion_weight_evidence_strength=0.40,
            promotion_weight_cross_session_frequency=0.20,
            promotion_weight_user_explicitness=0.15,
            promotion_weight_retrieval_utility=0.10,
            promotion_weight_temporal_stability=0.10,
            promotion_weight_contradiction_absence=0.05,
        )
        plugin = _make_plugin(config)
        w = plugin.promotion_scorer.weights
        assert w.evidence_strength == 0.40
        assert w.cross_session_frequency == 0.20

    def test_promotion_scorer_receives_feedback_system(self):
        config = _make_config()
        plugin = _make_plugin(config)
        # feedback_system is None in our test setup, but the scorer should accept it
        assert plugin.promotion_scorer._feedback_system is None

    def test_promotion_scorer_wired_to_dream_cycle(self):
        """PromotionScorer should be passed through to CircadianEngine → SleepPipeline → S2Promoter."""
        config = _make_config()
        plugin = _make_plugin(config)

        # Verify the scorer exists
        assert plugin.promotion_scorer is not None

        # Verify dream() passes it through by checking CircadianEngine construction
        from cortex.circadian.engine import CircadianEngine
        from cortex.sleep.pipeline import SleepPipeline

        # Create a SleepPipeline with the scorer
        mock_storage = MagicMock()
        mock_llm = MagicMock()
        pipeline = SleepPipeline(
            storage=mock_storage,
            llm=mock_llm,
            promotion_scorer=plugin.promotion_scorer,
        )
        assert pipeline._s2._promotion_scorer is plugin.promotion_scorer


# ---------------------------------------------------------------------------
# Full Init Smoke Test
# ---------------------------------------------------------------------------

class TestFullInitSmoke:
    """Verify that full init with all features enabled doesn't crash."""

    def test_full_init_all_features_enabled(self):
        """CortexPlugin should init successfully with all new features enabled."""
        config = _make_config(
            working_memory_enabled=True,
            working_memory_max_items=100,
            working_memory_default_ttl_seconds=1800,
            token_budget_memory_fraction=0.25,
            token_budget_user_fraction=0.55,
            token_budget_reserve_fraction=0.20,
            promotion_threshold=0.6,
            promotion_min_sessions=1,
        )
        plugin = _make_plugin(config)

        # All components should be present
        assert plugin.working_memory is not None
        assert plugin.token_budget_manager is not None
        assert plugin.promotion_scorer is not None

    def test_full_init_all_features_disabled(self):
        """CortexPlugin should init successfully with working memory disabled."""
        config = _make_config(working_memory_enabled=False)
        plugin = _make_plugin(config)

        assert plugin.working_memory is None
        assert plugin.token_budget_manager is not None
        assert plugin.promotion_scorer is not None  # scorer always created

    def test_actual_plugin_init_does_not_crash(self):
        """Real CortexPlugin.__init__ should not crash with defaults."""
        with patch("cortex.api.plugin.build_provider_cascade", return_value=None):
            from cortex.api.plugin import CortexPlugin
            plugin = CortexPlugin(db_path=":memory:")

        # Verify new components are present
        assert hasattr(plugin, "working_memory")
        assert hasattr(plugin, "promotion_scorer")
        assert plugin.token_budget_manager is not None
