"""
Deep behavioral tests for rate limiting patterns.
Tests token bucket, sliding window, throttling,
and burst handling logic.
"""

from datetime import datetime, timezone, timedelta
import time


# --- Token bucket patterns ---


class TestTokenBucket:
    """Tests for token bucket rate limiting."""

    def _create_bucket(self, capacity, refill_rate_per_second):
        """Create a token bucket."""
        return {
            "capacity": capacity,
            "tokens": capacity,
            "refill_rate": refill_rate_per_second,
            "last_refill": time.time(),
        }

    def _refill(self, bucket):
        """Refill tokens based on elapsed time."""
        now = time.time()
        elapsed = now - bucket["last_refill"]
        tokens_to_add = elapsed * bucket["refill_rate"]
        bucket["tokens"] = min(
            bucket["capacity"], bucket["tokens"] + tokens_to_add
        )
        bucket["last_refill"] = now
        return bucket

    def _consume(self, bucket, tokens=1):
        """Try to consume tokens from bucket."""
        self._refill(bucket)
        if bucket["tokens"] >= tokens:
            bucket["tokens"] -= tokens
            return True
        return False

    def test_bucket_starts_full(self):
        bucket = self._create_bucket(10, 1)
        assert bucket["tokens"] == 10

    def test_consume_reduces_tokens(self):
        bucket = self._create_bucket(10, 1)
        self._consume(bucket, 3)
        assert bucket["tokens"] == 7

    def test_consume_fails_when_empty(self):
        bucket = self._create_bucket(2, 1)
        self._consume(bucket, 2)
        result = self._consume(bucket, 1)
        assert result is False

    def test_consume_succeeds_with_tokens(self):
        bucket = self._create_bucket(10, 1)
        result = self._consume(bucket, 5)
        assert result is True

    def test_capacity_is_max(self):
        bucket = self._create_bucket(10, 1)
        bucket["tokens"] = 5
        bucket["last_refill"] = time.time() - 100  # Long ago
        self._refill(bucket)
        assert bucket["tokens"] == 10


# --- Sliding window patterns ---


class TestSlidingWindow:
    """Tests for sliding window rate limiting."""

    def _create_window(self, window_seconds, max_requests):
        """Create a sliding window."""
        return {
            "window_seconds": window_seconds,
            "max_requests": max_requests,
            "requests": [],
        }

    def _record_request(self, window):
        """Record a request in the window."""
        now = datetime.now(timezone.utc)
        cutoff = now - timedelta(seconds=window["window_seconds"])
        # Remove old requests
        window["requests"] = [r for r in window["requests"] if r >= cutoff]
        # Check if allowed
        if len(window["requests"]) >= window["max_requests"]:
            return False
        window["requests"].append(now)
        return True

    def _get_remaining(self, window):
        """Get remaining requests in window."""
        now = datetime.now(timezone.utc)
        cutoff = now - timedelta(seconds=window["window_seconds"])
        recent = [r for r in window["requests"] if r >= cutoff]
        return max(0, window["max_requests"] - len(recent))

    def test_allows_under_limit(self):
        window = self._create_window(60, 10)
        for _ in range(5):
            assert self._record_request(window) is True

    def test_blocks_over_limit(self):
        window = self._create_window(60, 3)
        for _ in range(3):
            self._record_request(window)
        assert self._record_request(window) is False

    def test_remaining_decreases(self):
        window = self._create_window(60, 10)
        assert self._get_remaining(window) == 10
        self._record_request(window)
        assert self._get_remaining(window) == 9


class TestSlidingWindowCounter:
    """Tests for sliding window counter pattern."""

    def _count_in_window(self, timestamps, window_seconds):
        """Count requests in sliding window."""
        now = datetime.now(timezone.utc)
        cutoff = now - timedelta(seconds=window_seconds)
        return sum(1 for ts in timestamps if ts >= cutoff)

    def _is_rate_limited(self, timestamps, window_seconds, max_requests):
        """Check if rate limited."""
        return self._count_in_window(timestamps, window_seconds) >= max_requests

    def test_counts_recent_only(self):
        now = datetime.now(timezone.utc)
        old = now - timedelta(seconds=120)
        timestamps = [old, now, now]
        count = self._count_in_window(timestamps, 60)
        assert count == 2

    def test_empty_not_limited(self):
        assert self._is_rate_limited([], 60, 10) is False

    def test_at_limit_is_limited(self):
        now = datetime.now(timezone.utc)
        timestamps = [now for _ in range(10)]
        assert self._is_rate_limited(timestamps, 60, 10) is True


