"""Unit tests for ``mgf.fed._http`` — the PAPER-119 429 backoff budget."""

from __future__ import annotations

import io
import json
import urllib.error
from contextlib import contextmanager
from email.message import Message

import pytest

from mgf.fed import _http
from mgf.fed._http import _backoff_delay, request_json


def _http_error(code: int, retry_after: str | None = None) -> urllib.error.HTTPError:
    headers = Message()
    if retry_after is not None:
        headers["Retry-After"] = retry_after
    return urllib.error.HTTPError("https://x", code, "err", headers, io.BytesIO(b""))


class _FakeOpener:
    """Scripted urlopen: raise the queued errors, then return the payload."""

    def __init__(self, errors: list[urllib.error.HTTPError], body: dict) -> None:
        self.errors = list(errors)
        self.body = body
        self.calls = 0

    @contextmanager
    def __call__(self, req, timeout=0):
        self.calls += 1
        if self.errors:
            raise self.errors.pop(0)

        class _Resp:
            def read(inner) -> bytes:
                return json.dumps(self.body).encode()

        yield _Resp()


def test_success_passthrough(monkeypatch) -> None:
    opener = _FakeOpener([], {"id": 7})
    monkeypatch.setattr(_http.urllib.request, "urlopen", opener)
    assert request_json("https://x/api", token="t", sleep=lambda s: None) == {"id": 7}  # noqa: S106
    assert opener.calls == 1


def test_429_backs_off_then_succeeds(monkeypatch) -> None:
    opener = _FakeOpener([_http_error(429), _http_error(429)], {"ok": True})
    monkeypatch.setattr(_http.urllib.request, "urlopen", opener)
    waits: list[float] = []
    assert request_json("https://x/api", sleep=waits.append) == {"ok": True}
    assert opener.calls == 3
    # exponential: second wait is drawn from a doubled base (jitter ±25%)
    assert len(waits) == 2
    assert waits[0] < waits[1]
    assert all(w > 0 for w in waits)


def test_429_hard_stop_after_budget(monkeypatch) -> None:
    opener = _FakeOpener([_http_error(429)] * 10, {"never": True})
    monkeypatch.setattr(_http.urllib.request, "urlopen", opener)
    waits: list[float] = []
    with pytest.raises(urllib.error.HTTPError) as exc:
        request_json("https://x/api", max_429_retries=2, sleep=waits.append)
    assert exc.value.code == 429
    assert opener.calls == 3  # initial + 2 retries, then the hard stop
    assert len(waits) == 2  # never sleeps after the final attempt


def test_non_429_raises_immediately(monkeypatch) -> None:
    opener = _FakeOpener([_http_error(401)], {"never": True})
    monkeypatch.setattr(_http.urllib.request, "urlopen", opener)
    with pytest.raises(urllib.error.HTTPError) as exc:
        request_json("https://x/api", sleep=lambda s: None)
    assert exc.value.code == 401
    assert opener.calls == 1


def test_backoff_honors_numeric_retry_after() -> None:
    # Retry-After: 30 → jittered around 30s, never the exponential base
    delays = [_backoff_delay(0, "30") for _ in range(20)]
    assert all(30 * 0.75 <= d <= 30 * 1.25 for d in delays)


def test_backoff_caps_and_ignores_junk_retry_after() -> None:
    assert _backoff_delay(10, None) <= 120 * 1.25  # capped
    d = _backoff_delay(1, "soon")  # junk header → exponential path (base*2)
    assert 10 * 0.75 <= d <= 10 * 1.25
