"""Tests for retry logic with exponential backoff."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from certisigma import (
    AuthenticationError,
    CertiSigmaError,
    QuotaExceededError,
    RateLimitError,
)

from certisigma_census.retry import retry_api_call


@patch("certisigma_census.retry.time.sleep")
def test_retry_succeeds_first_try(mock_sleep):
    """No retry needed when the call succeeds immediately."""
    fn = MagicMock(return_value="ok")
    result = retry_api_call(fn, "arg1", key="val")

    assert result == "ok"
    fn.assert_called_once_with("arg1", key="val")
    mock_sleep.assert_not_called()


@patch("certisigma_census.retry.time.sleep")
def test_retry_on_rate_limit(mock_sleep):
    """Retries after RateLimitError using server-provided retry_after."""
    fn = MagicMock(
        side_effect=[
            RateLimitError("Too many requests", retry_after=5),
            "success",
        ]
    )

    result = retry_api_call(fn, max_retries=3)

    assert result == "success"
    assert fn.call_count == 2
    mock_sleep.assert_called_once_with(5)


@patch("certisigma_census.retry.random.uniform", return_value=0.5)
@patch("certisigma_census.retry.time.sleep")
def test_retry_on_server_error(mock_sleep, _mock_random):
    """Retries on 500/502/503 with exponential backoff + jitter."""
    fn = MagicMock(
        side_effect=[
            CertiSigmaError("Internal Server Error", status_code=500),
            CertiSigmaError("Bad Gateway", status_code=502),
            "recovered",
        ]
    )

    result = retry_api_call(fn, max_retries=3, base_delay=1.0)

    assert result == "recovered"
    assert fn.call_count == 3
    # attempt 0: 1.0 * 2^0 + 0.5 = 1.5
    # attempt 1: 1.0 * 2^1 + 0.5 = 2.5
    assert mock_sleep.call_count == 2
    assert abs(mock_sleep.call_args_list[0][0][0] - 1.5) < 0.01
    assert abs(mock_sleep.call_args_list[1][0][0] - 2.5) < 0.01


@patch("certisigma_census.retry.time.sleep")
def test_no_retry_on_auth_error(mock_sleep):
    """AuthenticationError is raised immediately without retry."""
    fn = MagicMock(side_effect=AuthenticationError("Invalid API key"))

    with pytest.raises(AuthenticationError, match="Invalid API key"):
        retry_api_call(fn, max_retries=3)

    fn.assert_called_once()
    mock_sleep.assert_not_called()


@patch("certisigma_census.retry.time.sleep")
def test_no_retry_on_quota_exceeded(mock_sleep):
    """QuotaExceededError is raised immediately without retry."""
    fn = MagicMock(side_effect=QuotaExceededError("Monthly quota exhausted"))

    with pytest.raises(QuotaExceededError, match="quota"):
        retry_api_call(fn, max_retries=3)

    fn.assert_called_once()
    mock_sleep.assert_not_called()


@patch("certisigma_census.retry.time.sleep")
def test_max_retries_exceeded(mock_sleep):
    """Raises after exhausting all retry attempts."""
    fn = MagicMock(
        side_effect=RateLimitError("Too many requests", retry_after=1)
    )

    with pytest.raises(RateLimitError):
        retry_api_call(fn, max_retries=2)

    assert fn.call_count == 3  # initial + 2 retries
    assert mock_sleep.call_count == 2


@patch("certisigma_census.retry.time.sleep")
def test_no_retry_on_client_error(mock_sleep):
    """4xx errors other than 429 are raised immediately."""
    fn = MagicMock(
        side_effect=CertiSigmaError("Bad request", status_code=400)
    )

    with pytest.raises(CertiSigmaError, match="Bad request"):
        retry_api_call(fn, max_retries=3)

    fn.assert_called_once()
    mock_sleep.assert_not_called()
