"""Tests for RetryManager."""

import asyncio
from unittest.mock import AsyncMock, MagicMock

import httpx
import pytest

from ingress_edge.retry import RetryManager


class TestRetryManagerInit:
    """Tests for RetryManager initialization."""

    def test_init_default_max_retries(self):
        """Test default max_retries is 5."""
        manager = RetryManager()

        assert manager._max_retries == 5

    def test_init_custom_max_retries(self):
        """Test custom max_retries."""
        manager = RetryManager(max_retries=3)

        assert manager._max_retries == 3


class TestRetryManagerExecute:
    """Tests for execute() method."""

    @pytest.mark.asyncio
    async def test_execute_success_first_try(self):
        """Test successful execution on first try."""
        manager = RetryManager()
        fn = AsyncMock(return_value="success")

        result = await manager.execute(fn)

        assert result == "success"
        fn.assert_called_once()

    @pytest.mark.asyncio
    async def test_execute_retries_on_failure(self):
        """Test that execution retries on failure."""
        manager = RetryManager(max_retries=3)

        # Fail twice, then succeed
        fn = AsyncMock(side_effect=[Exception("fail"), Exception("fail"), "success"])

        result = await manager.execute(fn)

        assert result == "success"
        assert fn.call_count == 3

    @pytest.mark.asyncio
    async def test_execute_raises_after_max_retries(self):
        """Test that exception is raised after max retries."""
        manager = RetryManager(max_retries=2)

        # Always fail
        fn = AsyncMock(side_effect=Exception("persistent failure"))

        with pytest.raises(Exception, match="persistent failure"):
            await manager.execute(fn)

        # Initial attempt + 2 retries = 3 calls
        assert fn.call_count == 3

    @pytest.mark.asyncio
    async def test_execute_no_retry_on_4xx_error(self):
        """Test that 4xx errors are not retried."""
        manager = RetryManager(max_retries=3)

        # Create a 4xx HTTP error
        response = MagicMock()
        response.status_code = 400
        error = httpx.HTTPStatusError("Bad request", request=MagicMock(), response=response)

        fn = AsyncMock(side_effect=error)

        with pytest.raises(httpx.HTTPStatusError):
            await manager.execute(fn)

        # Should only be called once (no retries for 4xx)
        fn.assert_called_once()

    @pytest.mark.asyncio
    async def test_execute_retries_on_5xx_error(self):
        """Test that 5xx errors are retried."""
        manager = RetryManager(max_retries=2)

        # Create a 5xx HTTP error
        response = MagicMock()
        response.status_code = 500
        error = httpx.HTTPStatusError("Server error", request=MagicMock(), response=response)

        fn = AsyncMock(side_effect=[error, error, "success"])

        result = await manager.execute(fn)

        assert result == "success"
        assert fn.call_count == 3

    @pytest.mark.asyncio
    async def test_execute_all_4xx_not_retried(self):
        """Test that all 4xx status codes are not retried."""
        manager = RetryManager(max_retries=3)

        for status_code in [400, 401, 403, 404, 422, 429, 499]:
            response = MagicMock()
            response.status_code = status_code
            error = httpx.HTTPStatusError(
                f"Error {status_code}", request=MagicMock(), response=response
            )

            fn = AsyncMock(side_effect=error)

            with pytest.raises(httpx.HTTPStatusError):
                await manager.execute(fn)

            fn.assert_called_once()

    @pytest.mark.asyncio
    async def test_execute_waits_between_retries(self):
        """Test that execute waits between retries (exponential backoff)."""
        manager = RetryManager(max_retries=2)

        fn = AsyncMock(side_effect=[Exception("fail"), "success"])

        import time

        start = time.time()
        await manager.execute(fn)
        elapsed = time.time() - start

        # Should wait at least 1 second (first retry backoff)
        assert elapsed >= 0.9  # Allow some tolerance

    @pytest.mark.asyncio
    async def test_execute_exponential_backoff_timing(self):
        """Test exponential backoff timing."""
        # This is a basic timing test - may be flaky in slow CI environments
        manager = RetryManager(max_retries=1)

        fn = AsyncMock(side_effect=[Exception("fail"), "success"])

        import time

        start = time.time()
        await manager.execute(fn)
        elapsed = time.time() - start

        # First retry should wait ~1 second (2^0 = 1)
        assert 0.9 <= elapsed <= 1.5


