"""
Extended tests for rate_limiter module - Comprehensive rate limiting coverage.

Tests cover:
- Sliding window and token bucket patterns
- Per-endpoint and per-user rate limiting
- Concurrent request handling
- Rate limit headers and retry-after
- Distributed rate limiting scenarios
- Edge cases and error conditions
"""

import time
import threading


class TestRateLimitSlidingWindow:
    """Tests for sliding window rate limiting behavior."""

    def test_rate_limit_sliding_window_basic(self):
        """Sliding window should track requests over time."""
        window = {"requests": [], "window_size": 60}

        def add_request():
            now = time.time()
            # Remove old requests
            window["requests"] = [
                t for t in window["requests"] if now - t < window["window_size"]
            ]
            window["requests"].append(now)
            return len(window["requests"])

        # Add requests
        count1 = add_request()
        count2 = add_request()

        assert count1 == 1
        assert count2 == 2

    def test_rate_limit_sliding_window_expiry(self):
        """Old requests should expire from sliding window."""
        window = {"requests": [], "window_size": 0.1}  # 100ms window

        def add_request():
            now = time.time()
            window["requests"] = [
                t for t in window["requests"] if now - t < window["window_size"]
            ]
            window["requests"].append(now)
            return len(window["requests"])

        add_request()
        time.sleep(0.15)  # allow: unmarked-sleep  # Wait for window to expire
        count = add_request()

        assert count == 1  # Old request should have expired


class TestRateLimitTokenBucket:
    """Tests for token bucket rate limiting pattern."""

    def test_token_bucket_basic(self):
        """Token bucket should replenish tokens over time."""
        bucket = {
            "tokens": 10,
            "max_tokens": 10,
            "refill_rate": 1.0,  # 1 token per second
            "last_refill": time.time(),
        }

        def consume_token():
            now = time.time()
            elapsed = now - bucket["last_refill"]
            bucket["tokens"] = min(
                bucket["max_tokens"],
                bucket["tokens"] + elapsed * bucket["refill_rate"],
            )
            bucket["last_refill"] = now

            if bucket["tokens"] >= 1:
                bucket["tokens"] -= 1
                return True
            return False

        # Consume all tokens
        for _ in range(10):
            assert consume_token() is True

        # Should be denied
        assert consume_token() is False

    def test_token_bucket_refill(self):
        """Token bucket should refill over time."""
        bucket = {
            "tokens": 0,
            "max_tokens": 10,
            "refill_rate": 100.0,  # Fast refill for testing
            "last_refill": time.time(),
        }

        time.sleep(0.05)  # Wait for some refill

        now = time.time()
        elapsed = now - bucket["last_refill"]
        bucket["tokens"] = min(
            bucket["max_tokens"],
            bucket["tokens"] + elapsed * bucket["refill_rate"],
        )

        assert bucket["tokens"] > 0


class TestPerEndpointRateLimiting:
    """Tests for per-endpoint rate limiting."""

    def test_per_endpoint_limits_isolation(self):
        """Different endpoints should have separate limits."""
        endpoint_limits = {
            "/api/upload": {"limit": 10, "used": 0},
            "/api/search": {"limit": 100, "used": 0},
            "/api/download": {"limit": 50, "used": 0},
        }

        def request(endpoint):
            if (
                endpoint_limits[endpoint]["used"]
                < endpoint_limits[endpoint]["limit"]
            ):
                endpoint_limits[endpoint]["used"] += 1
                return True
            return False

        # Exhaust upload limit
        for _ in range(10):
            assert request("/api/upload") is True
        assert request("/api/upload") is False

        # Search should still work
        assert request("/api/search") is True

    def test_per_endpoint_default_limits(self):
        """Unknown endpoints should use default limits."""
        endpoint_limits = {}
        default_limit = 60

        def get_limit(endpoint):
            return endpoint_limits.get(endpoint, {"limit": default_limit})[
                "limit"
            ]

        assert get_limit("/api/unknown") == default_limit


