"""tests/test_provider_router_wiring.py — ProviderRouter wiring into CortexPlugin (#681)."""
from __future__ import annotations
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from cortex.llm.provider import CompletionResponse, EmbedResponse, ExtractionResponse, LLMProvider
from cortex.llm.provider_router import ProviderRouter
from cortex.config import CortexConfig

def _cr(t="ok"): return CompletionResponse(text=t,model="t",tokens_in=5,tokens_out=3,cost_estimate=0,latency_ms=10)
def _er(): return EmbedResponse(embeddings=[[0.1]],model="t",tokens_in=4,tokens_out=0,cost_estimate=0,latency_ms=5)
def _ex(): return ExtractionResponse(data={"claims":[]},model="t",tokens_in=5,tokens_out=3,cost_estimate=0,latency_ms=10)

class TestProviderRouterInit:
    def test_default_mode_is_host(self):
        r = ProviderRouter(config=CortexConfig()); assert r._mode == "host" and r.provider is not None
    def test_byok_mode(self):
        c = CortexConfig(); c.provider_mode = "byok"; assert ProviderRouter(config=c)._mode == "byok"
    def test_bundled_mode_raises(self):
        c = CortexConfig(); c.provider_mode = "bundled"
        with pytest.raises(NotImplementedError): ProviderRouter(config=c)
    def test_unknown_mode_raises(self):
        c = CortexConfig(); c.provider_mode = "invalid"
        with pytest.raises(ValueError): ProviderRouter(config=c)
    def test_provider_names(self):
        names = ProviderRouter(config=CortexConfig()).provider_names()
        assert isinstance(names, list) and names[0] == "host"

class TestProviderRouterDelegation:
    @pytest.fixture
    def rm(self):
        r = ProviderRouter(config=CortexConfig())
        m = MagicMock(spec=LLMProvider)
        m.PROVIDER_NAME = "mock"; m.complete = AsyncMock(return_value=_cr()); m.embed = AsyncMock(return_value=_er())
        m.extract = AsyncMock(return_value=_ex()); m.usage_summary = MagicMock(return_value={"calls": 0})
        r.provider = m; return r, m

    @pytest.mark.asyncio
    async def test_complete(self, rm):
        r, m = rm; res = await r.complete(system="s", user="u"); assert res.text == "ok"; m.complete.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_embed(self, rm):
        r, m = rm; res = await r.embed(texts=["hi"]); assert len(res.embeddings) == 1; m.embed.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_extract(self, rm):
        r, m = rm; res = await r.extract(system="s", user="u", schema={}); assert res.data == {"claims": []}; m.extract.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_no_provider_raises(self):
        r = ProviderRouter(config=CortexConfig()); r.provider = None
        with pytest.raises(RuntimeError, match="no provider configured"): await r.complete(system="s", user="u")

class TestProviderRouterConfigureFromHost:
    def test_configure_updates(self):
        r = ProviderRouter(config=CortexConfig())
        out = r.configure_from_host({"providers": {"openai": {"api_key": "sk", "base_url": ""}, "voyage": {"api_key": "pa", "base_url": ""}}})
        assert "openai" in out and "voyage" in out

    def test_configure_empty(self):
        assert ProviderRouter(config=CortexConfig()).configure_from_host({"providers": {}}) == []

class TestCortexPluginUsesProviderRouter:
    @patch("cortex.api.plugin.ProviderRouter")
    def test_plugin_creates_router(self, cls):
        cls.return_value = MagicMock(provider_names=MagicMock(return_value=["host"]))
        from cortex.api.plugin import CortexPlugin
        plugin = CortexPlugin(db_path=":memory:"); cls.assert_called_once()
        # ProviderRouter is called with config keyword arg
        assert cls.call_args[1]["config"] is plugin.config

    @patch("cortex.api.plugin.build_provider_cascade", side_effect=Exception("cascade also failed"))
    def test_plugin_records_error_when_both_fail(self, cascade_mock):
        """If both ProviderRouter and cascade fail, error is recorded and llm is None."""
        from cortex.api.plugin import CortexPlugin
        with patch("cortex.api.plugin.ProviderRouter", side_effect=Exception("router fail")):
            plugin = CortexPlugin(db_path=":memory:")
        assert plugin.llm is None
        assert any("llm" in e for e in plugin._init_errors)

class TestProviderRouterThroughModelRouter:
    def test_wraps_provider_router(self):
        cfg = CortexConfig(); r = ProviderRouter(config=cfg)
        from cortex.llm.router import ModelRouter
        assert ModelRouter(provider=r, config=cfg)._provider is r

    def test_configure_accessible(self):
        cfg = CortexConfig(); r = ProviderRouter(config=cfg)
        from cortex.llm.router import ModelRouter
        mr = ModelRouter(provider=r, config=cfg)
        inner = mr._provider if hasattr(mr, "_provider") else mr
        assert hasattr(inner, "configure_from_host") and isinstance(inner, ProviderRouter)

class TestEmbeddingPreserved:
    @pytest.mark.asyncio
    async def test_embed_passes_through(self):
        r = ProviderRouter(config=CortexConfig()); m = MagicMock(spec=LLMProvider); m.embed = AsyncMock(return_value=_er())
        r.provider = m; await r.embed(texts=["t"], model="voyage-code-3", input_type="document")
        m.embed.assert_awaited_once_with(texts=["t"], model="voyage-code-3", input_type="document")

class TestUsageSummary:
    def test_delegates(self):
        r = ProviderRouter(config=CortexConfig()); m = MagicMock(); m.usage_summary.return_value = {"calls": 5}
        r.provider = m; assert r.usage_summary()["calls"] == 5
    def test_no_provider(self):
        r = ProviderRouter(config=CortexConfig()); r.provider = None; assert isinstance(r.usage_summary(), dict)
