# tests/unit/middleware/test_rate_limiter.py
"""
Comprehensive unit tests for RateLimiter.

Tests cover:
- Request allowance within limits
- Request blocking when exceeding limits
- Sliding window algorithm
- Cleanup of old entries
- Client statistics
- Edge cases and concurrency scenarios
"""

from collections import deque
from datetime import UTC, datetime, timedelta

import pytest

from middleware.rate_limiter import RateLimiter


@pytest.mark.unit
@pytest.mark.auth
class TestRateLimiterBasic:
    """Test suite for basic rate limiter functionality."""

    def test_rate_limiter_initialization(self):
        """Test rate limiter initializes correctly."""
        limiter = RateLimiter()

        assert isinstance(limiter.clients, dict)
        assert limiter.cleanup_interval == 3600
        assert isinstance(limiter.last_cleanup, datetime)

    def test_allow_first_request(self):
        """Test rate limiter allows first request."""
        limiter = RateLimiter()
        client_id = "test_client_1"

        result = limiter.allow_request(client_id, max_requests=10, window_seconds=3600)

        assert result is True
        assert client_id in limiter.clients
        assert len(limiter.clients[client_id]) == 1

    def test_allow_multiple_requests_within_limit(self):
        """Test rate limiter allows multiple requests within limit."""
        limiter = RateLimiter()
        client_id = "test_client_2"
        max_requests = 10

        # Make 10 requests (at the limit)
        for i in range(max_requests):
            result = limiter.allow_request(client_id, max_requests, 3600)
            assert result is True, f"Request {i + 1} should be allowed"

        assert len(limiter.clients[client_id]) == max_requests

    def test_block_request_exceeding_limit(self):
        """Test rate limiter blocks requests exceeding limit."""
        limiter = RateLimiter()
        client_id = "test_client_blocked"
        max_requests = 3

        # Use up the allowed requests
        for _ in range(max_requests):
            assert limiter.allow_request(client_id, max_requests, 3600) is True

        # Next request should be blocked
        result = limiter.allow_request(client_id, max_requests, 3600)
        assert result is False

        # Client should still have only max_requests entries
        assert len(limiter.clients[client_id]) == max_requests

    def test_different_clients_separate_limits(self):
        """Test different clients have separate rate limits."""
        limiter = RateLimiter()
        client1 = "client_1"
        client2 = "client_2"
        max_requests = 5

        # Client 1 makes requests
        for _ in range(max_requests):
            assert limiter.allow_request(client1, max_requests, 3600) is True

        # Client 1 is blocked
        assert limiter.allow_request(client1, max_requests, 3600) is False

        # Client 2 should still be allowed
        assert limiter.allow_request(client2, max_requests, 3600) is True

        # Verify separate tracking
        assert len(limiter.clients[client1]) == max_requests
        assert len(limiter.clients[client2]) == 1


@pytest.mark.unit
@pytest.mark.auth
class TestRateLimiterSlidingWindow:
    """Test suite for sliding window algorithm."""

    def test_sliding_window_removes_old_requests(self):
        """Test sliding window removes requests outside time window."""
        limiter = RateLimiter()
        client_id = "test_client_window"
        max_requests = 3
        window_seconds = 10

        # Manually add old requests
        old_time = datetime.now(UTC) - timedelta(seconds=15)
        limiter.clients[client_id].append(old_time)
        limiter.clients[client_id].append(old_time)

        # These old requests should be removed, so new request should be allowed
        result = limiter.allow_request(client_id, max_requests, window_seconds)

        assert result is True
        assert len(limiter.clients[client_id]) == 1  # Only the new request

    def test_sliding_window_keeps_recent_requests(self):
        """Test sliding window keeps requests within time window."""
        limiter = RateLimiter()
        client_id = "test_client_recent"
        max_requests = 3
        window_seconds = 60

        # Add recent requests
        recent_time = datetime.now(UTC) - timedelta(seconds=30)
        limiter.clients[client_id].append(recent_time)
        limiter.clients[client_id].append(recent_time)

        # These requests are within window, so we can only add 1 more
        result = limiter.allow_request(client_id, max_requests, window_seconds)
        assert result is True

        # Now at limit
        result = limiter.allow_request(client_id, max_requests, window_seconds)
        assert result is False

    def test_sliding_window_mixed_old_and_recent(self):
        """Test sliding window with mix of old and recent requests."""
        limiter = RateLimiter()
        client_id = "test_client_mixed"
        max_requests = 3
        window_seconds = 30

        # Add old requests (outside window)
        old_time = datetime.now(UTC) - timedelta(seconds=60)
        limiter.clients[client_id].append(old_time)
        limiter.clients[client_id].append(old_time)

        # Add recent request (within window)
        recent_time = datetime.now(UTC) - timedelta(seconds=10)
        limiter.clients[client_id].append(recent_time)

        # Old requests removed, only 1 recent request, so 2 more allowed
        assert limiter.allow_request(client_id, max_requests, window_seconds) is True
        assert limiter.allow_request(client_id, max_requests, window_seconds) is True

        # Now at limit
        assert limiter.allow_request(client_id, max_requests, window_seconds) is False

    def test_sliding_window_with_zero_window(self):
        """Test rate limiter with very short window."""
        limiter = RateLimiter()
        client_id = "test_client_zero_window"
        max_requests = 5
        window_seconds = 1  # 1 second window

        # Add old request
        old_time = datetime.now(UTC) - timedelta(seconds=2)
        limiter.clients[client_id].append(old_time)

        # Should allow request because old one is outside 1-second window
        result = limiter.allow_request(client_id, max_requests, window_seconds)
        assert result is True