class TestPerUserRateLimiting:
    """Tests for per-user rate limiting."""

    def test_per_user_limits_isolation(self):
        """Different users should have separate limits."""
        user_limits = {}
        limit_per_user = 10

        def request(user_id):
            if user_id not in user_limits:
                user_limits[user_id] = {"used": 0, "limit": limit_per_user}

            if user_limits[user_id]["used"] < user_limits[user_id]["limit"]:
                user_limits[user_id]["used"] += 1
                return True
            return False

        # User1 exhausts their limit
        for _ in range(10):
            assert request("user1") is True
        assert request("user1") is False

        # User2 should still work
        assert request("user2") is True

    def test_per_user_anonymous_fallback(self):
        """Anonymous users should fall back to IP-based limiting."""

        def get_identifier(user_id, ip_address):
            if user_id:
                return f"user:{user_id}"
            return f"ip:{ip_address}"

        assert get_identifier("testuser", "192.168.1.1") == "user:testuser"
        assert get_identifier(None, "192.168.1.1") == "ip:192.168.1.1"


class TestGlobalRateLimiting:
    """Tests for global rate limiting."""

    def test_global_rate_limit(self):
        """Global limit should apply across all users."""
        global_state = {"total_requests": 0, "limit": 1000}

        def request():
            if global_state["total_requests"] < global_state["limit"]:
                global_state["total_requests"] += 1
                return True
            return False

        # Simulate many requests
        for _ in range(1000):
            assert request() is True
        assert request() is False


class TestBurstHandling:
    """Tests for burst request handling."""

    def test_burst_handling_with_burst_capacity(self):
        """System should handle bursts within capacity."""
        burst_capacity = 20
        rate_per_second = 10
        bucket = {
            "tokens": burst_capacity,
            "max_tokens": burst_capacity,
            "rate": rate_per_second,
        }

        def consume(n):
            if bucket["tokens"] >= n:
                bucket["tokens"] -= n
                return True
            return False

        # Burst of 20 requests should succeed
        assert consume(20) is True

        # Next request should fail (no tokens left)
        assert consume(1) is False

    def test_burst_recovery(self):
        """System should recover from burst after cooldown."""
        bucket = {
            "tokens": 0,
            "max_tokens": 10,
            "refill_per_ms": 10,  # Fast refill for testing
        }

        def refill(elapsed_ms):
            bucket["tokens"] = min(
                bucket["max_tokens"],
                bucket["tokens"] + elapsed_ms * bucket["refill_per_ms"],
            )

        # Simulate time passing
        refill(1)  # 1ms

        assert bucket["tokens"] >= 1


class TestRateLimitRecovery:
    """Tests for rate limit recovery scenarios."""

    def test_rate_limit_recovery_after_window(self):
        """Rate limit should reset after window expires."""
        window = {
            "requests": 10,
            "limit": 10,
            "window_start": time.time() - 100,  # Window already expired
            "window_size": 60,
        }

        def check_and_reset():
            now = time.time()
            if now - window["window_start"] > window["window_size"]:
                window["requests"] = 0
                window["window_start"] = now
            return window["requests"] < window["limit"]

        assert check_and_reset() is True
        assert window["requests"] == 0


class TestRateLimitHeaders:
    """Tests for rate limit headers in responses."""

    def test_rate_limit_headers_present(self):
        """Response should include rate limit headers."""
        headers = {
            "X-RateLimit-Limit": "100",
            "X-RateLimit-Remaining": "95",
            "X-RateLimit-Reset": "1640000000",
        }

        assert "X-RateLimit-Limit" in headers
        assert "X-RateLimit-Remaining" in headers
        assert "X-RateLimit-Reset" in headers

    def test_rate_limit_remaining_decrements(self):
        """Remaining count should decrement with each request."""
        state = {"limit": 100, "remaining": 100}

        def make_request():
            headers = {
                "X-RateLimit-Remaining": str(state["remaining"]),
            }
            state["remaining"] -= 1
            return headers

        h1 = make_request()
        h2 = make_request()

        assert int(h1["X-RateLimit-Remaining"]) > int(
            h2["X-RateLimit-Remaining"]
        )


