"""Per-surface perf regression pins (RA-11 / PC-*), baseline-relative (PF-01).

Each pin measures a hot-path metric and compares it to the recorded baseline in
``tests/perf/baselines.json`` via the ``baselines`` fixture
(``assert_no_regression`` — PF-01/07), NOT an absolute wall-clock budget (the
PAPER-50 anti-pattern). Scaling/ratio pins (redactor dict/string scaling,
async-vs-sync) are already *relative invariants* and need no baseline.

Run: ``pytest -m perf`` (excluded from the default gate). Re-baseline on the
reference tier (``hw_tiers.md``): ``mgf-common bench rebaseline`` or
``MGF_PERF_REBASELINE=1 pytest -m perf``.
"""

from __future__ import annotations

import logging
import tempfile
import time
from typing import TYPE_CHECKING

import pytest

import mgf.common
from mgf.common import _identity, current_context
from mgf.common.observability.json_formatter import JsonFormatter
from mgf.common.observability.redaction import Redactor

if TYPE_CHECKING:
    from pathlib import Path

    from tests.perf.conftest import Baselines, _Bench

pytestmark = pytest.mark.perf

# Scaling / ratio invariants — relative by construction (PF-01 compliant
# without a baseline: they compare two measurements of the SAME run).
_REDACTOR_DICT_SCALING_RATIO_BUDGET = 12.0  # 10x linear + 20% slack
_REDACTOR_STRING_SCALING_RATIO_BUDGET = 15.0
_ASYNC_VS_SYNC_RATIO_MAX = 0.5


class TestCurrentContextPerf:
    def test_current_context_no_regression(self, bench: _Bench, baselines: Baselines) -> None:
        _identity._set_identity("perf-bench", "0.0.0")
        per_call_ns = bench.measure(current_context, iterations=100_000) * 1e9
        baselines.assert_no_regression(
            "current_context_p50_ns",
            per_call_ns,
            units="ns",
            description="current_context() median, 100k iterations",
        )


class TestRedactorPerf:
    def test_redact_string_no_regression(self, bench: _Bench, baselines: Baselines) -> None:
        r = Redactor.default()
        sample = (
            "Authorization: Bearer abcdef1234567890abcdef1234567890 "
            "and some normal text after the secret"
        )
        per_call_us = bench.measure(lambda: r.redact_string(sample), iterations=50_000) * 1e6
        baselines.assert_no_regression(
            "redact_string_p50_us",
            per_call_us,
            units="us",
            description="Redactor.redact_string median over a 90-byte line, 50k iterations",
        )

    def test_redactor_scales_linearly_in_dict_size(self, bench: _Bench) -> None:
        def _measure(n_keys: int) -> float:
            r = Redactor.default()
            payload = {
                f"key_{i}": f"Bearer abcdef1234567890abcdef1234567890_{i}" for i in range(n_keys)
            }
            return bench.measure(lambda: r.redact(dict(payload)), iterations=50, warmup=10)

        ratio = _measure(1000) / _measure(100)
        assert ratio < _REDACTOR_DICT_SCALING_RATIO_BUDGET, (
            f"Redactor dict-scaling regressed: 1000-key cost is {ratio:.1f}x the "
            f"100-key cost (invariant: < {_REDACTOR_DICT_SCALING_RATIO_BUDGET:.0f}x; "
            f"~10x is linear; >50x suggests O(n²) in "
            f"redaction.py::Redactor._walk)."
        )

    def test_redactor_scales_linearly_in_string_length(self, bench: _Bench) -> None:
        r = Redactor.default()

        def _measure(n_bytes: int) -> float:
            sample = (
                "Bearer abcdef1234567890abcdef1234567890 "
                + ("x" * (n_bytes - 50))
                + " trailing text"
            )
            return bench.measure(lambda: r.redact_string(sample), iterations=500, warmup=10)

        ratio = _measure(100_000) / _measure(10_000)
        assert ratio < _REDACTOR_STRING_SCALING_RATIO_BUDGET, (
            f"Redactor string-scaling regressed: 100KB cost is {ratio:.1f}x the "
            f"10KB cost (invariant: < {_REDACTOR_STRING_SCALING_RATIO_BUDGET:.0f}x). "
            f"A backtracking BUILTIN regex is the canonical cause."
        )

    def test_redactor_large_dict_no_regression(self, bench: _Bench, baselines: Baselines) -> None:
        r = Redactor.default()
        payload = {f"key_{i}": f"Bearer abcdef1234567890abcdef1234567890_{i}" for i in range(1000)}
        per_call_ms = bench.measure(lambda: r.redact(dict(payload)), iterations=50, warmup=10) * 1e3
        baselines.assert_no_regression(
            "redactor_1000key_dict_p50_ms",
            per_call_ms,
            units="ms",
            description="Redactor.redact on a 1000-key dict, median",
        )


class TestJsonFormatterPerf:
    def test_json_formatter_no_regression(self, bench: _Bench, baselines: Baselines) -> None:
        formatter = JsonFormatter(redactor=Redactor.default())
        rec = logging.LogRecord(
            name="perf.bench",
            level=logging.INFO,
            pathname="/perf.py",
            lineno=1,
            msg="hello %s",
            args=("world",),
            exc_info=None,
        )
        per_call_us = bench.measure(lambda: formatter.format(rec), iterations=50_000) * 1e6
        baselines.assert_no_regression(
            "json_formatter_p50_us",
            per_call_us,
            units="us",
            description="JsonFormatter.format median per record, 50k iterations",
        )


