"""Tests for orchestration.providers.router and registry."""

from __future__ import annotations

import pytest

from orxaq.orchestration.providers.base import LLMProvider, LLMResponse
from orxaq.orchestration.providers.registry import ProviderRegistry
from orxaq.orchestration.providers.router import (
    DEFAULT_PRIORITY,
    TASK_ROUTES,
    ProviderRouter,
)

# ── Mock Providers ────────────────────────────────────────────


class FakeProvider(LLMProvider):
    """Deterministic provider for testing."""

    def __init__(self, name: str, healthy: bool = True) -> None:
        self.name = name
        self.models = [f"{name}-model"]
        self._healthy = healthy
        self._fail_generate = False

    def generate(
        self,
        prompt: str,
        model: str | None = None,
        max_tokens: int = 1024,
        temperature: float = 0.7,
    ) -> LLMResponse:
        if self._fail_generate:
            return LLMResponse(
                text="",
                model=model or self.models[0],
                provider=self.name,
                metadata={"error": f"{self.name} failed"},
            )
        return LLMResponse(
            text=f"response from {self.name}",
            model=model or self.models[0],
            provider=self.name,
            input_tokens=10,
            output_tokens=20,
            latency_ms=50.0,
        )

    def health_check(self) -> dict:
        return {
            "healthy": self._healthy,
            "provider": self.name,
            "models": self.models,
        }


# ── Fixtures ──────────────────────────────────────────────────


@pytest.fixture()
def registry() -> ProviderRegistry:
    reg = ProviderRegistry()
    reg.register(FakeProvider("vllm"))
    reg.register(FakeProvider("anthropic"))
    reg.register(FakeProvider("openai"))
    return reg


@pytest.fixture()
def router(registry: ProviderRegistry) -> ProviderRouter:
    return ProviderRouter(registry)


# ── ProviderRegistry ─────────────────────────────────────────


class TestProviderRegistry:
    def test_register_and_get(self) -> None:
        reg = ProviderRegistry()
        p = FakeProvider("test")
        reg.register(p)
        assert reg.get("test") is p

    def test_get_nonexistent(self) -> None:
        reg = ProviderRegistry()
        assert reg.get("missing") is None

    def test_list_providers_sorted(self, registry: ProviderRegistry) -> None:
        assert registry.list_providers() == ["anthropic", "openai", "vllm"]

    def test_all_providers(self, registry: ProviderRegistry) -> None:
        providers = registry.all_providers()
        assert len(providers) == 3
        names = {p.name for p in providers}
        assert names == {"vllm", "anthropic", "openai"}

    def test_health_check_all(self, registry: ProviderRegistry) -> None:
        results = registry.health_check_all()
        assert len(results) == 3
        for name in ["vllm", "anthropic", "openai"]:
            assert results[name]["healthy"] is True

    def test_available_models(self, registry: ProviderRegistry) -> None:
        models = registry.available_models()
        assert "vllm" in models
        assert models["vllm"] == ["vllm-model"]

    def test_remove_existing(self, registry: ProviderRegistry) -> None:
        assert registry.remove("vllm") is True
        assert registry.get("vllm") is None
        assert len(registry) == 2

    def test_remove_nonexistent(self, registry: ProviderRegistry) -> None:
        assert registry.remove("missing") is False

    def test_len(self, registry: ProviderRegistry) -> None:
        assert len(registry) == 3

    def test_contains(self, registry: ProviderRegistry) -> None:
        assert "vllm" in registry
        assert "missing" not in registry

    def test_default_creates_three_providers(self) -> None:
        reg = ProviderRegistry.default()
        assert len(reg) == 3
        assert "vllm" in reg
        assert "anthropic" in reg
        assert "openai" in reg


# ── ProviderRouter: Candidate Selection ──────────────────────