# --- Fixed window patterns ---


class TestFixedWindow:
    """Tests for fixed window rate limiting."""

    def _get_window_key(self, window_seconds):
        """Get current window key."""
        now = int(time.time())
        return now // window_seconds

    def _check_and_increment(self, counters, window_key, max_requests):
        """Check limit and increment counter."""
        current = counters.get(window_key, 0)
        if current >= max_requests:
            return False, current
        counters[window_key] = current + 1
        return True, current + 1

    def test_new_window_starts_at_zero(self):
        counters = {}
        window_key = self._get_window_key(60)
        allowed, count = self._check_and_increment(counters, window_key, 10)
        assert allowed is True
        assert count == 1

    def test_increments_counter(self):
        counters = {"key": 5}
        allowed, count = self._check_and_increment(counters, "key", 10)
        assert count == 6

    def test_blocks_at_limit(self):
        counters = {"key": 10}
        allowed, count = self._check_and_increment(counters, "key", 10)
        assert allowed is False


# --- Throttling patterns ---


class TestThrottling:
    """Tests for request throttling patterns."""

    def _calculate_delay(self, request_count, base_delay=0.1, max_delay=5.0):
        """Calculate exponential backoff delay."""
        delay = base_delay * (2 ** min(request_count, 10))
        return min(delay, max_delay)

    def _should_throttle(self, current_rate, max_rate):
        """Check if should throttle."""
        return current_rate > max_rate

    def test_first_request_minimal_delay(self):
        delay = self._calculate_delay(0)
        assert delay == 0.1

    def test_delay_increases_exponentially(self):
        delay1 = self._calculate_delay(1)
        delay2 = self._calculate_delay(2)
        delay3 = self._calculate_delay(3)
        assert delay1 < delay2 < delay3

    def test_delay_capped_at_max(self):
        delay = self._calculate_delay(100)
        assert delay == 5.0

    def test_throttle_when_over_max(self):
        assert self._should_throttle(150, 100) is True

    def test_no_throttle_under_max(self):
        assert self._should_throttle(50, 100) is False


class TestAdaptiveThrottling:
    """Tests for adaptive throttling patterns."""

    def _calculate_adaptive_limit(
        self, success_rate, base_limit, min_limit=10, max_limit=1000
    ):
        """Calculate adaptive rate limit based on success rate."""
        if success_rate >= 0.99:
            return min(int(base_limit * 1.1), max_limit)
        if success_rate < 0.90:
            return max(int(base_limit * 0.8), min_limit)
        return base_limit

    def test_high_success_increases_limit(self):
        new_limit = self._calculate_adaptive_limit(0.99, 100)
        assert new_limit == 110

    def test_low_success_decreases_limit(self):
        new_limit = self._calculate_adaptive_limit(0.85, 100)
        assert new_limit == 80

    def test_normal_success_maintains_limit(self):
        new_limit = self._calculate_adaptive_limit(0.95, 100)
        assert new_limit == 100

    def test_respects_max_limit(self):
        new_limit = self._calculate_adaptive_limit(0.99, 950, max_limit=1000)
        assert new_limit == 1000

    def test_respects_min_limit(self):
        new_limit = self._calculate_adaptive_limit(0.50, 15, min_limit=10)
        assert new_limit == 12  # 15 * 0.8 = 12


# --- Burst handling patterns ---


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

    def _allows_burst(self, bucket, burst_size):
        """Check if burst is allowed."""
        return bucket.get("tokens", 0) >= burst_size

    def _calculate_burst_recovery_time(self, needed_tokens, refill_rate):
        """Calculate time to recover tokens for burst."""
        if refill_rate <= 0:
            return float("inf")
        return needed_tokens / refill_rate

    def test_burst_allowed_with_tokens(self):
        bucket = {"tokens": 10}
        assert self._allows_burst(bucket, 5) is True

    def test_burst_denied_without_tokens(self):
        bucket = {"tokens": 3}
        assert self._allows_burst(bucket, 5) is False

    def test_recovery_time_calculation(self):
        recovery = self._calculate_burst_recovery_time(10, 2)
        assert recovery == 5.0  # 10 tokens / 2 per second = 5 seconds


