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

from __future__ import annotations

import argparse
import io
import json
import os
import subprocess
import sys
from contextlib import redirect_stdout
from pathlib import Path

import pytest

from mgf.fed.cli._bench import _resolve_perf_dir, _run_rebaseline, 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"
        )


# ---------------------------------------------------------------------------
# PAPER-153 — `bench rebaseline` operates on the CALLER's tree.
#
# `_run_rebaseline` used to resolve `Path(__file__).parents[4] / "tests" / "perf"`:
# the suite relative to the CLI's OWN install. That is correct-by-layout for a source
# checkout of the repo the code shipped in, and reachable from nowhere else — and
# after the 0.47 CLI split, from nowhere at all. Installed, `parents[4]` is
# `…/lib/python3.13`, whose `tests/perf` will never exist; from mgf-fed's own checkout
# `parents[4]` is the repo root, which has no `tests/perf` either (the perf suite
# belongs to mgf-common). A documented remedy — quoted in the failure message of the
# very gate it is meant to fix — that succeeded from zero directories.
#
# The paper named its own missing test: one that RUNS THE COMMAND FROM A DIRECTORY
# THAT IS NOT THE PACKAGE'S OWN INSTALL. Its absence is how this survived a CLI split,
# so `test_cli_entry_from_outside_the_install_*` below is the paper's ask, honoured
# literally — a real `python -m mgf.fed` subprocess with `cwd` in a tmp tree.
# ---------------------------------------------------------------------------


class TestRebaselineResolvesTheCallersTree:
    def test_finds_tests_perf_in_the_current_directory(self, tmp_path: Path) -> None:
        perf = tmp_path / "tests" / "perf"
        perf.mkdir(parents=True)
        found, why = _resolve_perf_dir(None, tmp_path)
        assert found == perf.resolve(), why

    def test_walks_up_to_the_repo_root_from_a_nested_subdir(self, tmp_path: Path) -> None:
        """The caller stands in `src/pkg/sub/`, as a caller usually does."""
        perf = tmp_path / "tests" / "perf"
        perf.mkdir(parents=True)
        deep = tmp_path / "src" / "pkg" / "sub"
        deep.mkdir(parents=True)
        found, _ = _resolve_perf_dir(None, deep)
        assert found == perf.resolve()

    def test_path_override_accepts_a_repo_root(self, tmp_path: Path) -> None:
        perf = tmp_path / "repo" / "tests" / "perf"
        perf.mkdir(parents=True)
        found, _ = _resolve_perf_dir(str(tmp_path / "repo"), tmp_path)
        assert found == perf.resolve()

    def test_path_override_accepts_the_perf_dir_itself(self, tmp_path: Path) -> None:
        perf = tmp_path / "repo" / "tests" / "perf"
        perf.mkdir(parents=True)
        found, _ = _resolve_perf_dir(str(perf), tmp_path)
        assert found == perf.resolve()

    def test_path_override_beats_the_cwd_search(self, tmp_path: Path) -> None:
        """`--path` is the explicit 'otherwise' — it must not silently lose to a
        `tests/perf` that happens to sit above the cwd."""
        (tmp_path / "here" / "tests" / "perf").mkdir(parents=True)
        elsewhere = tmp_path / "other" / "tests" / "perf"
        elsewhere.mkdir(parents=True)
        found, _ = _resolve_perf_dir(str(tmp_path / "other"), tmp_path / "here")
        assert found == elsewhere.resolve()

    def test_error_names_WHAT_was_looked_for_and_WHERE(self, tmp_path: Path) -> None:
        """The old message named `mgf-common` — a repo the code could no longer see —
        and never said where it looked. Both halves are pinned here."""
        found, why = _resolve_perf_dir(None, tmp_path)
        assert found is None
        assert "tests/perf" in why  # WHAT
        assert str(tmp_path.resolve()) in why  # WHERE it started
        assert "--path" in why  # the way out
        assert "mgf-common" not in why  # the 0.47-split residue is gone

    def test_error_for_a_bad_path_override_names_both_candidates(self, tmp_path: Path) -> None:
        found, why = _resolve_perf_dir(str(tmp_path / "nope"), tmp_path)
        assert found is None
        assert str(tmp_path / "nope" / "tests" / "perf") in why
        assert str(tmp_path / "nope") in why

    def test_run_rebaseline_returns_2_and_explains_when_nothing_is_found(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
    ) -> None:
        monkeypatch.chdir(tmp_path)
        assert _run_rebaseline(None, str(tmp_path / "absent")) == 2
        assert "no perf suite" in capsys.readouterr().err