class TestRouterCandidates:
    def test_candidates_no_task_type(self, router: ProviderRouter) -> None:
        candidates = router._candidates()
        names = [c.name for c in candidates]
        assert names == ["vllm", "anthropic", "openai"]

    def test_candidates_fast_task(self, router: ProviderRouter) -> None:
        candidates = router._candidates("fast")
        names = [c.name for c in candidates]
        assert names[0] == "vllm"

    def test_candidates_reasoning_task(self, router: ProviderRouter) -> None:
        candidates = router._candidates("reasoning")
        names = [c.name for c in candidates]
        assert names[0] == "anthropic"
        assert names[1] == "openai"

    def test_candidates_unknown_task_uses_default(self, router: ProviderRouter) -> None:
        candidates = router._candidates("unknown_task")
        names = [c.name for c in candidates]
        assert names == ["vllm", "anthropic", "openai"]

    def test_candidates_includes_unregistered_providers(self) -> None:
        reg = ProviderRegistry()
        reg.register(FakeProvider("custom"))
        router = ProviderRouter(reg)
        candidates = router._candidates()
        names = [c.name for c in candidates]
        assert "custom" in names

    def test_custom_priority(self, registry: ProviderRegistry) -> None:
        router = ProviderRouter(registry, priority=["openai", "vllm"])
        candidates = router._candidates()
        names = [c.name for c in candidates]
        assert names[0] == "openai"
        assert names[1] == "vllm"


# ── ProviderRouter: Route ────────────────────────────────────


class TestRouterRoute:
    def test_route_success(self, router: ProviderRouter) -> None:
        response = router.route("Hello")
        assert response.text == "response from vllm"
        assert response.provider == "vllm"

    def test_route_fallback_on_failure(self, registry: ProviderRegistry) -> None:
        failing_vllm = FakeProvider("vllm")
        failing_vllm._fail_generate = True
        registry._providers["vllm"] = failing_vllm

        router = ProviderRouter(registry)
        response = router.route("Hello")
        assert response.provider == "anthropic"
        assert response.text == "response from anthropic"

    def test_route_all_fail(self) -> None:
        reg = ProviderRegistry()
        p1 = FakeProvider("a")
        p1._fail_generate = True
        p2 = FakeProvider("b")
        p2._fail_generate = True
        reg.register(p1)
        reg.register(p2)

        router = ProviderRouter(reg, priority=["a", "b"])
        response = router.route("Hello")
        assert response.text == ""
        assert "error" in response.metadata
        assert response.metadata["error"] == "all providers failed"

    def test_route_no_providers(self) -> None:
        reg = ProviderRegistry()
        router = ProviderRouter(reg)
        response = router.route("Hello")
        assert response.text == ""
        assert "error" in response.metadata

    def test_route_with_task_type(self, router: ProviderRouter) -> None:
        response = router.route("Hello", task_type="fast")
        assert response.provider == "vllm"

    def test_route_with_model_override(self, router: ProviderRouter) -> None:
        response = router.route("Hello", model="custom-model")
        assert response.model == "custom-model"


# ── ProviderRouter: Health & Stats ───────────────────────────


class TestRouterStats:
    def test_health_check_delegates(self, router: ProviderRouter) -> None:
        results = router.health_check()
        assert "vllm" in results

    def test_stats_empty(self, router: ProviderRouter) -> None:
        stats = router.stats()
        assert stats["latency"] == {}
        assert stats["failures"] == {}
        assert stats["priority"] == DEFAULT_PRIORITY

    def test_stats_after_routing(self, router: ProviderRouter) -> None:
        router.route("Hello")
        stats = router.stats()
        assert "vllm" in stats["latency"]
        assert stats["latency"]["vllm"]["calls"] == 1
        assert stats["latency"]["vllm"]["avg_ms"] > 0

    def test_stats_tracks_failures(self) -> None:
        reg = ProviderRegistry()
        failing = FakeProvider("failing")
        failing._fail_generate = True
        ok = FakeProvider("ok")
        reg.register(failing)
        reg.register(ok)

        router = ProviderRouter(reg, priority=["failing", "ok"])
        router.route("Hello")
        stats = router.stats()
        assert stats["failures"]["failing"] == 1
        assert stats["failures"].get("ok", 0) == 0


# ── Constants ────────────────────────────────────────────────


