"""PC-02: ``AsyncHttpClient`` capacity profile under sustained load.

The wrapper itself should add NO accumulating cost as a consumer
makes many requests in series. Memory should stay stable, latency
should not drift, and request handling should not leak resources.

**Scope note.** The tracker's original framing mentioned the
httpx connection pool. That pool is owned by httpx (PC-INV-6 —
"we don't reinvent the pool semantics") and pool-specific
behavior (max_connections, keepalive expiry, idle-timeout) is
out of scope for mgf-http's tracker. What we DO own — and what
this test gates — is the **wrapper's resource discipline**:

  - Memory steady-state under sustained load (no per-request leak
    in `_log_response`, `_redact_headers`, span open/close, retry
    bookkeeping).
  - Latency stable across a sustained window (no drift in the
    Python-side wrapper code as request count grows).

httpx's request layer is stubbed at the `AsyncClient.request`
boundary via `AsyncMock` so this test isolates the wrapper. The
production pool's behavior under real network load is exercised
end-to-end by consumer-side integration tests (e.g., mgf-apiprobe's
suite throughput pin in its own tracker, PC-01).

Strategy: drive 5 000 requests through the wrapper; track RSS
delta + p95 latency over the window; assert both stay within
expected bounds.

Local baseline (Linux 6.17, Python 3.11, v0.6.0):
  - RSS delta over 5 000 requests:    <1 MB
  - p95 wrapper latency:                ~30 µs
  - p50 wrapper latency:                ~24 µs

Budgets give comfortable cushion against the baseline.

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

from __future__ import annotations

import asyncio
import datetime as dt
import gc
import resource
import time
from unittest.mock import AsyncMock

import httpx
import pytest

from mgf.http import AsyncHttpClient

# Wrapper RSS-delta budget over a 5000-request sustained window.
# A leak in `_log_response`, header redaction, or retry-state
# tracking would surface as growing RSS. 10 MB is generous —
# local baseline measures <1 MB delta.
_RSS_DELTA_BUDGET_MB = 10.0

# Wrapper latency-stability budget. p95 over the second half of
# the window should not exceed 3x the p50 of the first quarter —
# catches latency drift (a GC-pressure pattern, a list that
# grows monotonically).
_LATENCY_DRIFT_RATIO_BUDGET = 3.0

# Total request count for the sustained window.
_N_REQUESTS = 5000


def _rss_mb() -> float:
    """Return current process RSS in MB.

    `ru_maxrss` is in KB on Linux (man getrusage). Force a GC
    first so transient allocations don't pollute the reading.
    """
    gc.collect()
    return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0


def _make_httpx_mock(method: str, url: str) -> AsyncMock:
    response = httpx.Response(
        200,
        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.slow
class TestPoolCapacity:
    """Pin the wrapper's resource discipline under sustained load."""

    def test_sustained_load_no_leak_no_drift(self) -> None:
        url = "https://api.example.com/items"

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

                # Warm-up — pay first-call import / cache costs.
                for _ in range(200):
                    await client.request("GET", url)

                rss_before = _rss_mb()
                latencies_us: list[float] = []
                for _ in range(_N_REQUESTS):
                    t0 = time.perf_counter()
                    await client.request("GET", url)
                    latencies_us.append((time.perf_counter() - t0) * 1_000_000)
                rss_after = _rss_mb()

                return rss_before, rss_after, latencies_us
            finally:
                await client.aclose()

        rss_before, rss_after, latencies = asyncio.run(_drive())
        rss_delta = rss_after - rss_before

        # 1) RSS steady-state.
        assert rss_delta < _RSS_DELTA_BUDGET_MB, (
            f"PC-02: RSS growth over {_N_REQUESTS} requests: "
            f"{rss_delta:.1f} MB (budget {_RSS_DELTA_BUDGET_MB:.0f} MB). "
            f"Wrapper is leaking — investigate per-request allocations "
            f"in src/mgf/http/_client.py (log emission, header redaction, "
            f"retry bookkeeping)."
        )

        # 2) Latency stability — compare p50 of first quarter vs.
        # p95 of second half. If the wrapper has a leak that pushes
        # GC pressure up, the second-half p95 will balloon.
        first_quarter = sorted(latencies[: _N_REQUESTS // 4])
        second_half = sorted(latencies[_N_REQUESTS // 2 :])

        p50_first = first_quarter[len(first_quarter) // 2]
        p95_second = second_half[int(len(second_half) * 0.95)]

        drift_ratio = p95_second / p50_first if p50_first > 0 else 0.0

        assert drift_ratio < _LATENCY_DRIFT_RATIO_BUDGET, (
            f"PC-02: latency drift over sustained load: "
            f"p95_second_half ({p95_second:.1f} µs) is "
            f"{drift_ratio:.2f}x the p50_first_quarter "
            f"({p50_first:.1f} µs), exceeding budget "
            f"{_LATENCY_DRIFT_RATIO_BUDGET:.1f}x. Wrapper is "
            f"accumulating work — investigate src/mgf/http/_client.py."
        )
