"""Tests for at-capacity cloud fallback in the dispatcher."""

from __future__ import annotations

from collections import deque

import pytest

from llmhosts.config import FallbackConfig, LLMHostsConfig


@pytest.fixture
def fallback_config() -> LLMHostsConfig:
    """Config with fallback enabled."""
    cfg = LLMHostsConfig()
    cfg.fallback = FallbackConfig(
        enabled=True,
        queue_depth_threshold=5,
        latency_threshold_ms=3000,
        latency_window_size=20,
        model_mapping={
            "qwen3:14b": "gpt-4o-mini",
            "qwen3:32b": "gpt-4o",
        },
    )
    return cfg


@pytest.fixture
def disabled_config() -> LLMHostsConfig:
    """Config with fallback disabled."""
    cfg = LLMHostsConfig()
    cfg.fallback = FallbackConfig(enabled=False)
    return cfg


class TestFallbackConfig:
    """Tests for FallbackConfig model."""

    def test_default_disabled(self) -> None:
        cfg = FallbackConfig()
        assert cfg.enabled is False

    def test_default_thresholds(self) -> None:
        cfg = FallbackConfig()
        assert cfg.queue_depth_threshold == 10
        assert cfg.latency_threshold_ms == 5000
        assert cfg.latency_window_size == 50

    def test_default_model_mapping(self) -> None:
        cfg = FallbackConfig()
        assert "qwen3:14b" in cfg.model_mapping
        assert cfg.model_mapping["qwen3:14b"] == "gpt-4o-mini"

    def test_custom_config(self, fallback_config: LLMHostsConfig) -> None:
        assert fallback_config.fallback.enabled is True
        assert fallback_config.fallback.queue_depth_threshold == 5

    def test_in_root_config(self) -> None:
        cfg = LLMHostsConfig()
        assert hasattr(cfg, "fallback")
        assert isinstance(cfg.fallback, FallbackConfig)


class TestCapacityDetection:
    """Tests for _is_at_capacity logic."""

    def test_not_at_capacity_when_disabled(self, disabled_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = disabled_config
        dispatcher._active_requests = 100  # Even at high load
        dispatcher._latency_history = deque(maxlen=50)
        assert not dispatcher._is_at_capacity()

    def test_at_capacity_by_queue_depth(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        dispatcher._active_requests = 5  # Equal to threshold
        dispatcher._latency_history = deque(maxlen=20)
        assert dispatcher._is_at_capacity()

    def test_at_capacity_above_threshold(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        dispatcher._active_requests = 10  # Above threshold
        dispatcher._latency_history = deque(maxlen=20)
        assert dispatcher._is_at_capacity()

    def test_not_at_capacity_below_threshold(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        dispatcher._active_requests = 2
        dispatcher._latency_history = deque(maxlen=20)
        assert not dispatcher._is_at_capacity()

    def test_at_capacity_by_latency(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        dispatcher._active_requests = 0
        # Fill with high latencies
        dispatcher._latency_history = deque([4000.0] * 20, maxlen=20)
        assert dispatcher._is_at_capacity()

    def test_not_at_capacity_with_low_latency(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        dispatcher._active_requests = 0
        dispatcher._latency_history = deque([100.0] * 20, maxlen=20)
        assert not dispatcher._is_at_capacity()

    def test_not_at_capacity_with_few_samples(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        dispatcher._active_requests = 0
        # Only 5 samples -- below the 10 minimum for latency check
        dispatcher._latency_history = deque([5000.0] * 5, maxlen=20)
        assert not dispatcher._is_at_capacity()

    def test_mixed_latencies_p95_above(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        dispatcher._active_requests = 0
        # 18 fast + 2 slow -- p95 hits the slow ones
        latencies = [100.0] * 18 + [4000.0] * 2
        dispatcher._latency_history = deque(latencies, maxlen=20)
        assert dispatcher._is_at_capacity()

    def test_mixed_latencies_p95_below(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        dispatcher._active_requests = 0
        # All fast -- p95 well below threshold
        latencies = [100.0] * 20
        dispatcher._latency_history = deque(latencies, maxlen=20)
        assert not dispatcher._is_at_capacity()


class TestFallbackModelMapping:
    """Tests for cloud model fallback mapping."""

    def test_known_model_mapping(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        assert dispatcher._get_fallback_model("qwen3:14b") == "gpt-4o-mini"
        assert dispatcher._get_fallback_model("qwen3:32b") == "gpt-4o"

    def test_unknown_model_returns_none(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        assert dispatcher._get_fallback_model("unknown-model") is None

    def test_disabled_returns_none(self, disabled_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = disabled_config
        assert dispatcher._get_fallback_model("qwen3:14b") is None

    def test_default_mapping_entries(self) -> None:
        cfg = FallbackConfig(enabled=True)
        assert cfg.model_mapping["qwen3:14b"] == "gpt-4o-mini"
        assert cfg.model_mapping["qwen3:32b"] == "gpt-4o"
        assert cfg.model_mapping["llama3.3:70b"] == "gpt-4o"
        assert cfg.model_mapping["qwen3:8b"] == "gpt-4o-mini"
        assert cfg.model_mapping["qwen3:4b"] == "gpt-4o-mini"


class TestFallbackHeader:
    """Tests for X-LLMHosts-Fallback response header."""

    def test_fallback_header_format(self) -> None:
        original = "qwen3:14b"
        cloud = "gpt-4o-mini"
        header = f"true; original={original}; cloud={cloud}"
        assert "true" in header
        assert "qwen3:14b" in header
        assert "gpt-4o-mini" in header

    def test_no_header_when_no_fallback(self) -> None:
        headers: dict[str, str] = {}
        # When no fallback occurs, header should not be present
        assert "X-LLMHosts-Fallback" not in headers

    def test_fallback_info_in_response_extra(self) -> None:
        from llmhosts.proxy.models import UnifiedResponse

        resp = UnifiedResponse(
            content="hello",
            extra={"fallback_info": {"original": "qwen3:14b", "cloud": "gpt-4o-mini"}},
        )
        info = resp.extra["fallback_info"]
        assert info["original"] == "qwen3:14b"
        assert info["cloud"] == "gpt-4o-mini"


class TestLatencyTracking:
    """Tests for latency history tracking."""

    def test_latency_window_bounded(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._config = fallback_config
        dispatcher._latency_history = deque(maxlen=20)

        # Add more than window size
        for i in range(50):
            dispatcher._latency_history.append(float(i))

        assert len(dispatcher._latency_history) == 20

    def test_p95_calculation(self) -> None:
        latencies = sorted([100.0] * 19 + [5000.0])
        p95_idx = int(len(latencies) * 0.95)
        p95 = latencies[p95_idx]
        assert p95 == 5000.0

    def test_active_request_counter(self) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._active_requests = 0

        dispatcher._active_requests += 1
        assert dispatcher._active_requests == 1

        dispatcher._active_requests += 1
        assert dispatcher._active_requests == 2

        dispatcher._active_requests -= 1
        assert dispatcher._active_requests == 1

    def test_init_creates_tracking_attrs(self) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher()
        assert dispatcher._active_requests == 0
        assert isinstance(dispatcher._latency_history, deque)
        assert dispatcher._latency_history.maxlen == 50  # default window size

    def test_init_with_custom_config(self, fallback_config: LLMHostsConfig) -> None:
        from llmhosts.proxy.dispatcher import BackendDispatcher

        dispatcher = BackendDispatcher(config=fallback_config)
        assert dispatcher._latency_history.maxlen == 20  # custom window size
        assert dispatcher._config.fallback.enabled is True
