"""Tests for ``mgf-common bench`` subcommand."""

from __future__ import annotations

import argparse
import io
import json
from contextlib import redirect_stdout

import pytest

from mgf.fed.cli._bench import add_bench_args, run_bench


@pytest.fixture(autouse=True)
def _reset_bootstrap_state() -> None:
    """Tear down any AppContext bench leaves on the process-global.

    The bench runs real bootstrap() calls which set
    ``mgf.common._lifecycle._GLOBAL_CONTEXT``; without explicit
    cleanup, sibling tests (config, django, lifecycle) inherit the
    leftover bench-app context and break.
    """
    import mgf.common._lifecycle as lc

    yield
    if lc._GLOBAL_CONTEXT is not None:
        lc._GLOBAL_CONTEXT.shutdown()
        lc._GLOBAL_CONTEXT = None


def _make_args(*, quick: bool = True, json_out: bool = False) -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    add_bench_args(parser)
    return parser.parse_args(
        ["--quick"] * quick + ["--json"] * json_out,
    )


class TestBenchHumanOutput:
    def test_runs_and_returns_zero(self) -> None:
        # Capture stdout so the test runner output isn't polluted.
        buffer = io.StringIO()
        with redirect_stdout(buffer):
            exit_code = run_bench(_make_args(quick=True))
        assert exit_code == 0

    def test_human_output_contains_expected_rows(self) -> None:
        buffer = io.StringIO()
        with redirect_stdout(buffer):
            run_bench(_make_args(quick=True))
        output = buffer.getvalue()

        # All seven measurements must appear.
        assert "current_context()" in output
        assert "bootstrap (cold)" in output
        assert "bootstrap (warm)" in output
        assert "operation_span open+close" in output
        assert "log call (sync, JSON)" in output
        assert "log call (async, JSON)" in output
        assert "redaction pass" in output

    def test_human_output_includes_walltime(self) -> None:
        buffer = io.StringIO()
        with redirect_stdout(buffer):
            run_bench(_make_args(quick=True))
        assert "total walltime:" in buffer.getvalue()


class TestBenchJsonOutput:
    def test_json_output_is_valid(self) -> None:
        buffer = io.StringIO()
        with redirect_stdout(buffer):
            run_bench(_make_args(quick=True, json_out=True))
        payload = json.loads(buffer.getvalue())

        assert "version" in payload
        assert "walltime_s" in payload
        assert "rows" in payload
        assert len(payload["rows"]) == 7

    def test_json_rows_have_expected_shape(self) -> None:
        buffer = io.StringIO()
        with redirect_stdout(buffer):
            run_bench(_make_args(quick=True, json_out=True))
        payload = json.loads(buffer.getvalue())

        for row in payload["rows"]:
            assert "name" in row
            assert "ns_per_call" in row
            assert "iterations" in row
            assert isinstance(row["ns_per_call"], (int, float))
            assert row["ns_per_call"] > 0
            assert row["iterations"] >= 1


class TestBenchPerformanceContract:
    @pytest.mark.perf  # timing-based bench (tier-3): excluded from the fast PR gate —
    # it runs the full+quick benches and can exceed the per-test timeout on a
    # loaded/shared CI runner (e.g. the self-hosted Woodpecker host).
    def test_quick_mode_is_faster_than_full(self) -> None:
        # `--quick` targets ~1s vs the full ~5s pass, so it MUST do
        # meaningfully less work. Asserted as a relative invariant
        # (quick < full) rather than an absolute wall-clock budget: an
        # absolute bound is the PF-01 anti-pattern and false-positives on
        # slow / loaded runners (precise budgets live in tests/perf/).
        import time

        def _elapsed(*, quick: bool) -> float:
            buffer = io.StringIO()
            start = time.perf_counter()
            with redirect_stdout(buffer):
                run_bench(_make_args(quick=quick))
            return time.perf_counter() - start

        full = _elapsed(quick=False)
        quick = _elapsed(quick=True)
        assert quick < full, (
            f"quick bench ({quick:.2f}s) was not faster than full ({full:.2f}s) — "
            f"quick mode should run meaningfully fewer iterations"
        )
