"""
Unit tests for the Redis token bucket rate limiter.

Tests CHAN-05: Rate limiting enforces per-tenant, per-channel limits.

These tests use fakeredis to run without a live Redis instance.
"""

from __future__ import annotations

import asyncio

import fakeredis
import pytest
import pytest_asyncio

from router.ratelimit import RateLimitExceeded, check_rate_limit


@pytest_asyncio.fixture
async def fake_redis():
    """Provide a fake async Redis client backed by fakeredis."""
    r = fakeredis.aioredis.FakeRedis(decode_responses=True)
    yield r
    await r.aclose()


class TestTokenBucketAllows:
    """Tests for requests within the rate limit."""

    async def test_single_request_allowed(self, fake_redis) -> None:
        """A single request is always allowed."""
        result = await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)
        assert result is True

    async def test_requests_under_limit_all_allowed(self, fake_redis) -> None:
        """All 29 requests within a 30-request limit are allowed."""
        for i in range(29):
            result = await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)
            assert result is True, f"Request {i + 1} was unexpectedly rejected"

    async def test_exactly_at_limit_allowed(self, fake_redis) -> None:
        """The 30th request (exactly at limit) is allowed."""
        for _ in range(30):
            result = await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)
            assert result is True


class TestTokenBucketRejects:
    """Tests for requests exceeding the rate limit."""

    async def test_31st_request_rejected(self, fake_redis) -> None:
        """The 31st request in a 30-request window is rejected."""
        # Fill up the bucket
        for _ in range(30):
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)

        # 31st request must raise
        with pytest.raises(RateLimitExceeded) as exc_info:
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)

        assert exc_info.value.tenant_id == "tenant_a"
        assert exc_info.value.channel == "slack"

    async def test_rate_limit_exceeded_has_remaining_seconds(self, fake_redis) -> None:
        """RateLimitExceeded must expose remaining_seconds attribute."""
        for _ in range(30):
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=30, window_seconds=60)

        with pytest.raises(RateLimitExceeded) as exc_info:
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=30, window_seconds=60)

        assert isinstance(exc_info.value.remaining_seconds, int)
        assert exc_info.value.remaining_seconds >= 0

    async def test_continued_requests_after_exceeded_also_rejected(self, fake_redis) -> None:
        """Additional requests after exceeding the limit continue to be rejected."""
        for _ in range(30):
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)

        for _ in range(5):
            with pytest.raises(RateLimitExceeded):
                await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)


class TestTokenBucketTenantIsolation:
    """Tests that rate limit counters are isolated per tenant."""

    async def test_tenant_a_limit_independent_of_tenant_b(self, fake_redis) -> None:
        """Exhausting tenant A's limit does not affect tenant B."""
        # Exhaust tenant A's limit
        for _ in range(30):
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)
        with pytest.raises(RateLimitExceeded):
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)

        # Tenant B should still be allowed
        result = await check_rate_limit("tenant_b", "slack", fake_redis, limit=30)
        assert result is True

    async def test_channel_limits_are_independent(self, fake_redis) -> None:
        """Exhausting Slack limit does not affect Telegram limit."""
        for _ in range(30):
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)
        with pytest.raises(RateLimitExceeded):
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=30)

        # A different channel should still be allowed
        result = await check_rate_limit("tenant_a", "telegram", fake_redis, limit=30)
        assert result is True

    async def test_multiple_tenants_independent_counters(self, fake_redis) -> None:
        """Multiple tenants maintain separate counter keys."""
        tenants = ["tenant_a", "tenant_b", "tenant_c"]
        # Each tenant makes 15 requests
        for tenant in tenants:
            for _ in range(15):
                result = await check_rate_limit(tenant, "slack", fake_redis, limit=30)
                assert result is True, f"Unexpected rejection for {tenant}"

        # None should be at the limit yet
        for tenant in tenants:
            result = await check_rate_limit(tenant, "slack", fake_redis, limit=30)
            assert result is True


class TestTokenBucketWindowReset:
    """Tests for rate limit window expiry."""

    async def test_limit_resets_after_window_expires(self, fake_redis) -> None:
        """After the TTL expires, the rate limit resets and requests are allowed again."""
        # Use a very short window (1 second) to test expiry
        for _ in range(5):
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=5, window_seconds=1)
        with pytest.raises(RateLimitExceeded):
            await check_rate_limit("tenant_a", "slack", fake_redis, limit=5, window_seconds=1)

        # Manually expire the key to simulate window reset
        from shared.redis_keys import rate_limit_key
        key = rate_limit_key("tenant_a", "slack")
        await fake_redis.delete(key)

        # After key expiry, requests should be allowed again
        result = await check_rate_limit("tenant_a", "slack", fake_redis, limit=5, window_seconds=1)
        assert result is True

    async def test_rate_limit_key_has_ttl(self, fake_redis) -> None:
        """Rate limit key must have a TTL set (window expiry)."""
        from shared.redis_keys import rate_limit_key
        key = rate_limit_key("tenant_a", "slack")

        await check_rate_limit("tenant_a", "slack", fake_redis, limit=30, window_seconds=60)

        ttl = await fake_redis.ttl(key)
        # TTL should be set (positive) — key will expire at end of window
        assert ttl > 0
        assert ttl <= 60