#: A perf suite seeded into a tmp tree. It asserts the two things the command
#: promises: the rebaseline env var is set, and the test that ran is the one in the
#: CALLER's directory (it writes its own resolved location as the proof artifact).
_SEEDED_PERF_TEST = """\
import os
from pathlib import Path

import pytest


@pytest.mark.perf
def test_rebaseline_marker():
    assert os.environ.get("MGF_PERF_REBASELINE") == "1"
    Path(__file__).with_name("ran-here.txt").write_text(str(Path(__file__).parent))
"""

#: The seeded repo registers the `perf` marker itself, as a real federation member
#: does — and its presence makes tmp_path the rootdir, so the run is unambiguously
#: the caller's and not this repo's.
_SEEDED_PYTEST_INI = """\
[pytest]
markers =
    perf: performance pin (PF-* + TS-14); tier-3
"""


def _seed_perf_suite(root: Path) -> Path:
    perf = root / "tests" / "perf"
    perf.mkdir(parents=True)
    (perf / "test_seeded_perf.py").write_text(_SEEDED_PERF_TEST, encoding="utf-8")
    (root / "pytest.ini").write_text(_SEEDED_PYTEST_INI, encoding="utf-8")
    return perf


def _run_cli(*argv: str, cwd: Path) -> subprocess.CompletedProcess[str]:
    """Invoke the real CLI entry point as a subprocess rooted at ``cwd``.

    `python -m mgf.fed` is the same entry `mgf-fed` resolves to
    (`[project.scripts] mgf-fed = "mgf.fed.__main__:main"`), and running it out of
    process with an unrelated `cwd` is the only shape that could ever have caught
    PAPER-153: an in-process call inherits the test runner's cwd, which IS a checkout.
    """
    env = {**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[3] / "src")}
    env.pop("MGF_PERF_REBASELINE", None)
    return subprocess.run(  # noqa: S603 — fixed argv, no shell
        [sys.executable, "-m", "mgf.fed", *argv],
        cwd=str(cwd),
        env=env,
        capture_output=True,
        text=True,
        timeout=300,
    )


@pytest.mark.integration  # launches a real CLI subprocess (which launches pytest)
class TestRebaselineFromOutsideTheInstall:
    """The paper's own ask. Nothing here imports the code under test in-process."""

    def test_cli_entry_from_outside_the_install_rebaselines_the_callers_tree(
        self, tmp_path: Path
    ) -> None:
        perf = _seed_perf_suite(tmp_path)
        done = _run_cli("bench", "rebaseline", cwd=tmp_path)
        assert done.returncode == 0, f"stdout={done.stdout}\nstderr={done.stderr}"
        proof = perf / "ran-here.txt"
        assert proof.is_file(), (
            f"the command did not run the CALLER's suite: {done.stdout}\n{done.stderr}"
        )
        # ...and the suite it ran is the seeded one, not one belonging to any install.
        assert proof.read_text().strip() == str(perf)

    def test_cli_entry_from_outside_the_install_explains_when_there_is_no_suite(
        self, tmp_path: Path
    ) -> None:
        """The same directory WITHOUT a seeded suite: exit 2, and the message names
        what it looked for and where — no dead-end, no phantom mgf-common."""
        done = _run_cli("bench", "rebaseline", cwd=tmp_path)
        assert done.returncode == 2
        assert "tests/perf" in done.stderr
        assert "mgf-common source checkout" not in done.stderr

    def test_cli_entry_honours_path_from_an_unrelated_cwd(self, tmp_path: Path) -> None:
        """`--path` reaches a tree the cwd cannot see — the escape hatch works from
        the one place the old code always ran from: somewhere else entirely."""
        repo = tmp_path / "repo"
        repo.mkdir()
        perf = _seed_perf_suite(repo)
        elsewhere = tmp_path / "elsewhere"
        elsewhere.mkdir()
        done = _run_cli("bench", "rebaseline", "--path", str(repo), cwd=elsewhere)
        assert done.returncode == 0, f"stdout={done.stdout}\nstderr={done.stderr}"
        assert (perf / "ran-here.txt").is_file()
