"""PC-02: per-probe wrapper tax over httpx.

`Suite._run_one_probe` + `HttpxTransport.send` add Python work on
top of `httpx.AsyncClient.request`:

  1. Probe URL resolution (`probe.absolute_url()`).
  2. `probe.auth.prepare()` if auth is set (env-var resolution +
     creds build).
  3. Header / cookie / query-param assembly + auth-fragment merge.
  4. Timeout select (probe-override vs settings default).
  5. The httpx send (THE COST — uncontrolled, but stubbed here at
     the `AsyncClient.request` boundary so httpx's own protocol
     code does not factor in — that's what "wrapper tax over httpx"
     means: the cost OWNED by apiprobe).
  6. `_to_apiprobe_response()` — Response dataclass alloc.
  7. Check chain iteration — driven with an empty `checks=()` so
     this stays as the wrapper-only measurement (PC-03 already pins
     the chain cost separately).

The mock replaces `transport._client.request` with an `AsyncMock`
returning a pre-built `httpx.Response`. This isolates the apiprobe
wrapper from httpx's internal protocol code (URL parsing, header
normalisation, request lifecycle). Using respx as the tracker
originally suggested would still let httpx run all its protocol
code (just stub the underlying transport) — that measures wrapper
+ httpx, not "wrapper over httpx".

Local baseline (Linux 6.17, Python 3.11, v0.3.0): ~8-15 µs/probe.
Budget: 100 µs/probe — ~10x cushion for CI noise + slower hardware
+ Python-version drift.

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.apiprobe.core.context import Context
from mgf.apiprobe.core.probe import Probe
from mgf.apiprobe.core.suite import Suite
from mgf.apiprobe.transport.httpx_transport import HttpxTransport

# Per-probe wrapper budget. EXCLUDES httpx's own protocol cost
# (we mock at the `AsyncClient.request` boundary) AND the check
# chain (PC-03 pins it separately). Catches regressions in the
# apiprobe-owned per-probe path: URL resolution, header / query
# assembly, auth-fragment merge, timeout select, Response alloc.
_PER_PROBE_WRAPPER_BUDGET_US = 100.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 the
    `_to_apiprobe_response()` converter doesn't trip the "response
    not read" guard."""
    response = httpx.Response(
        status,
        request=httpx.Request(method, url),
        content=b"{}",
        headers={"content-type": "application/json"},
    )
    # httpx normally sets `.elapsed` after read/close; we're skipping
    # that machinery so seed it manually with a non-zero delta.
    object.__setattr__(response, "_elapsed", dt.timedelta(milliseconds=1))
    return AsyncMock(return_value=response)


@pytest.mark.perf
class TestProbeWrapperOverhead:
    """Pin the per-probe wrapper tax owned by apiprobe."""

    def test_no_auth_wrapper_under_budget(self) -> None:
        """Baseline: probe with no auth flow, no per-probe overrides."""
        probe = Probe.get("/items").id("perf-probe").base("https://api.example.com")
        suite = Suite(probes=(probe,), checks=())
        ctx = Context.empty()

        async def _drive() -> float:
            async with HttpxTransport() as transport:
                transport._client.request = _make_httpx_mock("GET", "https://api.example.com/items")

                # Warm-up — pay first-call import / cache costs
                # before the measurement window opens.
                for _ in range(100):
                    await suite._run_one_probe(probe, ctx, transport)

                n = 1000
                start = time.perf_counter()
                for _ in range(n):
                    await suite._run_one_probe(probe, ctx, transport)
                elapsed = time.perf_counter() - start
                return (elapsed / n) * 1_000_000

        per_probe_us = asyncio.run(_drive())

        assert per_probe_us < _PER_PROBE_WRAPPER_BUDGET_US, (
            f"PC-02: no-auth probe wrapper budget breach: "
            f"{per_probe_us:.2f} µs/probe (budget "
            f"{_PER_PROBE_WRAPPER_BUDGET_US:.0f} µs). "
            f"Investigate Suite._run_one_probe + HttpxTransport.send "
            f"in src/mgf/apiprobe/core/suite.py and "
            f"src/mgf/apiprobe/transport/httpx_transport.py."
        )

    def test_with_headers_and_query_under_budget(self) -> None:
        """Realistic shape: a few headers + query params + json body."""
        probe = (
            Probe.post("/items")
            .id("perf-probe")
            .base("https://api.example.com")
            .header("Accept", "application/json")
            .header("X-Tenant", "acme")
            .query("page", 1)
            .query("limit", 50)
            .body({"name": "item-0", "qty": 1})
        )
        suite = Suite(probes=(probe,), checks=())
        ctx = Context.empty()

        async def _drive() -> float:
            async with HttpxTransport() as transport:
                transport._client.request = _make_httpx_mock(
                    "POST", "https://api.example.com/items", status=201
                )

                for _ in range(100):
                    await suite._run_one_probe(probe, ctx, transport)

                n = 1000
                start = time.perf_counter()
                for _ in range(n):
                    await suite._run_one_probe(probe, ctx, transport)
                elapsed = time.perf_counter() - start
                return (elapsed / n) * 1_000_000

        per_probe_us = asyncio.run(_drive())

        assert per_probe_us < _PER_PROBE_WRAPPER_BUDGET_US, (
            f"PC-02: with-headers-and-query probe wrapper budget breach: "
            f"{per_probe_us:.2f} µs/probe (budget "
            f"{_PER_PROBE_WRAPPER_BUDGET_US:.0f} µs). "
            f"Investigate request-shape assembly in "
            f"src/mgf/apiprobe/transport/httpx_transport.py::HttpxTransport.send."
        )
