"""Tests for circuit breaker: error classification and key blocking."""

import pytest

from llm_rotator._types import Candidate
from llm_rotator.backends import InMemoryBackend
from llm_rotator.circuit_breaker import CircuitBreaker
from llm_rotator.config import ClientType
from llm_rotator.exceptions import (
    KeyDeadError,
    ModelRateLimitError,
    ServerError,
)


def _make_candidate(
    key_alias: str = "main",
    model: str = "gpt-4o",
    provider_name: str = "openai",
) -> Candidate:
    return Candidate(
        provider_name=provider_name,
        model=model,
        key_alias=key_alias,
        key_token="sk-test",
        model_group="flagship",
        base_url="https://api.openai.com/v1",
        client_type=ClientType.OPENAI,
    )


@pytest.fixture
def backend():
    return InMemoryBackend(clock=lambda: _clock[0])


@pytest.fixture(autouse=True)
def _reset_clock():
    _clock[0] = 1000.0


_clock = [1000.0]


class TestCircuitBreakerClassification:
    async def test_key_dead_401(self, backend):
        cb = CircuitBreaker(backend)
        err = KeyDeadError(key_alias="main", status_code=401)
        candidate = _make_candidate()

        await cb.record_failure(candidate, err)

        assert await cb.is_candidate_available(candidate) is False

    async def test_key_dead_402(self, backend):
        cb = CircuitBreaker(backend)
        err = KeyDeadError(key_alias="main", status_code=402)
        candidate = _make_candidate()

        await cb.record_failure(candidate, err)

        # Dead globally — any model blocked
        assert await cb.is_candidate_available(candidate) is False
        other_model = _make_candidate(model="gpt-5")
        assert await cb.is_candidate_available(other_model) is False

    async def test_rate_limit_429_blocks_model_only(self, backend):
        cb = CircuitBreaker(backend)
        err = ModelRateLimitError(key_alias="main", model="gpt-5", retry_after=30)
        candidate = _make_candidate(model="gpt-5")

        await cb.record_failure(candidate, err)

        assert await cb.is_candidate_available(candidate) is False
        # Other model still available
        other = _make_candidate(model="gpt-4o")
        assert await cb.is_candidate_available(other) is True

    async def test_rate_limit_uses_retry_after(self, backend):
        cb = CircuitBreaker(backend)
        err = ModelRateLimitError(key_alias="main", model="gpt-5", retry_after=30)
        candidate = _make_candidate(model="gpt-5")

        await cb.record_failure(candidate, err)

        # Still blocked at t=1029
        _clock[0] = 1029.0
        assert await cb.is_candidate_available(candidate) is False

        # Available after TTL
        _clock[0] = 1031.0
        assert await cb.is_candidate_available(candidate) is True

    async def test_rate_limit_default_ttl_without_retry_after(self, backend):
        cb = CircuitBreaker(backend, default_block_ttl=60)
        err = ModelRateLimitError(key_alias="main", model="gpt-5", retry_after=None)
        candidate = _make_candidate(model="gpt-5")

        await cb.record_failure(candidate, err)

        assert await cb.is_candidate_available(candidate) is False
        _clock[0] = 1061.0
        assert await cb.is_candidate_available(candidate) is True

    async def test_server_error_blocks_model(self, backend):
        cb = CircuitBreaker(backend)
        err = ServerError(provider="openai", status_code=500)
        candidate = _make_candidate(model="gpt-4o")

        await cb.record_failure(candidate, err)

        assert await cb.is_candidate_available(candidate) is False
        # Other model still available
        other = _make_candidate(model="gpt-5")
        assert await cb.is_candidate_available(other) is True

    async def test_server_error_ttl_expiry(self, backend):
        cb = CircuitBreaker(backend, default_block_ttl=30)
        err = ServerError(provider="openai", status_code=503)
        candidate = _make_candidate()

        await cb.record_failure(candidate, err)

        _clock[0] = 1031.0
        assert await cb.is_candidate_available(candidate) is True

    async def test_healthy_candidate_available(self, backend):
        cb = CircuitBreaker(backend)
        candidate = _make_candidate()
        assert await cb.is_candidate_available(candidate) is True

    async def test_different_keys_independent(self, backend):
        cb = CircuitBreaker(backend)
        err = ModelRateLimitError(key_alias="key1", model="gpt-5", retry_after=60)
        c1 = _make_candidate(key_alias="key1", model="gpt-5")
        c2 = _make_candidate(key_alias="key2", model="gpt-5")

        await cb.record_failure(c1, err)

        assert await cb.is_candidate_available(c1) is False
        assert await cb.is_candidate_available(c2) is True
