"""PC-01: ``AsyncHttpClient.request`` per-call wrapper overhead.

`AsyncHttpClient.request` adds work over `httpx.AsyncClient.request`:

  1. `method.upper()` — 1 op.
  2. Retry-policy lookup (cached on self).
  3. Per-call timeout select.
  4. `_client_span(method, url)` — OTel span open (no-op when
     OTel isn't installed).
  5. Inside `_send_with_retries`:
     - `is_method_retryable(method)` — set membership.
     - Cookie pack (if cookies non-empty).
     - `_client.request(...)` — THE COST. Stubbed here at the
       `AsyncClient.request` boundary via `AsyncMock`, so httpx
       protocol code is excluded from the measurement.
     - `_log_response(...)` — log emit + header redaction.
  6. Span close.

This isolates the apiprobe-style "wrapper tax over httpx" — the
cost mgf-http OWNS — by mocking at `AsyncClient.request`. Using
respx would let httpx's protocol code run; that would measure
"wrapper + httpx", which is not what THIS pin asserts.

Local baseline (Linux 6.17, Python 3.11, v0.6.0): ~24 µs/call.
Budget: 50 µs/call — ~2x cushion. A breach signals a regression
in the wrapper: extra log line, extra header pass, redundant
retry-policy lookup, etc.

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 asyncio
import datetime as dt
import time
from unittest.mock import AsyncMock

import httpx
import pytest

from mgf.http import AsyncHttpClient

# Per-call wrapper budget. EXCLUDES httpx's own protocol cost
# (mocked at `AsyncClient.request`) AND the consumer's network
# latency (no real I/O). Catches regressions in the mgf-http
# wrapper: log emission, header redaction, retry-policy lookup,
# OTel span open/close, Response handling.
_PER_CALL_WRAPPER_BUDGET_US = 50.0


def _make_httpx_mock(method: str, url: str, status: int = 200) -> AsyncMock:
    """Build an AsyncMock for `httpx.AsyncClient.request` returning
    a pre-built Response. The `.elapsed` field is force-set so
    consumer code (e.g., logging that reads `response.elapsed`)
    doesn't trip the "response not read" guard."""
    response = httpx.Response(
        status,
        request=httpx.Request(method, url),
        content=b"{}",
        headers={"content-type": "application/json"},
    )
    object.__setattr__(response, "_elapsed", dt.timedelta(milliseconds=1))
    return AsyncMock(return_value=response)


@pytest.mark.perf
class TestWrapperOverhead:
    """Pin the per-call wrapper tax owned by mgf-http."""

    def test_get_wrapper_under_budget(self) -> None:
        """Baseline: plain GET, no per-call headers/params/cookies."""
        url = "https://api.example.com/items"

        async def _drive() -> float:
            client = AsyncHttpClient()
            try:
                client._client.request = _make_httpx_mock("GET", url)

                for _ in range(100):
                    await client.request("GET", url)

                n = 5000
                start = time.perf_counter()
                for _ in range(n):
                    await client.request("GET", url)
                elapsed = time.perf_counter() - start
                return (elapsed / n) * 1_000_000
            finally:
                await client.aclose()

        per_call_us = asyncio.run(_drive())

        assert per_call_us < _PER_CALL_WRAPPER_BUDGET_US, (
            f"PC-01: AsyncHttpClient.request GET wrapper budget breach: "
            f"{per_call_us:.2f} µs/call (budget "
            f"{_PER_CALL_WRAPPER_BUDGET_US:.0f} µs). Investigate "
            f"src/mgf/http/_client.py::AsyncHttpClient.request + "
            f"_send_with_retries — likely a regression in log emission, "
            f"header redaction, or retry-policy lookup."
        )

    def test_post_with_headers_under_budget(self) -> None:
        """Realistic shape: POST with custom headers + json body."""
        url = "https://api.example.com/items"

        async def _drive() -> float:
            client = AsyncHttpClient()
            try:
                client._client.request = _make_httpx_mock("POST", url, status=201)

                headers = {"Accept": "application/json", "X-Tenant": "acme"}
                body = {"name": "item-0", "qty": 1}

                for _ in range(100):
                    await client.request("POST", url, headers=headers, json=body)

                n = 5000
                start = time.perf_counter()
                for _ in range(n):
                    await client.request("POST", url, headers=headers, json=body)
                elapsed = time.perf_counter() - start
                return (elapsed / n) * 1_000_000
            finally:
                await client.aclose()

        per_call_us = asyncio.run(_drive())

        assert per_call_us < _PER_CALL_WRAPPER_BUDGET_US, (
            f"PC-01: AsyncHttpClient.request POST wrapper budget breach: "
            f"{per_call_us:.2f} µs/call (budget "
            f"{_PER_CALL_WRAPPER_BUDGET_US:.0f} µs). Investigate "
            f"the request-shape assembly + log redaction path in "
            f"src/mgf/http/_client.py."
        )
