"""Tests for ``mgf.http.RetryPolicy``."""

from __future__ import annotations

import pytest

from mgf.http import RetryPolicy


class TestRetryPolicyDefaults:
    def test_zero_retries_by_default(self) -> None:
        # Deterministic by default — explicit consumer choice required.
        assert RetryPolicy().max_attempts == 0

    def test_default_retryable_methods_are_idempotent(self) -> None:
        policy = RetryPolicy()
        assert policy.is_method_retryable("GET")
        assert policy.is_method_retryable("HEAD")
        assert policy.is_method_retryable("PUT")
        assert policy.is_method_retryable("DELETE")
        assert policy.is_method_retryable("OPTIONS")
        # POST is intentionally NOT in the default set.
        assert not policy.is_method_retryable("POST")
        assert not policy.is_method_retryable("PATCH")

    def test_default_retryable_statuses(self) -> None:
        policy = RetryPolicy()
        assert policy.is_status_retryable(429)
        assert policy.is_status_retryable(503)
        assert policy.is_status_retryable(504)
        # Not retryable: 4xx (mostly) and 200/201/etc.
        assert not policy.is_status_retryable(400)
        assert not policy.is_status_retryable(401)
        assert not policy.is_status_retryable(404)
        assert not policy.is_status_retryable(500)
        assert not policy.is_status_retryable(200)


class TestRetryPolicyValidation:
    @pytest.mark.parametrize("bad", [-1, -100])
    def test_max_attempts_must_be_non_negative(self, bad: int) -> None:
        with pytest.raises(ValueError, match=">= 0"):
            RetryPolicy(max_attempts=bad)

    @pytest.mark.parametrize("bad", [0, -0.5])
    def test_base_backoff_must_be_positive(self, bad: float) -> None:
        with pytest.raises(ValueError, match="> 0"):
            RetryPolicy(base_backoff_seconds=bad)

    def test_max_backoff_must_be_at_least_base(self) -> None:
        with pytest.raises(ValueError, match=">= base_backoff"):
            RetryPolicy(base_backoff_seconds=10.0, max_backoff_seconds=5.0)


class TestBackoffComputation:
    def test_attempt_zero_bounded_by_base(self) -> None:
        # Backoff for attempt 0 is uniform random in [0, base * 2^0] = [0, base].
        policy = RetryPolicy(max_attempts=3, base_backoff_seconds=1.0)
        for _ in range(20):
            backoff = policy.backoff_for_attempt(0)
            assert 0 <= backoff <= 1.0

    def test_attempt_grows_exponentially(self) -> None:
        # Attempt N has ceiling base * 2^N, capped at max_backoff.
        policy = RetryPolicy(
            max_attempts=10,
            base_backoff_seconds=1.0,
            max_backoff_seconds=100.0,
        )
        # Sample attempt=3 many times; ceiling is 1.0 * 2^3 = 8.
        for _ in range(20):
            assert 0 <= policy.backoff_for_attempt(3) <= 8.0

    def test_capped_at_max_backoff(self) -> None:
        # Attempt 10 with base=1, max=5 should cap at 5 (not 1024).
        policy = RetryPolicy(
            max_attempts=20,
            base_backoff_seconds=1.0,
            max_backoff_seconds=5.0,
        )
        for _ in range(20):
            assert 0 <= policy.backoff_for_attempt(10) <= 5.0

    def test_negative_attempt_rejected(self) -> None:
        policy = RetryPolicy(max_attempts=3)
        with pytest.raises(ValueError, match=">= 0"):
            policy.backoff_for_attempt(-1)


class TestImmutability:
    def test_frozen(self) -> None:
        import dataclasses

        policy = RetryPolicy()
        with pytest.raises(dataclasses.FrozenInstanceError):
            policy.max_attempts = 5  # type: ignore[misc]