# --- Per-user rate limiting patterns ---


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

    def _get_user_bucket(self, user_buckets, user_id, default_capacity=100):
        """Get or create user's rate limit bucket."""
        if user_id not in user_buckets:
            user_buckets[user_id] = {
                "capacity": default_capacity,
                "tokens": default_capacity,
                "last_refill": time.time(),
            }
        return user_buckets[user_id]

    def _check_user_limit(self, user_buckets, user_id, tokens=1):
        """Check if user request is allowed."""
        bucket = self._get_user_bucket(user_buckets, user_id)
        if bucket["tokens"] >= tokens:
            bucket["tokens"] -= tokens
            return True
        return False

    def test_new_user_gets_full_bucket(self):
        user_buckets = {}
        bucket = self._get_user_bucket(user_buckets, "user1")
        assert bucket["tokens"] == 100

    def test_different_users_separate_buckets(self):
        user_buckets = {}
        self._check_user_limit(user_buckets, "user1", 50)
        bucket1 = self._get_user_bucket(user_buckets, "user1")
        bucket2 = self._get_user_bucket(user_buckets, "user2")
        assert bucket1["tokens"] == 50
        assert bucket2["tokens"] == 100

    def test_user_limited_independently(self):
        user_buckets = {}
        # Exhaust user1's tokens
        for _ in range(100):
            self._check_user_limit(user_buckets, "user1")
        # user1 should be limited
        assert self._check_user_limit(user_buckets, "user1") is False
        # user2 should still be allowed
        assert self._check_user_limit(user_buckets, "user2") is True


# --- Rate limit headers patterns ---


class TestRateLimitHeaders:
    """Tests for rate limit response header patterns."""

    def _build_rate_limit_headers(self, remaining, limit, reset_time):
        """Build rate limit headers for response."""
        return {
            "X-RateLimit-Limit": str(limit),
            "X-RateLimit-Remaining": str(max(0, remaining)),
            "X-RateLimit-Reset": str(int(reset_time)),
        }

    def _parse_retry_after(self, reset_time):
        """Calculate Retry-After value."""
        now = time.time()
        return max(0, int(reset_time - now))

    def test_headers_have_limit(self):
        headers = self._build_rate_limit_headers(50, 100, time.time() + 60)
        assert headers["X-RateLimit-Limit"] == "100"

    def test_headers_have_remaining(self):
        headers = self._build_rate_limit_headers(50, 100, time.time() + 60)
        assert headers["X-RateLimit-Remaining"] == "50"

    def test_remaining_never_negative(self):
        headers = self._build_rate_limit_headers(-5, 100, time.time() + 60)
        assert headers["X-RateLimit-Remaining"] == "0"

    def test_retry_after_calculation(self):
        reset_time = time.time() + 30
        retry_after = self._parse_retry_after(reset_time)
        assert 29 <= retry_after <= 31


# --- Endpoint-specific rate limiting patterns ---


class TestEndpointRateLimiting:
    """Tests for endpoint-specific rate limits."""

    def _get_endpoint_limit(self, endpoint, default_limit=100):
        """Get rate limit for specific endpoint."""
        endpoint_limits = {
            "/api/search": 30,
            "/api/research": 10,
            "/api/news": 60,
            "/api/cards": 100,
        }
        return endpoint_limits.get(endpoint, default_limit)

    def _is_sensitive_endpoint(self, endpoint):
        """Check if endpoint is rate-limit sensitive."""
        sensitive = ["/api/research", "/api/llm"]
        return any(s in endpoint for s in sensitive)

    def test_search_has_lower_limit(self):
        limit = self._get_endpoint_limit("/api/search")
        assert limit == 30

    def test_research_has_lowest_limit(self):
        limit = self._get_endpoint_limit("/api/research")
        assert limit == 10

    def test_unknown_endpoint_default(self):
        limit = self._get_endpoint_limit("/api/unknown")
        assert limit == 100

    def test_sensitive_endpoint_detected(self):
        assert self._is_sensitive_endpoint("/api/research") is True
        assert self._is_sensitive_endpoint("/api/llm/chat") is True

    def test_normal_endpoint_not_sensitive(self):
        assert self._is_sensitive_endpoint("/api/cards") is False