@pytest.mark.unit
@pytest.mark.auth
class TestRateLimiterCleanup:
    """Test suite for cleanup functionality."""

    def test_cleanup_removes_old_entries(self):
        """Test cleanup removes entries older than 24 hours."""
        limiter = RateLimiter()

        # Add clients with old requests
        old_time = datetime.now(UTC) - timedelta(hours=25)
        limiter.clients["old_client_1"].append(old_time)
        limiter.clients["old_client_2"].append(old_time)

        # Add client with recent requests
        recent_time = datetime.now(UTC) - timedelta(hours=1)
        limiter.clients["recent_client"].append(recent_time)

        # Run cleanup
        limiter._cleanup_old_entries()

        # Old clients should be removed
        assert "old_client_1" not in limiter.clients
        assert "old_client_2" not in limiter.clients

        # Recent client should remain
        assert "recent_client" in limiter.clients

    def test_cleanup_removes_empty_clients(self):
        """Test cleanup removes clients with no requests."""
        limiter = RateLimiter()

        # Add empty clients
        limiter.clients["empty_client_1"] = deque()
        limiter.clients["empty_client_2"] = deque()

        # Add client with requests
        limiter.clients["active_client"].append(datetime.now(UTC))

        # Run cleanup
        limiter._cleanup_old_entries()

        # Empty clients should be removed
        assert "empty_client_1" not in limiter.clients
        assert "empty_client_2" not in limiter.clients

        # Active client should remain
        assert "active_client" in limiter.clients

    def test_cleanup_partial_removal(self):
        """Test cleanup removes only old requests, keeps recent ones."""
        limiter = RateLimiter()
        client_id = "mixed_client"

        # Add old requests
        old_time = datetime.now(UTC) - timedelta(hours=30)
        limiter.clients[client_id].append(old_time)
        limiter.clients[client_id].append(old_time)

        # Add recent requests
        recent_time = datetime.now(UTC) - timedelta(hours=1)
        limiter.clients[client_id].append(recent_time)
        limiter.clients[client_id].append(recent_time)

        # Run cleanup
        limiter._cleanup_old_entries()

        # Client should remain with only recent requests
        assert client_id in limiter.clients
        assert len(limiter.clients[client_id]) == 2

    def test_automatic_cleanup_triggers(self):
        """Test automatic cleanup triggers after interval."""
        limiter = RateLimiter()
        limiter.cleanup_interval = 0  # Force immediate cleanup

        # Set last_cleanup to past
        limiter.last_cleanup = datetime.now(UTC) - timedelta(hours=2)

        # Add old client
        old_time = datetime.now(UTC) - timedelta(hours=25)
        limiter.clients["old_client"].append(old_time)

        # Make request (should trigger cleanup)
        limiter.allow_request("new_client", 10, 3600)

        # Old client should be cleaned up
        assert "old_client" not in limiter.clients

    def test_cleanup_does_not_affect_active_clients(self):
        """Test cleanup doesn't remove clients with recent requests."""
        limiter = RateLimiter()

        # Add multiple active clients
        now = datetime.now(UTC)
        for i in range(5):
            limiter.clients[f"client_{i}"].append(now - timedelta(hours=i))

        # Run cleanup
        limiter._cleanup_old_entries()

        # All clients should still exist (all within 24 hours)
        assert len(limiter.clients) == 5


