"""PC-05: ``RetryPolicy`` per-call lookup cost.

The retry loop consults the policy on the hot path:

  - ``is_method_retryable(method)`` — once per ``request()`` call.
  - ``is_status_retryable(status)`` — once per response when an
    attempt remains.
  - ``backoff_for_attempt(attempt)`` — once per retry sleep.

Each is sub-microsecond: ``RetryPolicy`` is a frozen dataclass with
slots; method / status checks are frozenset memberships; backoff is
arithmetic + ``random.random()``. This pin catches a regression
where the policy lookup somehow grows linearly (e.g., a refactor
that replaces the frozenset with a list, or adds a regex match per
call).

Strategy: 100 000 iterations of each lookup; assert per-call ≤ 1 µs.

Local baseline (Linux 6.17, Python 3.11):
  - ``is_method_retryable``:   ~0.15 µs/call
  - ``is_status_retryable``:   ~0.15 µs/call
  - ``backoff_for_attempt``:   ~0.4 µs/call (random.random + min)

Budget gives ~3-6x cushion over baseline. A breach signals the
policy shape changed away from O(1) lookups.

Marker: ``perf`` (registered in pyproject.toml). Excluded from the
default suite; run via ``pytest -m perf`` for the nightly sweep.
"""

from __future__ import annotations

import time

import pytest

from mgf.http import RetryPolicy

_PER_CALL_BUDGET_US = 1.0


@pytest.mark.perf
class TestRetryPolicyLookup:
    """Pin per-call cost of the retry-policy hot-path methods."""

    def test_is_method_retryable_under_budget(self) -> None:
        policy = RetryPolicy(max_attempts=3)

        # Warm-up.
        for _ in range(1000):
            policy.is_method_retryable("GET")

        n = 100_000
        start = time.perf_counter()
        for _ in range(n):
            policy.is_method_retryable("GET")
        elapsed = time.perf_counter() - start
        per_call_us = (elapsed / n) * 1_000_000

        assert per_call_us < _PER_CALL_BUDGET_US, (
            f"PC-05: RetryPolicy.is_method_retryable budget breach: "
            f"{per_call_us:.3f} us/call (budget "
            f"{_PER_CALL_BUDGET_US:.1f} us). Investigate "
            f"src/mgf/http/_retry.py — the retryable_methods set "
            f"must stay a frozenset (O(1) membership)."
        )

    def test_is_status_retryable_under_budget(self) -> None:
        policy = RetryPolicy(max_attempts=3)

        for _ in range(1000):
            policy.is_status_retryable(503)

        n = 100_000
        start = time.perf_counter()
        for _ in range(n):
            policy.is_status_retryable(503)
        elapsed = time.perf_counter() - start
        per_call_us = (elapsed / n) * 1_000_000

        assert per_call_us < _PER_CALL_BUDGET_US, (
            f"PC-05: RetryPolicy.is_status_retryable budget breach: "
            f"{per_call_us:.3f} us/call (budget "
            f"{_PER_CALL_BUDGET_US:.1f} us). Investigate "
            f"src/mgf/http/_retry.py — the retryable_statuses set "
            f"must stay a frozenset (O(1) membership)."
        )

    def test_backoff_for_attempt_under_budget(self) -> None:
        policy = RetryPolicy(
            max_attempts=5,
            base_backoff_seconds=0.5,
            max_backoff_seconds=30.0,
        )

        for _ in range(1000):
            policy.backoff_for_attempt(2)

        n = 100_000
        start = time.perf_counter()
        for _ in range(n):
            policy.backoff_for_attempt(2)
        elapsed = time.perf_counter() - start
        per_call_us = (elapsed / n) * 1_000_000

        assert per_call_us < _PER_CALL_BUDGET_US, (
            f"PC-05: RetryPolicy.backoff_for_attempt budget breach: "
            f"{per_call_us:.3f} us/call (budget "
            f"{_PER_CALL_BUDGET_US:.1f} us). Investigate "
            f"src/mgf/http/_retry.py — the math should be a single "
            f"min() + random.random()."
        )