class TestAsyncHandlerEmitPerf:
    _ITERATIONS = 5000

    def test_async_emit_no_regression_and_faster_than_sync(self, baselines: Baselines) -> None:
        from mgf.common.observability.async_handler import AsyncJsonHandler

        record = logging.LogRecord(
            name="pc10.bench",
            level=logging.INFO,
            pathname=__file__,
            lineno=1,
            msg="pc-10 perf pin",
            args=(),
            exc_info=None,
        )

        class _Sink(logging.Handler):
            def emit(self, record: logging.LogRecord) -> None:
                self.format(record)

        sync_sink = _Sink()
        sync_sink.setFormatter(JsonFormatter(redactor=Redactor.default()))
        t0 = time.perf_counter()
        for _ in range(self._ITERATIONS):
            sync_sink.handle(record)
        sync_us = (time.perf_counter() - t0) / self._ITERATIONS * 1e6

        async_target = _Sink()
        async_target.setFormatter(JsonFormatter(redactor=Redactor.default()))
        handler = AsyncJsonHandler(target=async_target, max_queue_size=self._ITERATIONS * 2)
        try:
            t0 = time.perf_counter()
            for _ in range(self._ITERATIONS):
                handler.emit(record)
            async_us = (time.perf_counter() - t0) / self._ITERATIONS * 1e6
        finally:
            handler.close()

        baselines.assert_no_regression(
            "async_emit_p50_us",
            async_us,
            units="us",
            description="AsyncJsonHandler.emit (queue-put on calling thread), per emit",
        )
        ratio = async_us / sync_us
        assert ratio < _ASYNC_VS_SYNC_RATIO_MAX, (
            f"async/sync ratio is {ratio:.2f} (sync {sync_us:.2f} µs, async "
            f"{async_us:.2f} µs, invariant < {_ASYNC_VS_SYNC_RATIO_MAX}): the "
            f"async handler exists to be faster than the sync chain — either "
            f"both slowed together or the async path is doing sync work."
        )


class TestOperationSpanNoOpPerf:
    _ITERATIONS = 1000

    def test_operation_span_noop_no_regression(
        self,
        bench: _Bench,
        baselines: Baselines,
        monkeypatch: pytest.MonkeyPatch,
        tmp_path: Path,
    ) -> None:
        from mgf.common.observability.otel_setup import operation_span

        monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path))
        ctx = mgf.common.bootstrap(app_name="pc07-bench", app_version="0.0.0")
        try:

            def _span() -> None:
                with operation_span("pc07.bench", attr="x"):
                    pass

            per_call_us = bench.measure(_span, iterations=self._ITERATIONS, warmup=50) * 1e6
        finally:
            ctx.shutdown()
        baselines.assert_no_regression(
            "operation_span_noop_p50_us",
            per_call_us,
            units="us",
            description="operation_span() no-op cost (OTel disabled), per call",
        )


class TestFileSinkLoggingPerf:
    """Per-record cost through the synchronous RotatingFileHandler JSON sink.

    Rule **LG-12** (LOGGING.md §13) — the file write per record is the IO
    surface the project owns. This is the baseline-relative successor to the
    old absolute 100 µs budget in ``tests/unit/test_logging_perf.py`` (which
    was the PF-01 / PAPER-50 anti-pattern: an absolute wall-clock budget that
    false-positived on a loaded CI container at ~154 µs while passing locally).
    """

    def test_per_record_no_regression(
        self,
        bench: _Bench,
        baselines: Baselines,
        monkeypatch: pytest.MonkeyPatch,
        tmp_path: Path,
    ) -> None:
        import contextlib
        from logging.handlers import RotatingFileHandler

        from mgf.common.observability.logging_setup import _wire_logging

        _identity._set_identity("perf-bench", "0.0.0")
        monkeypatch.delenv("JOURNAL_STREAM", raising=False)
        monkeypatch.delenv("INVOCATION_ID", raising=False)

        _wire_logging(level="DEBUG", fmt="json", log_file=tmp_path / "perf.log")
        # Measure the rotating-file path only — drop the console handler so the
        # timed loop doesn't pay for pytest's stderr-capture per record
        # (LOGGING.md §11.1).
        for h in logging.root.handlers[:]:
            if not isinstance(h, RotatingFileHandler):
                logging.root.removeHandler(h)

        log = logging.getLogger("perf.bench.file_sink")

        def _emit() -> None:
            log.info(
                "benchmark message",
                extra={"iteration": 1, "subject": "alice", "kind": "info"},
            )

        try:
            per_record_us = bench.measure(_emit, iterations=5_000, warmup=100) * 1e6
        finally:
            for h in logging.root.handlers[:]:
                with contextlib.suppress(Exception):
                    h.close()
                logging.root.removeHandler(h)

        baselines.assert_no_regression(
            "log_file_record_p50_us",
            per_record_us,
            units="us",
            description=(
                "per-record cost through the RotatingFileHandler JSON sink at INFO, median over 5k"
            ),
        )


class TestBootstrapPerf:
    def test_bootstrap_no_regression(
        self, baselines: Baselines, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        durations: list[float] = []
        for _ in range(5):
            with tempfile.TemporaryDirectory() as tmp:
                monkeypatch.setenv("XDG_STATE_HOME", tmp)
                t0 = time.perf_counter()
                ctx = mgf.common.bootstrap(app_name="perf-bench", app_version="0.0.0")
                durations.append((time.perf_counter() - t0) * 1000)
                ctx.shutdown()
        median_ms = sorted(durations)[len(durations) // 2]
        baselines.assert_no_regression(
            "bootstrap_p50_ms",
            median_ms,
            units="ms",
            description="bootstrap() median cold-start over 5 runs",
            threshold_pct=200,  # cold-start is noisier
        )