@pytest.mark.unit
@pytest.mark.auth
class TestRateLimiterStatistics:
    """Test suite for client statistics functionality."""

    def test_get_client_stats_empty(self):
        """Test getting stats for client with no requests."""
        limiter = RateLimiter()
        client_id = "new_client"

        stats = limiter.get_client_stats(client_id)

        assert stats["requests_last_hour"] == 0
        assert stats["requests_last_day"] == 0
        assert stats["total_tracked_requests"] == 0

    def test_get_client_stats_with_requests(self):
        """Test getting stats for client with requests."""
        limiter = RateLimiter()
        client_id = "active_client"

        # Add requests at different times
        now = datetime.now(UTC)
        limiter.clients[client_id].append(now - timedelta(minutes=30))  # Within hour
        limiter.clients[client_id].append(now - timedelta(hours=2))  # Within day
        limiter.clients[client_id].append(now - timedelta(hours=12))  # Within day

        stats = limiter.get_client_stats(client_id)

        assert stats["requests_last_hour"] == 1
        assert stats["requests_last_day"] == 3
        assert stats["total_tracked_requests"] == 3

    def test_get_client_stats_old_requests(self):
        """Test stats exclude requests older than tracking period."""
        limiter = RateLimiter()
        client_id = "old_requests_client"

        # Add very old requests
        now = datetime.now(UTC)
        limiter.clients[client_id].append(now - timedelta(days=2))
        limiter.clients[client_id].append(now - timedelta(days=3))

        stats = limiter.get_client_stats(client_id)

        assert stats["requests_last_hour"] == 0
        assert stats["requests_last_day"] == 0
        assert stats["total_tracked_requests"] == 2  # Still tracked, just not counted

    def test_get_client_stats_nonexistent_client(self):
        """Test getting stats for client that doesn't exist."""
        limiter = RateLimiter()

        stats = limiter.get_client_stats("nonexistent_client")

        assert stats["requests_last_hour"] == 0
        assert stats["requests_last_day"] == 0
        assert stats["total_tracked_requests"] == 0

    def test_get_client_stats_boundary_cases(self):
        """Test stats at exact hour/day boundaries."""
        limiter = RateLimiter()
        client_id = "boundary_client"

        now = datetime.now(UTC)

        # Exactly 1 hour ago (should be included)
        limiter.clients[client_id].append(now - timedelta(hours=1, seconds=1))

        # Exactly 24 hours ago (should be excluded)
        limiter.clients[client_id].append(now - timedelta(days=1, seconds=1))

        stats = limiter.get_client_stats(client_id)

        # Boundary behavior depends on > vs >= comparison
        assert stats["requests_last_hour"] >= 0
        assert stats["requests_last_day"] >= 1