class TestRetryAfterHeader:
    """Tests for Retry-After header behavior."""

    def test_retry_after_header_on_limit_exceeded(self):
        """Retry-After header should be present when limit exceeded."""
        headers = {}
        limit_exceeded = True

        if limit_exceeded:
            headers["Retry-After"] = "60"  # Seconds until reset

        assert "Retry-After" in headers

    def test_retry_after_calculation(self):
        """Retry-After should reflect actual wait time."""
        window_start = time.time()
        window_size = 60
        current_time = window_start + 30

        retry_after = max(0, window_start + window_size - current_time)

        assert retry_after == 30


class TestConcurrentRequests:
    """Tests for concurrent request handling."""

    def test_concurrent_requests_thread_safe(self):
        """Rate limiting should be thread-safe."""
        state = {"count": 0, "limit": 100}
        lock = threading.Lock()
        results = []

        def make_request():
            with lock:
                if state["count"] < state["limit"]:
                    state["count"] += 1
                    results.append(True)
                else:
                    results.append(False)

        threads = [threading.Thread(target=make_request) for _ in range(150)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert results.count(True) == 100
        assert results.count(False) == 50

    def test_concurrent_requests_no_race_condition(self):
        """Should not exceed limit due to race conditions."""
        state = {"count": 0, "limit": 100}
        lock = threading.Lock()

        def increment():
            with lock:
                if state["count"] < state["limit"]:
                    state["count"] += 1
                    return True
                return False

        threads = [threading.Thread(target=increment) for _ in range(200)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert state["count"] == 100


class TestDistributedRateLimiting:
    """Tests for distributed rate limiting scenarios."""

    def test_distributed_state_sync(self):
        """Distributed nodes should sync state."""
        nodes = {
            "node1": {"count": 5},
            "node2": {"count": 3},
        }

        def get_total_count():
            return sum(n["count"] for n in nodes.values())

        assert get_total_count() == 8

    def test_distributed_limit_check(self):
        """Should check global limit across nodes."""
        global_limit = 100
        nodes = {
            "node1": {"count": 50},
            "node2": {"count": 49},
        }

        def can_accept():
            total = sum(n["count"] for n in nodes.values())
            return total < global_limit

        assert can_accept() is True
        nodes["node2"]["count"] = 50
        assert can_accept() is False


class TestRateLimitWhitelist:
    """Tests for rate limit whitelist functionality."""

    def test_whitelist_bypasses_limit(self):
        """Whitelisted IPs should bypass rate limits."""
        whitelist = {"192.168.1.1", "10.0.0.1"}

        def is_rate_limited(ip, requests):
            if ip in whitelist:
                return False  # Never rate limited
            return requests > 100

        assert is_rate_limited("192.168.1.1", 1000) is False
        assert is_rate_limited("8.8.8.8", 101) is True


class TestRateLimitBlacklist:
    """Tests for rate limit blacklist functionality."""

    def test_blacklist_always_limited(self):
        """Blacklisted IPs should always be limited."""
        blacklist = {"malicious.ip.1", "malicious.ip.2"}

        def is_blocked(ip):
            return ip in blacklist

        assert is_blocked("malicious.ip.1") is True
        assert is_blocked("good.ip.1") is False


class TestDynamicRateLimitAdjustment:
    """Tests for dynamic rate limit adjustment."""

    def test_dynamic_adjustment_under_load(self):
        """Limits should adjust based on system load."""
        base_limit = 100

        def get_adjusted_limit(load):
            if load > 0.9:
                return base_limit * 0.5
            if load > 0.7:
                return base_limit * 0.75
            return base_limit

        assert get_adjusted_limit(0.8) == 75

    def test_dynamic_adjustment_low_load(self):
        """Limits should be higher when load is low."""
        base_limit = 100

        def get_adjusted_limit(load):
            if load < 0.3:
                return base_limit * 1.5  # Allow more when idle
            return base_limit

        assert get_adjusted_limit(0.2) == 150


class TestRateLimitLogging:
    """Tests for rate limit logging."""

    def test_rate_limit_exceeded_logged(self):
        """Rate limit exceeded events should be logged."""
        logged_events = []

        def log_rate_limit(user_id, endpoint, limit):
            logged_events.append(
                {
                    "event": "rate_limit_exceeded",
                    "user": user_id,
                    "endpoint": endpoint,
                    "limit": limit,
                }
            )

        log_rate_limit("user123", "/api/upload", 10)

        assert len(logged_events) == 1
        assert logged_events[0]["event"] == "rate_limit_exceeded"


class TestRateLimitMetrics:
    """Tests for rate limit metrics collection."""

    def test_metrics_collection(self):
        """Rate limit metrics should be collected."""
        metrics = {
            "total_requests": 0,
            "limited_requests": 0,
            "by_endpoint": {},
        }

        def record_request(endpoint, was_limited):
            metrics["total_requests"] += 1
            if was_limited:
                metrics["limited_requests"] += 1
            metrics["by_endpoint"][endpoint] = (
                metrics["by_endpoint"].get(endpoint, 0) + 1
            )

        record_request("/api/upload", True)
        record_request("/api/upload", False)
        record_request("/api/search", False)

        assert metrics["total_requests"] == 3
        assert metrics["limited_requests"] == 1
        assert metrics["by_endpoint"]["/api/upload"] == 2


class TestRateLimitTTLCleanup:
    """Tests for TTL-based cleanup of rate limit data."""

    def test_ttl_cleanup_expired_entries(self):
        """Expired entries should be cleaned up."""
        entries = {
            "user1": {"count": 5, "expires": time.time() - 100},  # Expired
            "user2": {"count": 3, "expires": time.time() + 100},  # Valid
        }

        def cleanup():
            now = time.time()
            expired = [k for k, v in entries.items() if v["expires"] < now]
            for k in expired:
                del entries[k]

        cleanup()

        assert "user1" not in entries
        assert "user2" in entries


class TestRateLimitPersistence:
    """Tests for rate limit state persistence."""

    def test_persistence_save_and_load(self):
        """Rate limit state should persist across restarts."""
        state = {"user1": {"count": 5, "window_start": time.time()}}

        # Simulate save
        saved = dict(state)

        # Simulate load
        loaded = dict(saved)

        assert loaded == state


class TestRateLimitOverflowProtection:
    """Tests for overflow protection in rate limiting."""

    def test_counter_overflow_protection(self):
        """Counters should not overflow."""
        import sys

        max_safe = sys.maxsize
        counter = max_safe - 5

        def safe_increment(value):
            if value >= max_safe - 1:
                return max_safe - 1  # Cap at max safe value
            return value + 1

        result = safe_increment(counter)
        assert result <= max_safe


class TestAPIvsWebRateLimiting:
    """Tests for different limits for API vs web requests."""

    def test_api_has_higher_limits(self):
        """API endpoints should have higher limits than web."""
        limits = {
            "api": {"limit": 1000, "window": 60},
            "web": {"limit": 100, "window": 60},
        }

        assert limits["api"]["limit"] > limits["web"]["limit"]

    def test_endpoint_type_detection(self):
        """Should detect endpoint type correctly."""

        def get_endpoint_type(path):
            if path.startswith("/api/"):
                return "api"
            return "web"

        assert get_endpoint_type("/api/v1/research") == "api"
        assert get_endpoint_type("/dashboard") == "web"