# --- Rate limit bypass patterns ---


class TestRateLimitBypass:
    """Tests for rate limit bypass conditions."""

    def _should_bypass_rate_limit(self, user, endpoint):
        """Check if rate limit should be bypassed."""
        # Admins bypass rate limits
        if user.get("is_admin"):
            return True
        # Whitelisted IPs bypass
        if user.get("ip") in ["127.0.0.1", "::1"]:
            return True
        # Health check endpoints bypass
        if endpoint in ["/health", "/api/health"]:
            return True
        return False

    def test_admin_bypasses(self):
        user = {"is_admin": True}
        assert self._should_bypass_rate_limit(user, "/api/search") is True

    def test_localhost_bypasses(self):
        user = {"ip": "127.0.0.1"}
        assert self._should_bypass_rate_limit(user, "/api/search") is True

    def test_health_endpoint_bypasses(self):
        user = {}
        assert self._should_bypass_rate_limit(user, "/health") is True

    def test_normal_user_no_bypass(self):
        user = {"ip": "192.168.1.1"}
        assert self._should_bypass_rate_limit(user, "/api/search") is False


# --- Concurrent request patterns ---


class TestConcurrentRequestLimiting:
    """Tests for concurrent request limiting."""

    def _can_acquire(self, concurrency_tracker, user_id, max_concurrent=5):
        """Try to acquire a concurrency slot."""
        current = concurrency_tracker.get(user_id, 0)
        if current >= max_concurrent:
            return False
        concurrency_tracker[user_id] = current + 1
        return True

    def _release(self, concurrency_tracker, user_id):
        """Release a concurrency slot."""
        current = concurrency_tracker.get(user_id, 0)
        if current > 0:
            concurrency_tracker[user_id] = current - 1
            return True
        return False

    def test_acquire_succeeds_under_limit(self):
        tracker = {}
        assert self._can_acquire(tracker, "user1") is True

    def test_acquire_fails_at_limit(self):
        tracker = {"user1": 5}
        assert self._can_acquire(tracker, "user1") is False

    def test_release_decrements(self):
        tracker = {"user1": 3}
        self._release(tracker, "user1")
        assert tracker["user1"] == 2

    def test_release_allows_new_acquire(self):
        tracker = {"user1": 5}
        assert self._can_acquire(tracker, "user1") is False
        self._release(tracker, "user1")
        assert self._can_acquire(tracker, "user1") is True


# --- Rate limit stats patterns ---


class TestRateLimitStats:
    """Tests for rate limit statistics patterns."""

    def _build_stats(self, user_buckets):
        """Build rate limit statistics."""
        total_users = len(user_buckets)
        limited_users = sum(
            1
            for bucket in user_buckets.values()
            if bucket.get("tokens", 0) == 0
        )
        avg_tokens = (
            sum(b.get("tokens", 0) for b in user_buckets.values()) / total_users
            if total_users > 0
            else 0
        )
        return {
            "total_users": total_users,
            "limited_users": limited_users,
            "average_remaining": round(avg_tokens, 2),
        }

    def test_counts_total_users(self):
        buckets = {"u1": {"tokens": 50}, "u2": {"tokens": 30}}
        stats = self._build_stats(buckets)
        assert stats["total_users"] == 2

    def test_counts_limited_users(self):
        buckets = {
            "u1": {"tokens": 0},
            "u2": {"tokens": 30},
            "u3": {"tokens": 0},
        }
        stats = self._build_stats(buckets)
        assert stats["limited_users"] == 2

    def test_calculates_average(self):
        buckets = {"u1": {"tokens": 50}, "u2": {"tokens": 30}}
        stats = self._build_stats(buckets)
        assert stats["average_remaining"] == 40.0

    def test_empty_buckets(self):
        stats = self._build_stats({})
        assert stats["total_users"] == 0
        assert stats["average_remaining"] == 0
