"""Tests for resilience components: circuit breaker, rate limiter, cache, retry."""
from __future__ import annotations

import asyncio
import pytest

from runblob._internal.circuit_breaker import CircuitBreaker, CircuitState
from runblob._internal.rate_limiter import TokenBucketRateLimiter
from runblob._internal.cache import AsyncTTLCache
from runblob._internal.retry import calculate_delay, should_retry_response

import httpx


# ---------------------------------------------------------------------------
# Circuit Breaker
# ---------------------------------------------------------------------------

class TestCircuitBreaker:
    async def test_starts_closed(self):
        cb = CircuitBreaker(threshold=3)
        assert cb.state == CircuitState.CLOSED
        assert cb.is_available

    async def test_trips_after_threshold(self):
        cb = CircuitBreaker(threshold=3, timeout=0.1)

        for _ in range(3):
            await cb.on_failure()

        assert cb.state == CircuitState.OPEN
        assert not cb.is_available
        assert cb.total_trips == 1

    async def test_half_open_after_timeout(self):
        cb = CircuitBreaker(threshold=2, timeout=0.05)

        await cb.on_failure()
        await cb.on_failure()
        assert cb.state == CircuitState.OPEN

        await asyncio.sleep(0.06)
        assert cb.state == CircuitState.HALF_OPEN

    async def test_closes_on_success_after_half_open(self):
        cb = CircuitBreaker(threshold=2, timeout=0.05)

        await cb.on_failure()
        await cb.on_failure()
        await asyncio.sleep(0.06)

        assert cb.state == CircuitState.HALF_OPEN
        await cb.on_success()
        assert cb.state == CircuitState.CLOSED

    async def test_reopens_on_failure_in_half_open(self):
        cb = CircuitBreaker(threshold=2, timeout=0.05)

        await cb.on_failure()
        await cb.on_failure()
        await asyncio.sleep(0.06)

        await cb.on_failure()
        assert cb.state == CircuitState.OPEN
        assert cb.total_trips == 2

    async def test_reset(self):
        cb = CircuitBreaker(threshold=1)
        await cb.on_failure()
        assert cb.state == CircuitState.OPEN

        await cb.reset()
        assert cb.state == CircuitState.CLOSED


# ---------------------------------------------------------------------------
# Rate Limiter
# ---------------------------------------------------------------------------

class TestRateLimiter:
    async def test_allows_burst(self):
        rl = TokenBucketRateLimiter(rps=100.0, burst=5.0)
        for _ in range(5):
            await rl.acquire()

    async def test_throttles_when_empty(self):
        rl = TokenBucketRateLimiter(rps=100.0, burst=1.0)
        await rl.acquire()  # Use the one token

        import time
        start = time.monotonic()
        await rl.acquire()  # Must wait for refill
        elapsed = time.monotonic() - start

        assert elapsed >= 0.005  # At least some wait


# ---------------------------------------------------------------------------
# Cache
# ---------------------------------------------------------------------------

class TestCache:
    async def test_set_and_get(self):
        cache = AsyncTTLCache(ttl=10.0)
        await cache.set("key1", "value1")
        assert await cache.get("key1") == "value1"

    async def test_missing_key_returns_none(self):
        cache = AsyncTTLCache()
        assert await cache.get("nonexistent") is None

    async def test_expired_entry_returns_none(self):
        cache = AsyncTTLCache(ttl=0.01)
        await cache.set("key1", "value1")
        await asyncio.sleep(0.02)
        assert await cache.get("key1") is None

    async def test_delete(self):
        cache = AsyncTTLCache()
        await cache.set("key1", "value1")
        assert await cache.delete("key1") is True
        assert await cache.get("key1") is None

    async def test_max_size_eviction(self):
        cache = AsyncTTLCache(max_size=2)
        await cache.set("a", 1)
        await cache.set("b", 2)
        await cache.set("c", 3)

        assert await cache.get("a") is None  # Evicted (oldest)
        assert await cache.get("b") == 2
        assert await cache.get("c") == 3

    async def test_cleanup_expired(self):
        cache = AsyncTTLCache(ttl=0.01)
        await cache.set("a", 1)
        await cache.set("b", 2)
        await asyncio.sleep(0.02)
        removed = await cache.cleanup_expired()
        assert removed == 2
        assert cache.size == 0

    async def test_clear(self):
        cache = AsyncTTLCache()
        await cache.set("a", 1)
        await cache.set("b", 2)
        await cache.clear()
        assert cache.size == 0


# ---------------------------------------------------------------------------
# Retry helpers
# ---------------------------------------------------------------------------

class TestRetryHelpers:
    def test_calculate_delay_default(self):
        d0 = calculate_delay(0)
        assert 0.5 <= d0 <= 1.0  # base=0.5, jitter up to 0.5

        d1 = calculate_delay(1)
        assert 1.0 <= d1 <= 1.5

    def test_calculate_delay_respects_retry_after(self):
        delay = calculate_delay(0, retry_after="5.0")
        assert delay == 5.0

    def test_calculate_delay_caps_at_max(self):
        delay = calculate_delay(10, max_delay=30.0)
        assert delay <= 30.0

    def test_should_retry_response(self):
        assert should_retry_response(httpx.Response(429)) is True
        assert should_retry_response(httpx.Response(500)) is True
        assert should_retry_response(httpx.Response(502)) is True
        assert should_retry_response(httpx.Response(200)) is False
        assert should_retry_response(httpx.Response(401)) is False
        assert should_retry_response(httpx.Response(404)) is False