class TestConstants:
    def test_default_priority_has_all_providers(self) -> None:
        assert "vllm" in DEFAULT_PRIORITY
        assert "anthropic" in DEFAULT_PRIORITY
        assert "openai" in DEFAULT_PRIORITY

    def test_task_routes_has_fast(self) -> None:
        assert "fast" in TASK_ROUTES
        assert TASK_ROUTES["fast"] == ["vllm"]

    def test_task_routes_has_reasoning(self) -> None:
        assert "reasoning" in TASK_ROUTES
        assert "anthropic" in TASK_ROUTES["reasoning"]


# ── ProviderRouter: Cache ───────────────────────────────────


class TestRouterCache:
    def test_cache_hit_returns_same_response(self, registry: ProviderRegistry) -> None:
        router = ProviderRouter(registry, enable_cache=True)
        r1 = router.route("test prompt", model="m1")
        r2 = router.route("test prompt", model="m1")
        assert r1.text == r2.text
        assert r1.provider == r2.provider
        stats = router.stats()
        assert stats["cache"]["hits"] == 1
        assert stats["cache"]["misses"] == 1

    def test_cache_miss_on_different_prompt(self, registry: ProviderRegistry) -> None:
        router = ProviderRouter(registry, enable_cache=True)
        r1 = router.route("prompt A")
        r2 = router.route("prompt B")
        assert r1.text == r2.text  # Same provider, same text template
        stats = router.stats()
        assert stats["cache"]["hits"] == 0
        assert stats["cache"]["misses"] == 2

    def test_cache_miss_on_different_model(self, registry: ProviderRegistry) -> None:
        router = ProviderRouter(registry, enable_cache=True)
        router.route("test", model="m1")
        router.route("test", model="m2")
        stats = router.stats()
        assert stats["cache"]["hits"] == 0
        assert stats["cache"]["misses"] == 2

    def test_cache_disabled(self, registry: ProviderRegistry) -> None:
        router = ProviderRouter(registry, enable_cache=False)
        router.route("test")
        router.route("test")
        stats = router.stats()
        assert stats["cache"]["enabled"] is False
        assert stats["cache"]["hits"] == 0
        assert stats["cache"]["misses"] == 0

    def test_clear_cache(self, registry: ProviderRegistry) -> None:
        router = ProviderRouter(registry, enable_cache=True)
        router.route("test")
        assert router.stats()["cache"]["entries"] == 1
        router.clear_cache()
        assert router.stats()["cache"]["entries"] == 0

    def test_cache_maxsize_eviction(self, registry: ProviderRegistry) -> None:
        router = ProviderRouter(registry, enable_cache=True, cache_maxsize=2)
        router.route("prompt_1")
        router.route("prompt_2")
        router.route("prompt_3")
        stats = router.stats()
        assert stats["cache"]["entries"] == 2
        assert stats["cache"]["evictions"] >= 1

    def test_cache_ttl_expiry(self, registry: ProviderRegistry) -> None:
        router = ProviderRouter(registry, enable_cache=True, cache_ttl=0.001)
        router.route("test")
        import time

        time.sleep(0.01)
        # Second call should miss because TTL expired.
        router.route("test")
        stats = router.stats()
        assert stats["cache"]["misses"] == 2

    def test_cache_does_not_cache_errors(self) -> None:
        reg = ProviderRegistry()
        failing = FakeProvider("fail")
        failing._fail_generate = True
        reg.register(failing)
        router = ProviderRouter(reg, priority=["fail"], enable_cache=True)
        router.route("test")
        stats = router.stats()
        # Error responses are not cached.
        assert stats["cache"]["entries"] == 0

    def test_stats_includes_cache_hit_rate(self, registry: ProviderRegistry) -> None:
        router = ProviderRouter(registry, enable_cache=True)
        router.route("A")
        router.route("A")
        router.route("B")
        stats = router.stats()
        assert stats["cache"]["hit_rate"] == pytest.approx(1 / 3)

    def test_cache_registered_in_kernel_registry(self, registry: ProviderRegistry) -> None:
        from orxaq.kernel.cache import get_registry

        router = ProviderRouter(registry, enable_cache=True)
        kernel_cache = get_registry().get("provider_router")
        assert kernel_cache is router._cache
