"""Tests for `mgf.test_supervisor.perf` — TS-28 perf reader/renderer.

The supervisor never imports mgf-common; it only parses the JSON wire shape
the mgf-common pytest plugin writes. These tests pin that contract.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING

from mgf.test_supervisor import perf

if TYPE_CHECKING:
    from pathlib import Path


def _artifact(test_id: str, total_ms: float, *, functions=None, spans=None) -> dict:
    return {
        "schema_version": "1.0.0",
        "test_id": test_id,
        "total_ms": total_ms,
        "functions": functions or [],
        "spans": spans or [],
    }


def _write(perf_dir: Path, name: str, payload: dict) -> None:
    perf_dir.mkdir(parents=True, exist_ok=True)
    (perf_dir / name).write_text(json.dumps(payload), encoding="utf-8")


class TestPerfStore:
    def test_indexes_by_test_id_inside_file(self, tmp_path: Path) -> None:
        # Filename intentionally does NOT match the test_id — the store
        # must index by the test_id carried *inside* the artifact.
        _write(tmp_path, "deadbeef.json", _artifact("tests/x.py::test_a", 5.0))
        store = perf.PerfStore(tmp_path)
        store.refresh()
        assert store.get("tests/x.py::test_a") == _artifact("tests/x.py::test_a", 5.0)
        assert store.get("missing") is None

    def test_refresh_is_incremental(self, tmp_path: Path) -> None:
        _write(tmp_path, "a.json", _artifact("t::a", 1.0))
        store = perf.PerfStore(tmp_path)
        store.refresh()
        assert set(store.by_test) == {"t::a"}
        _write(tmp_path, "b.json", _artifact("t::b", 2.0))
        store.refresh()
        assert set(store.by_test) == {"t::a", "t::b"}

    def test_missing_dir_is_fail_open(self, tmp_path: Path) -> None:
        store = perf.PerfStore(tmp_path / "nope")
        store.refresh()  # must not raise
        assert not store

    def test_malformed_file_skipped(self, tmp_path: Path) -> None:
        tmp_path.mkdir(exist_ok=True)
        (tmp_path / "bad.json").write_text("{not json", encoding="utf-8")
        _write(tmp_path, "good.json", _artifact("t::ok", 1.0))
        store = perf.PerfStore(tmp_path)
        store.refresh()
        assert set(store.by_test) == {"t::ok"}

    def test_record_without_test_id_skipped(self, tmp_path: Path) -> None:
        _write(tmp_path, "x.json", {"total_ms": 1.0})  # no test_id
        store = perf.PerfStore(tmp_path)
        store.refresh()
        assert not store


class TestRenderPerfSection:
    def test_none_renders_hint(self) -> None:
        assert "mtest --perf" in perf.render_perf_section(None)

    def test_renders_spans_and_functions(self) -> None:
        rec = _artifact(
            "t::a",
            42.0,
            functions=[
                {"func": "a.py:1(slow)", "ncalls": 2, "tottime_ms": 30.0, "cumtime_ms": 40.0},
                {"func": "b.py:2(fast)", "ncalls": 1, "tottime_ms": 1.0, "cumtime_ms": 2.0},
            ],
            spans=[
                {"label": "load", "ms": 10.0, "depth": 0},
                {"label": "query", "ms": 30.0, "depth": 1},
            ],
        )
        out = perf.render_perf_section(rec)
        assert "total call-phase time: 42.00 ms" in out
        assert "load: 10.00 ms" in out
        assert "    query: 30.00 ms" in out  # depth 1 → indented further
        assert "a.py:1(slow)" in out
        assert "top 2 functions" in out

    def test_top_caps_function_rows(self) -> None:
        funcs = [
            {"func": f"f{i}", "ncalls": 1, "tottime_ms": float(i), "cumtime_ms": float(i)}
            for i in range(20)
        ]
        out = perf.render_perf_section(_artifact("t::a", 1.0, functions=funcs), top=3)
        assert "top 3 functions" in out

    def test_fail_open_on_garbage_values(self) -> None:
        # Non-numeric fields must not raise — coerce to 0.0.
        rec = _artifact(
            "t::a",
            "oops",
            functions=[{"func": "x", "ncalls": "n", "tottime_ms": None, "cumtime_ms": "z"}],
            spans=[{"label": "s", "ms": "bad", "depth": "deep"}],
        )
        out = perf.render_perf_section(rec)  # must not raise
        assert "x" in out


class TestAggregation:
    def test_aggregate_self_time_sums_across_tests(self, tmp_path: Path) -> None:
        _write(
            tmp_path,
            "1.json",
            _artifact(
                "t::a",
                10.0,
                functions=[{"func": "hot", "ncalls": 1, "tottime_ms": 5.0, "cumtime_ms": 5.0}],
            ),
        )
        _write(
            tmp_path,
            "2.json",
            _artifact(
                "t::b",
                10.0,
                functions=[{"func": "hot", "ncalls": 1, "tottime_ms": 7.0, "cumtime_ms": 7.0}],
            ),
        )
        store = perf.PerfStore(tmp_path)
        store.refresh()
        agg = perf.aggregate_self_time(store)
        assert agg[0] == ("hot", 12.0)

    def test_slowest_tests_sorted(self, tmp_path: Path) -> None:
        _write(tmp_path, "1.json", _artifact("t::slow", 100.0))
        _write(tmp_path, "2.json", _artifact("t::fast", 1.0))
        store = perf.PerfStore(tmp_path)
        store.refresh()
        slow = perf.slowest_tests(store, top=2)
        assert slow[0][0] == "t::slow"
        assert slow[1][0] == "t::fast"

    def test_run_summary_mentions_hotspots_and_tests(self, tmp_path: Path) -> None:
        _write(
            tmp_path,
            "1.json",
            _artifact(
                "t::slow",
                100.0,
                functions=[{"func": "hot", "ncalls": 1, "tottime_ms": 50.0, "cumtime_ms": 50.0}],
            ),
        )
        store = perf.PerfStore(tmp_path)
        store.refresh()
        md = perf.render_run_summary(store)
        assert "slowest" in md
        assert "hottest" in md
        assert "t::slow" in md
        assert "hot" in md

    def test_empty_store_summary_is_blank(self, tmp_path: Path) -> None:
        store = perf.PerfStore(tmp_path)
        assert perf.render_run_summary(store) == ""
