"""PC-03: per-check + total-chain iteration cost for ``DEFAULT_CHECKS``.

The check chain runs once per probe response (see
``suite.py::Suite._run_one_probe`` — the ``for check in self.checks``
loop). For a 100-probe suite x 18 default checks, that's 1800
``check.run`` invocations. Each must stay cheap, and the cumulative
chain cost must stay below a per-probe budget — otherwise a slow
check silently caps suite throughput.

Per-check budgets:
  - Simple presence/value checks (header missing, status unexpected):
    sub-µs to a few µs.
  - Body-based checks (BodyInvalidJson, SchemaShapeMismatch,
    BodyEchoesSecret): µs — O(body size) at worst.

Pins this test takes:
  - **Per-check** ≤ 50 µs against a 4 KB JSON body + the full
    default check set.
  - **Total chain** ≤ 250 µs for the 18 default checks combined.

A breach signals one of:
  - A specific check grew expensive (per-check assertion identifies
    which).
  - The chain accumulated a hidden per-check cost (total assertion
    catches death-by-thousand-paper-cuts).

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 json
import time
from types import MappingProxyType

import pytest

from mgf.apiprobe.checks import DEFAULT_CHECKS
from mgf.apiprobe.core.context import Context
from mgf.apiprobe.core.probe import Probe
from mgf.apiprobe.core.response import Response

# Per-check budget: 50 µs. Most default checks are sub-µs; the
# body-based ones (BodyInvalidJson, SchemaShapeMismatch) are the
# upper bound. The budget leaves slack for CI noise + slower hardware.
_PER_CHECK_BUDGET_US = 50.0

# Total-chain budget: 250 µs across all 18 default checks. Tighter
# than 18 x 50 µs (= 900 µs) because the chain shouldn't approach the
# worst-case per-check on every check — total budget catches creeping
# regressions that pass the per-check gate.
_TOTAL_CHAIN_BUDGET_US = 250.0


def _build_response() -> Response:
    """Build a 4 KB JSON-bodied 200 OK response with the standard
    contract + observability headers, so checks have realistic input."""
    payload = {
        "id": "12345678-1234-1234-1234-123456789012",
        "email": "user@example.com",
        "created_at": "2026-05-16T10:00:00Z",
        "items": [{"sku": f"item-{i:04d}", "qty": i, "price": i * 1.5} for i in range(60)],
    }
    body = json.dumps(payload).encode("utf-8")
    # Pad to ~4 KB if the synthetic payload undershoots.
    if len(body) < 4096:
        body += b"\n# padding " * ((4096 - len(body)) // 12 + 1)

    headers = MappingProxyType(
        {
            "content-type": "application/json",
            "x-request-id": "req-abcdef-12345",
            "x-ratelimit-limit": "1000",
            "x-ratelimit-remaining": "999",
            "strict-transport-security": "max-age=31536000",
            "x-content-type-options": "nosniff",
            "x-frame-options": "DENY",
            "server": "nginx/1.25",
        }
    )
    return Response(
        url="https://api.example.com/items",
        status=200,
        headers=headers,
        body=body,
        elapsed_ms=42.0,
        request_id="req-abcdef-12345",
    )


@pytest.mark.perf
class TestCheckChainCost:
    """Pin per-check + total-chain iteration cost for DEFAULT_CHECKS."""

    def test_per_check_under_budget(self) -> None:
        """Every check in DEFAULT_CHECKS stays under the per-call budget.

        Per-check error message names the offending check so a future
        regressor knows exactly which check.run grew expensive.
        """
        probe = (
            Probe.get("/items").id("perf-probe").base("https://api.example.com").expect_status(200)
        )
        response = _build_response()
        ctx = Context.empty()

        for check in DEFAULT_CHECKS:
            # Warm-up — pay first-call cache costs before measuring.
            for _ in range(50):
                list(check.run(probe, response, ctx))

            n = 1000
            start = time.perf_counter()
            for _ in range(n):
                list(check.run(probe, response, ctx))
            elapsed = time.perf_counter() - start
            per_call_us = (elapsed / n) * 1_000_000

            assert per_call_us < _PER_CHECK_BUDGET_US, (
                f"PC-03: {type(check).__name__}.run budget breach: "
                f"{per_call_us:.2f} µs/call (budget "
                f"{_PER_CHECK_BUDGET_US:.0f} µs). "
                f"Investigate {type(check).__module__}.{type(check).__name__}.run."
            )

    def test_total_chain_under_budget(self) -> None:
        """The full DEFAULT_CHECKS chain stays under the per-probe budget.

        Mirrors what Suite._run_one_probe does — one full pass of the
        check chain against one response. A per-probe regression here
        compounds across the suite.
        """
        probe = (
            Probe.get("/items").id("perf-probe").base("https://api.example.com").expect_status(200)
        )
        response = _build_response()
        ctx = Context.empty()

        # Warm-up.
        for _ in range(50):
            for check in DEFAULT_CHECKS:
                list(check.run(probe, response, ctx))

        n = 500
        start = time.perf_counter()
        for _ in range(n):
            for check in DEFAULT_CHECKS:
                list(check.run(probe, response, ctx))
        elapsed = time.perf_counter() - start
        per_chain_us = (elapsed / n) * 1_000_000

        assert per_chain_us < _TOTAL_CHAIN_BUDGET_US, (
            f"PC-03: DEFAULT_CHECKS chain budget breach: "
            f"{per_chain_us:.2f} µs/probe (budget "
            f"{_TOTAL_CHAIN_BUDGET_US:.0f} µs). Run the per-check "
            f"test (above) to identify which check regressed."
        )
