"""Tests for state backends (InMemoryBackend)."""

import asyncio

import pytest

from llm_rotator.backends import AbstractStateBackend, InMemoryBackend


class TestAbstractStateBackend:
    def test_cannot_instantiate(self):
        with pytest.raises(TypeError):
            AbstractStateBackend()


class TestInMemoryBackend:
    def _make_backend(self, clock_time: float = 1000.0) -> tuple[InMemoryBackend, list[float]]:
        """Create backend with controllable clock. Returns (backend, time_holder)."""
        time_holder = [clock_time]
        backend = InMemoryBackend(clock=lambda: time_holder[0])
        return backend, time_holder

    async def test_key_available_by_default(self):
        backend, _ = self._make_backend()
        assert await backend.is_key_available("key1", "gpt-4o") is True

    async def test_block_key_for_model_granular(self):
        backend, _ = self._make_backend()
        await backend.block_key_for_model("key1", "gpt-5", ttl_seconds=60)
        assert await backend.is_key_available("key1", "gpt-5") is False
        assert await backend.is_key_available("key1", "gpt-4o") is True

    async def test_mark_key_dead_global(self):
        backend, _ = self._make_backend()
        await backend.mark_key_dead("key1")
        assert await backend.is_key_available("key1", "gpt-5") is False
        assert await backend.is_key_available("key1", "gpt-4o") is False
        assert await backend.is_key_available("key1", "any-model") is False

    async def test_dead_key_not_affected_by_model_block(self):
        """Once a key is dead, model-level blocks don't change anything."""
        backend, _ = self._make_backend()
        await backend.mark_key_dead("key1")
        await backend.block_key_for_model("key1", "gpt-5", ttl_seconds=60)
        assert await backend.is_key_available("key1", "gpt-5") is False

    async def test_ttl_expiry_model_block(self):
        backend, time_holder = self._make_backend(clock_time=1000.0)
        await backend.block_key_for_model("key1", "gpt-5", ttl_seconds=60)
        assert await backend.is_key_available("key1", "gpt-5") is False

        # Advance time past TTL
        time_holder[0] = 1061.0
        assert await backend.is_key_available("key1", "gpt-5") is True

    async def test_ttl_not_expired_yet(self):
        backend, time_holder = self._make_backend(clock_time=1000.0)
        await backend.block_key_for_model("key1", "gpt-5", ttl_seconds=60)

        # Still within TTL
        time_holder[0] = 1059.0
        assert await backend.is_key_available("key1", "gpt-5") is False

    async def test_dead_key_is_permanent(self):
        """Dead keys don't have TTL — they stay dead."""
        backend, time_holder = self._make_backend(clock_time=1000.0)
        await backend.mark_key_dead("key1")

        time_holder[0] = 999999.0
        assert await backend.is_key_available("key1", "gpt-5") is False

    async def test_multiple_model_blocks_independent(self):
        backend, time_holder = self._make_backend(clock_time=1000.0)
        await backend.block_key_for_model("key1", "gpt-5", ttl_seconds=30)
        await backend.block_key_for_model("key1", "gpt-4o", ttl_seconds=90)

        time_holder[0] = 1035.0  # past gpt-5 block, within gpt-4o block
        assert await backend.is_key_available("key1", "gpt-5") is True
        assert await backend.is_key_available("key1", "gpt-4o") is False

    async def test_different_keys_independent(self):
        backend, _ = self._make_backend()
        await backend.block_key_for_model("key1", "gpt-5", ttl_seconds=60)
        assert await backend.is_key_available("key1", "gpt-5") is False
        assert await backend.is_key_available("key2", "gpt-5") is True

    # --- Quota ---

    async def test_quota_initial_usage_zero(self):
        backend, _ = self._make_backend()
        usage = await backend.get_quota_usage("scope1")
        assert usage == 0

    async def test_quota_increment(self):
        backend, _ = self._make_backend()
        result = await backend.increment_quota("scope1", amount=100, ttl_seconds=3600)
        assert result == 100

    async def test_quota_accumulates(self):
        backend, _ = self._make_backend()
        await backend.increment_quota("scope1", amount=100, ttl_seconds=3600)
        result = await backend.increment_quota("scope1", amount=50, ttl_seconds=3600)
        assert result == 150
        assert await backend.get_quota_usage("scope1") == 150

    async def test_quota_ttl_resets(self):
        backend, time_holder = self._make_backend(clock_time=1000.0)
        await backend.increment_quota("scope1", amount=100, ttl_seconds=60)
        assert await backend.get_quota_usage("scope1") == 100

        # Advance past TTL
        time_holder[0] = 1061.0
        assert await backend.get_quota_usage("scope1") == 0

    async def test_quota_ttl_not_expired(self):
        backend, time_holder = self._make_backend(clock_time=1000.0)
        await backend.increment_quota("scope1", amount=100, ttl_seconds=60)

        time_holder[0] = 1050.0
        assert await backend.get_quota_usage("scope1") == 100

    async def test_quota_different_scopes_independent(self):
        backend, _ = self._make_backend()
        await backend.increment_quota("scope_a", amount=100, ttl_seconds=3600)
        await backend.increment_quota("scope_b", amount=200, ttl_seconds=3600)
        assert await backend.get_quota_usage("scope_a") == 100
        assert await backend.get_quota_usage("scope_b") == 200

    async def test_quota_increment_after_ttl_resets(self):
        """After TTL expires, new increments start from zero."""
        backend, time_holder = self._make_backend(clock_time=1000.0)
        await backend.increment_quota("scope1", amount=100, ttl_seconds=60)

        time_holder[0] = 1061.0
        result = await backend.increment_quota("scope1", amount=50, ttl_seconds=60)
        assert result == 50

    async def test_quota_concurrent_atomic_increments(self):
        """100 concurrent increments should produce exactly 100."""
        backend, _ = self._make_backend()
        results = await asyncio.gather(
            *[backend.increment_quota("scope1", amount=1, ttl_seconds=3600) for _ in range(100)]
        )
        assert max(results) == 100
        assert await backend.get_quota_usage("scope1") == 100