@pytest.mark.unit
@pytest.mark.auth
class TestRateLimiterEdgeCases:
    """Test suite for edge cases and error scenarios."""

    def test_rate_limiter_with_zero_max_requests(self):
        """Test rate limiter with max_requests=0."""
        limiter = RateLimiter()
        client_id = "zero_limit_client"

        result = limiter.allow_request(client_id, max_requests=0, window_seconds=3600)

        # Should be blocked immediately
        assert result is False

    def test_rate_limiter_with_negative_max_requests(self):
        """Test rate limiter with negative max_requests."""
        limiter = RateLimiter()
        client_id = "negative_limit_client"

        result = limiter.allow_request(client_id, max_requests=-1, window_seconds=3600)

        # Should be blocked
        assert result is False

    def test_rate_limiter_with_large_max_requests(self):
        """Test rate limiter with very large max_requests."""
        limiter = RateLimiter()
        client_id = "large_limit_client"
        max_requests = 1000000

        result = limiter.allow_request(client_id, max_requests, 3600)

        # Should be allowed
        assert result is True

    def test_rate_limiter_very_short_window(self):
        """Test rate limiter with very short time window."""
        limiter = RateLimiter()
        client_id = "short_window_client"

        result = limiter.allow_request(client_id, max_requests=1, window_seconds=1)

        assert result is True

    def test_rate_limiter_very_long_window(self):
        """Test rate limiter with very long time window."""
        limiter = RateLimiter()
        client_id = "long_window_client"

        result = limiter.allow_request(
            client_id, max_requests=10, window_seconds=86400 * 365
        )

        assert result is True

    def test_rate_limiter_same_client_different_limits(self):
        """Test same client with different rate limits."""
        limiter = RateLimiter()
        client_id = "same_client"

        # Allow with one limit
        assert (
            limiter.allow_request(client_id, max_requests=10, window_seconds=3600)
            is True
        )

        # Check with stricter limit
        assert (
            limiter.allow_request(client_id, max_requests=1, window_seconds=3600)
            is False
        )

    def test_rate_limiter_unicode_client_id(self):
        """Test rate limiter with unicode client ID."""
        limiter = RateLimiter()
        client_id = "クライアント_123"

        result = limiter.allow_request(client_id, max_requests=5, window_seconds=3600)

        assert result is True
        assert client_id in limiter.clients

    def test_rate_limiter_special_characters_client_id(self):
        """Test rate limiter with special characters in client ID."""
        limiter = RateLimiter()
        client_id = "client!@#$%^&*()_+-=[]{}|;:,.<>?"

        result = limiter.allow_request(client_id, max_requests=5, window_seconds=3600)

        assert result is True
        assert client_id in limiter.clients

    def test_rate_limiter_empty_client_id(self):
        """Test rate limiter with empty client ID."""
        limiter = RateLimiter()
        client_id = ""

        result = limiter.allow_request(client_id, max_requests=5, window_seconds=3600)

        assert result is True
        assert client_id in limiter.clients

    def test_rate_limiter_concurrent_same_client(self):
        """Test rate limiter behavior with rapid requests from same client."""
        limiter = RateLimiter()
        client_id = "concurrent_client"
        max_requests = 3

        # Simulate rapid requests
        results = []
        for _ in range(5):
            results.append(limiter.allow_request(client_id, max_requests, 3600))

        # First 3 should be True, rest False
        assert results[:3] == [True, True, True]
        assert results[3:] == [False, False]

    def test_cleanup_updates_last_cleanup_time(self):
        """Test that cleanup updates last_cleanup timestamp."""
        limiter = RateLimiter()

        # Set last_cleanup to past to ensure cleanup triggers
        old_cleanup_time = datetime.now(UTC) - timedelta(hours=2)
        limiter.last_cleanup = old_cleanup_time
        limiter.cleanup_interval = 0  # Force cleanup on next request

        # Make request (should trigger cleanup)
        limiter.allow_request("test", 10, 3600)

        # last_cleanup should be updated
        assert limiter.last_cleanup > old_cleanup_time


@pytest.mark.unit
@pytest.mark.auth
class TestRateLimiterMemoryManagement:
    """Test suite for memory management and performance."""

    def test_limiter_does_not_grow_indefinitely(self):
        """Test that old entries are cleaned up to prevent memory leaks."""
        limiter = RateLimiter()
        limiter.cleanup_interval = 0  # Force cleanup on every request

        # Simulate many requests over time
        for i in range(100):
            limiter.allow_request(f"client_{i}", max_requests=10, window_seconds=3600)

        # Force cleanup
        limiter._cleanup_old_entries()

        # Should have cleaned up clients with no recent activity
        # Exact number depends on timing, but should be less than 100
        assert len(limiter.clients) <= 100

    def test_deque_efficiency(self):
        """Test that deque is used for efficient popleft operations."""
        limiter = RateLimiter()
        client_id = "efficiency_test"

        # Add many old requests
        old_time = datetime.now(UTC) - timedelta(hours=2)
        for _ in range(1000):
            limiter.clients[client_id].append(old_time)

        # This should efficiently remove all old requests
        limiter.allow_request(client_id, max_requests=10, window_seconds=3600)

        # Should only have the new request
        assert len(limiter.clients[client_id]) == 1

    def test_multiple_clients_independence(self):
        """Test that multiple clients don't interfere with each other."""
        limiter = RateLimiter()
        num_clients = 100

        # Create many clients with requests
        for i in range(num_clients):
            limiter.allow_request(f"client_{i}", max_requests=5, window_seconds=3600)

        # Each client should have exactly 1 request
        assert len(limiter.clients) == num_clients
        for i in range(num_clients):
            assert len(limiter.clients[f"client_{i}"]) == 1