class TestRetryManagerIsClientError:
    """Tests for _is_client_error() method."""

    def test_is_client_error_400(self):
        """Test that 400 is a client error."""
        response = MagicMock()
        response.status_code = 400
        error = httpx.HTTPStatusError("Bad request", request=MagicMock(), response=response)

        assert RetryManager._is_client_error(error) is True

    def test_is_client_error_404(self):
        """Test that 404 is a client error."""
        response = MagicMock()
        response.status_code = 404
        error = httpx.HTTPStatusError("Not found", request=MagicMock(), response=response)

        assert RetryManager._is_client_error(error) is True

    def test_is_client_error_499(self):
        """Test that 499 is a client error."""
        response = MagicMock()
        response.status_code = 499
        error = httpx.HTTPStatusError("Client error", request=MagicMock(), response=response)

        assert RetryManager._is_client_error(error) is True

    def test_is_client_error_500_not_client_error(self):
        """Test that 500 is not a client error."""
        response = MagicMock()
        response.status_code = 500
        error = httpx.HTTPStatusError("Server error", request=MagicMock(), response=response)

        assert RetryManager._is_client_error(error) is False

    def test_is_client_error_503_not_client_error(self):
        """Test that 503 is not a client error."""
        response = MagicMock()
        response.status_code = 503
        error = httpx.HTTPStatusError("Service unavailable", request=MagicMock(), response=response)

        assert RetryManager._is_client_error(error) is False

    def test_is_client_error_non_http_error(self):
        """Test that non-HTTP errors are not client errors."""
        error = ValueError("Not an HTTP error")

        assert RetryManager._is_client_error(error) is False

    def test_is_client_error_connection_error(self):
        """Test that connection errors are not client errors."""
        error = ConnectionError("Network error")

        assert RetryManager._is_client_error(error) is False


class TestRetryManagerCalculateBackoff:
    """Tests for calculate_backoff() method."""

    def test_calculate_backoff_attempt_0(self):
        """Test backoff for attempt 0."""
        backoff = RetryManager.calculate_backoff(0)

        assert backoff == 1.0  # 1.0 * 2^0 = 1.0

    def test_calculate_backoff_attempt_1(self):
        """Test backoff for attempt 1."""
        backoff = RetryManager.calculate_backoff(1)

        assert backoff == 2.0  # 1.0 * 2^1 = 2.0

    def test_calculate_backoff_attempt_2(self):
        """Test backoff for attempt 2."""
        backoff = RetryManager.calculate_backoff(2)

        assert backoff == 4.0  # 1.0 * 2^2 = 4.0

    def test_calculate_backoff_attempt_3(self):
        """Test backoff for attempt 3."""
        backoff = RetryManager.calculate_backoff(3)

        assert backoff == 8.0  # 1.0 * 2^3 = 8.0

    def test_calculate_backoff_attempt_4(self):
        """Test backoff for attempt 4."""
        backoff = RetryManager.calculate_backoff(4)

        assert backoff == 16.0  # 1.0 * 2^4 = 16.0

    def test_calculate_backoff_capped_at_30(self):
        """Test that backoff is capped at 30 seconds."""
        backoff = RetryManager.calculate_backoff(5)

        assert backoff == 30.0  # min(32.0, 30.0) = 30.0

    def test_calculate_backoff_very_high_attempt(self):
        """Test backoff for very high attempt number."""
        backoff = RetryManager.calculate_backoff(100)

        assert backoff == 30.0  # Always capped at 30

    def test_calculate_backoff_formula(self):
        """Test the backoff formula explicitly."""
        for attempt in range(10):
            expected = min(1.0 * (2**attempt), 30.0)
            actual = RetryManager.calculate_backoff(attempt)
            assert actual == expected, f"Failed for attempt {attempt}"
