"""Edge case tests for rate limiting.

Tests the rate_limit_dependency from src/server/utils/dependencies.py
under various edge conditions: multiple IPs, window resets, bursts, etc.
"""

import time
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi import HTTPException

from src.server.utils.dependencies import (
    _RATE_LIMIT_BUCKETS,
    _RATE_LIMIT_WINDOW_SECONDS,
    RateLimitRecord,
    rate_limit_dependency,
)


def _make_request(ip: str = "127.0.0.1") -> MagicMock:
    """Create a mock Request with a given client IP."""
    req = MagicMock()
    req.client = MagicMock()
    req.client.host = ip
    return req


@pytest.fixture(autouse=True)
def _clear_buckets():
    """Clear rate limit buckets before each test."""
    _RATE_LIMIT_BUCKETS.clear()
    yield
    _RATE_LIMIT_BUCKETS.clear()


class TestRateLimitBasic:
    """Basic rate limit behaviour."""

    @pytest.mark.asyncio
    @patch("src.server.utils.dependencies.settings")
    async def test_first_request_passes(self, mock_settings):
        """First request from a new IP is always allowed."""
        mock_settings.api_rate_limit = 100
        await rate_limit_dependency(_make_request("10.0.0.1"))

    @pytest.mark.asyncio
    @patch("src.server.utils.dependencies.settings")
    async def test_exceeding_limit_raises_429(self, mock_settings):
        """Exceeding max requests within a window raises 429."""
        mock_settings.api_rate_limit = 3
        req = _make_request("10.0.0.2")
        for _ in range(3):
            await rate_limit_dependency(req)
        with pytest.raises(HTTPException) as exc_info:
            await rate_limit_dependency(req)
        assert exc_info.value.status_code == 429


class TestMultipleIPs:
    """Rate limiting with different client IPs."""

    @pytest.mark.asyncio
    @patch("src.server.utils.dependencies.settings")
    async def test_different_ips_have_separate_buckets(self, mock_settings):
        """Each IP has its own counter."""
        mock_settings.api_rate_limit = 2
        ip1 = _make_request("1.1.1.1")
        ip2 = _make_request("2.2.2.2")

        # Both use 2 requests
        await rate_limit_dependency(ip1)
        await rate_limit_dependency(ip1)
        await rate_limit_dependency(ip2)
        await rate_limit_dependency(ip2)

        # ip1 is at limit, ip2 is at limit
        with pytest.raises(HTTPException):
            await rate_limit_dependency(ip1)
        with pytest.raises(HTTPException):
            await rate_limit_dependency(ip2)

    @pytest.mark.asyncio
    @patch("src.server.utils.dependencies.settings")
    async def test_one_ip_limited_does_not_affect_other(self, mock_settings):
        """Blocking one IP doesn't block another."""
        mock_settings.api_rate_limit = 1
        ip_blocked = _make_request("10.0.0.1")
        ip_ok = _make_request("10.0.0.2")

        await rate_limit_dependency(ip_blocked)
        with pytest.raises(HTTPException):
            await rate_limit_dependency(ip_blocked)

        # Other IP still fine
        await rate_limit_dependency(ip_ok)


class TestWindowReset:
    """Rate limit window expiration and reset."""

    @pytest.mark.asyncio
    @patch("src.server.utils.dependencies.settings")
    async def test_requests_allowed_after_window_expires(self, mock_settings):
        """Counter resets when window expires."""
        mock_settings.api_rate_limit = 1
        req = _make_request("10.0.0.3")

        await rate_limit_dependency(req)
        with pytest.raises(HTTPException):
            await rate_limit_dependency(req)

        # Manually expire the window
        bucket = _RATE_LIMIT_BUCKETS.get("10.0.0.3")
        if bucket:
            bucket.window_start -= _RATE_LIMIT_WINDOW_SECONDS + 1

        # Should pass now
        await rate_limit_dependency(req)


class TestBurstTraffic:
    """Burst traffic handling."""

    @pytest.mark.asyncio
    @patch("src.server.utils.dependencies.settings")
    async def test_burst_up_to_limit_passes(self, mock_settings):
        """Rapid requests up to the limit all pass."""
        mock_settings.api_rate_limit = 50
        req = _make_request("10.0.0.4")
        for _ in range(50):
            await rate_limit_dependency(req)

    @pytest.mark.asyncio
    @patch("src.server.utils.dependencies.settings")
    async def test_burst_over_limit_blocked(self, mock_settings):
        """Request 51 in a burst of 50 is blocked."""
        mock_settings.api_rate_limit = 50
        req = _make_request("10.0.0.5")
        for _ in range(50):
            await rate_limit_dependency(req)
        with pytest.raises(HTTPException) as exc_info:
            await rate_limit_dependency(req)
        assert exc_info.value.status_code == 429


class TestMissingClient:
    """Requests without client information."""

    @pytest.mark.asyncio
    @patch("src.server.utils.dependencies.settings")
    async def test_no_client_uses_unknown_key(self, mock_settings):
        """When request.client is None, 'unknown' is used as key."""
        mock_settings.api_rate_limit = 100
        req = MagicMock()
        req.client = None
        await rate_limit_dependency(req)
        assert "unknown" in _RATE_LIMIT_BUCKETS

    @pytest.mark.asyncio
    @patch("src.server.utils.dependencies.settings")
    async def test_multiple_unknown_share_bucket(self, mock_settings):
        """All client-less requests share the 'unknown' bucket."""
        mock_settings.api_rate_limit = 2
        req = MagicMock()
        req.client = None
        await rate_limit_dependency(req)
        await rate_limit_dependency(req)
        with pytest.raises(HTTPException):
            await rate_limit_dependency(req)


class TestRateLimitRecord:
    """Unit tests for the RateLimitRecord dataclass."""

    def test_creation(self):
        """Can create a RateLimitRecord."""
        rec = RateLimitRecord(count=5, window_start=time.time())
        assert rec.count == 5

    def test_mutation(self):
        """Count can be incremented."""
        rec = RateLimitRecord(count=0, window_start=time.time())
        rec.count += 1
        assert rec.count == 1
