import time

import pytest

from a3api._retry import (
    RetryConfig,
    calculate_delay,
    is_retryable_status,
    parse_retry_after,
    with_retry_async,
    with_retry_sync,
)


class TestIsRetryableStatus:
    @pytest.mark.parametrize("code", [429, 500, 502, 503, 504])
    def test_retryable_codes(self, code: int):
        assert is_retryable_status(code) is True

    @pytest.mark.parametrize("code", [200, 201, 400, 401, 403, 404])
    def test_non_retryable_codes(self, code: int):
        assert is_retryable_status(code) is False


class TestParseRetryAfter:
    def test_none_returns_none(self):
        assert parse_retry_after(None) is None

    def test_numeric_seconds(self):
        assert parse_retry_after("5") == 5.0

    def test_zero(self):
        assert parse_retry_after("0") == 0.0

    def test_unparseable_returns_none(self):
        assert parse_retry_after("abc") is None


class TestCalculateDelay:
    def test_returns_non_negative(self):
        for _ in range(100):
            delay = calculate_delay(0, 500)
            assert 0 <= delay <= 0.5  # in seconds

    def test_increases_exponentially(self):
        max_attempt_0 = 0.5  # 500ms / 1000
        max_attempt_2 = 2.0  # 500 * 4 / 1000
        for _ in range(100):
            assert calculate_delay(2, 500) <= max_attempt_2
        assert max_attempt_2 > max_attempt_0


class TestWithRetrySync:
    def test_succeeds_first_try(self):
        call_count = 0

        def fn():
            nonlocal call_count
            call_count += 1
            return "ok"

        result = with_retry_sync(fn, RetryConfig(max_retries=2), lambda _: (True, 0))
        assert result == "ok"
        assert call_count == 1

    def test_retries_then_succeeds(self):
        call_count = 0

        def fn():
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise RuntimeError("fail")
            return "ok"

        result = with_retry_sync(fn, RetryConfig(max_retries=2), lambda _: (True, 0))
        assert result == "ok"
        assert call_count == 2

    def test_no_retry_when_not_retryable(self):
        call_count = 0

        def fn():
            nonlocal call_count
            call_count += 1
            raise ValueError("not retryable")

        with pytest.raises(ValueError, match="not retryable"):
            with_retry_sync(fn, RetryConfig(max_retries=2), lambda _: (False, None))
        assert call_count == 1

    def test_exhausts_max_retries(self):
        call_count = 0

        def fn():
            nonlocal call_count
            call_count += 1
            raise RuntimeError("always fails")

        with pytest.raises(RuntimeError, match="always fails"):
            with_retry_sync(fn, RetryConfig(max_retries=2), lambda _: (True, 0))
        assert call_count == 3


class TestWithRetryAsync:
    async def test_succeeds_first_try(self):
        call_count = 0

        async def fn():
            nonlocal call_count
            call_count += 1
            return "ok"

        result = await with_retry_async(fn, RetryConfig(max_retries=2), lambda _: (True, 0))
        assert result == "ok"
        assert call_count == 1

    async def test_retries_then_succeeds(self):
        call_count = 0

        async def fn():
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise RuntimeError("fail")
            return "ok"

        result = await with_retry_async(fn, RetryConfig(max_retries=2), lambda _: (True, 0))
        assert result == "ok"
        assert call_count == 2
