"""Integration tests for RedisBackend using fakeredis."""

from __future__ import annotations

import asyncio

import fakeredis.aioredis
import pytest

from llm_rotator.backends.redis import RedisBackend


@pytest.fixture
def redis_backend() -> RedisBackend:
    """Create a RedisBackend backed by fakeredis."""
    fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
    return RedisBackend(redis=fake_redis, prefix="test:")


# ---------------------------------------------------------------------------
# Circuit Breaker
# ---------------------------------------------------------------------------


class TestRedisBackendCircuitBreaker:
    async def test_key_available_by_default(self, redis_backend: RedisBackend) -> None:
        assert await redis_backend.is_key_available("key1", "gpt-4o") is True

    async def test_block_key_for_model_granular(self, redis_backend: RedisBackend) -> None:
        await redis_backend.block_key_for_model("key1", "gpt-5", ttl_seconds=60)
        assert await redis_backend.is_key_available("key1", "gpt-5") is False
        assert await redis_backend.is_key_available("key1", "gpt-4o") is True

    async def test_mark_key_dead_global(self, redis_backend: RedisBackend) -> None:
        await redis_backend.mark_key_dead("key1")
        assert await redis_backend.is_key_available("key1", "gpt-5") is False
        assert await redis_backend.is_key_available("key1", "gpt-4o") is False
        assert await redis_backend.is_key_available("key1", "any-model") is False

    async def test_dead_key_not_affected_by_model_block(
        self, redis_backend: RedisBackend
    ) -> None:
        await redis_backend.mark_key_dead("key1")
        await redis_backend.block_key_for_model("key1", "gpt-5", ttl_seconds=60)
        assert await redis_backend.is_key_available("key1", "gpt-5") is False

    async def test_dead_key_persistent(self, redis_backend: RedisBackend) -> None:
        """Dead key has no TTL — persists indefinitely."""
        await redis_backend.mark_key_dead("key1")
        # Verify the key has no TTL (persist = -1 in Redis)
        ttl = await redis_backend._redis.ttl("test:dead:key1")
        assert ttl == -1  # No expiry

    async def test_multiple_model_blocks_independent(
        self, redis_backend: RedisBackend
    ) -> None:
        await redis_backend.block_key_for_model("key1", "gpt-5", ttl_seconds=30)
        await redis_backend.block_key_for_model("key1", "gpt-4o", ttl_seconds=90)
        assert await redis_backend.is_key_available("key1", "gpt-5") is False
        assert await redis_backend.is_key_available("key1", "gpt-4o") is False

    async def test_different_keys_independent(self, redis_backend: RedisBackend) -> None:
        await redis_backend.block_key_for_model("key1", "gpt-5", ttl_seconds=60)
        assert await redis_backend.is_key_available("key1", "gpt-5") is False
        assert await redis_backend.is_key_available("key2", "gpt-5") is True


# ---------------------------------------------------------------------------
# Quota Counters
# ---------------------------------------------------------------------------


class TestRedisBackendQuota:
    async def test_quota_initial_usage_zero(self, redis_backend: RedisBackend) -> None:
        assert await redis_backend.get_quota_usage("scope1") == 0

    async def test_quota_increment(self, redis_backend: RedisBackend) -> None:
        result = await redis_backend.increment_quota("scope1", amount=100, ttl_seconds=3600)
        assert result == 100

    async def test_quota_accumulates(self, redis_backend: RedisBackend) -> None:
        await redis_backend.increment_quota("scope1", amount=100, ttl_seconds=3600)
        result = await redis_backend.increment_quota("scope1", amount=50, ttl_seconds=3600)
        assert result == 150
        assert await redis_backend.get_quota_usage("scope1") == 150

    async def test_quota_different_scopes_independent(
        self, redis_backend: RedisBackend
    ) -> None:
        await redis_backend.increment_quota("scope_a", amount=100, ttl_seconds=3600)
        await redis_backend.increment_quota("scope_b", amount=200, ttl_seconds=3600)
        assert await redis_backend.get_quota_usage("scope_a") == 100
        assert await redis_backend.get_quota_usage("scope_b") == 200

    async def test_quota_ttl_respected(self, redis_backend: RedisBackend) -> None:
        """Quota key should have a TTL set."""
        await redis_backend.increment_quota("scope1", amount=100, ttl_seconds=3600)
        ttl = await redis_backend._redis.ttl("test:quota:scope1")
        assert 0 < ttl <= 3600

    async def test_concurrent_quota_atomic(self, redis_backend: RedisBackend) -> None:
        """100 concurrent INCRBY should produce exactly 100."""
        results = await asyncio.gather(
            *[
                redis_backend.increment_quota("scope1", amount=1, ttl_seconds=3600)
                for _ in range(100)
            ]
        )
        assert max(results) == 100
        assert await redis_backend.get_quota_usage("scope1") == 100


# ---------------------------------------------------------------------------
# Key prefix
# ---------------------------------------------------------------------------


class TestRedisBackendKeyPrefix:
    async def test_key_prefix(self) -> None:
        """All keys should use the configured prefix."""
        fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
        backend = RedisBackend(redis=fake_redis, prefix="myapp:")

        await backend.mark_key_dead("k1")
        await backend.block_key_for_model("k2", "gpt-4o", ttl_seconds=60)
        await backend.increment_quota("s1", amount=10, ttl_seconds=3600)

        keys = [k async for k in fake_redis.scan_iter("*")]
        for k in keys:
            assert k.startswith("myapp:"), f"Key {k!r} doesn't have expected prefix"

    async def test_custom_prefix(self) -> None:
        """Different prefixes isolate data."""
        fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
        b1 = RedisBackend(redis=fake_redis, prefix="app1:")
        b2 = RedisBackend(redis=fake_redis, prefix="app2:")

        await b1.mark_key_dead("shared_key")
        assert await b1.is_key_available("shared_key", "gpt-4o") is False
        assert await b2.is_key_available("shared_key", "gpt-4o") is True


# ---------------------------------------------------------------------------
# Connection: from_url
# ---------------------------------------------------------------------------


class TestRedisBackendFromUrl:
    async def test_from_url(self) -> None:
        """from_url creates a working backend (uses fakeredis monkey-patch)."""
        # We can't easily test from_url with fakeredis without monkeypatching,
        # so we just test the constructor path directly.
        fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
        backend = RedisBackend(redis=fake_redis)
        await backend.increment_quota("test", amount=1, ttl_seconds=60)
        assert await backend.get_quota_usage("test") == 1


# ---------------------------------------------------------------------------
# Connection error handling
# ---------------------------------------------------------------------------


class TestRedisBackendConnectionError:
    async def test_connection_error_raises(self) -> None:
        """Operations on a disconnected client raise an error."""
        import redis.asyncio as aioredis

        # Use a non-existent host to trigger connection error
        client = aioredis.Redis(host="nonexistent-host-12345", port=9999, socket_timeout=0.1)
        backend = RedisBackend(redis=client)

        with pytest.raises((ConnectionError, OSError, Exception)):
            await backend.get_quota_usage("scope1")
