"""One-spawn collection with real markers (M2 Wave-2, PLAN §2.3).

`collect_tests` runs pytest ONCE and gets node-ids + markers from the
injected marker-dump plugin — the 13 per-marker `--collect-only`
spawns are gone. These tests drive a real pytest subprocess against a
tiny temp project and count the spawns.
"""

from __future__ import annotations

import subprocess
import sys
from typing import TYPE_CHECKING

import pytest

from mgf.test_supervisor import supervisor as sup_mod
from mgf.test_supervisor.supervisor import collect_test_markers, collect_tests

if TYPE_CHECKING:
    from pathlib import Path

_PYPROJECT = """\
[tool.pytest.ini_options]
markers = [
    "smoke: fast sanity",
    "integration: cross-module",
    "e2e: end to end",
]
"""

_TEST_FILE = """\
import pytest


@pytest.mark.smoke
def test_smoke_marked():
    assert True


@pytest.mark.integration
@pytest.mark.e2e
def test_double_marked():
    assert True


def test_unmarked():
    assert True
"""


@pytest.fixture
def tiny_project(tmp_path: Path) -> Path:
    (tmp_path / "pyproject.toml").write_text(_PYPROJECT, encoding="utf-8")
    tests = tmp_path / "tests"
    tests.mkdir()
    (tests / "__init__.py").write_text("", encoding="utf-8")
    (tests / "test_canned.py").write_text(_TEST_FILE, encoding="utf-8")
    return tmp_path


@pytest.mark.integration
def test_one_spawn_returns_ids_and_markers(
    tiny_project: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    spawns: list[list[str]] = []
    real_run = subprocess.run

    def counting_run(argv, **kwargs):  # type: ignore[no-untyped-def]
        spawns.append(list(argv))
        return real_run(argv, **kwargs)

    monkeypatch.setattr(sup_mod.subprocess, "run", counting_run)

    out = collect_tests([sys.executable, "-m", "pytest"], tiny_project)

    assert len(spawns) == 1, f"collection must be ONE pytest spawn, saw {len(spawns)}"
    by_id = dict(out)
    assert by_id["tests/test_canned.py::test_smoke_marked"] == ("smoke",)
    assert by_id["tests/test_canned.py::test_double_marked"] == ("e2e", "integration")
    assert by_id["tests/test_canned.py::test_unmarked"] == ()
    # Collection order preserved (pytest's own order).
    assert [tid for tid, _ in out] == [
        "tests/test_canned.py::test_smoke_marked",
        "tests/test_canned.py::test_double_marked",
        "tests/test_canned.py::test_unmarked",
    ]


@pytest.mark.integration
def test_collect_test_markers_is_a_derivation_not_extra_spawns(
    tiny_project: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    spawns: list[list[str]] = []
    real_run = subprocess.run

    def counting_run(argv, **kwargs):  # type: ignore[no-untyped-def]
        spawns.append(list(argv))
        return real_run(argv, **kwargs)

    monkeypatch.setattr(sup_mod.subprocess, "run", counting_run)

    markers = collect_test_markers([sys.executable, "-m", "pytest"], tiny_project)

    assert len(spawns) == 1, "collect_test_markers must not re-probe per marker"
    assert markers == {
        "tests/test_canned.py::test_smoke_marked": ("smoke",),
        "tests/test_canned.py::test_double_marked": ("e2e", "integration"),
    }
    # Unmarked tests are omitted (callers .get(tid, ()) — the old contract).
    assert "tests/test_canned.py::test_unmarked" not in markers


def test_dump_parse_tolerates_garbage(tmp_path: Path) -> None:
    bad = tmp_path / "dump.json"
    bad.write_text("{not json", encoding="utf-8")
    assert sup_mod._parse_collect_dump(bad) is None
    bad.write_text('{"a": 1}', encoding="utf-8")  # not a list
    assert sup_mod._parse_collect_dump(bad) is None
    bad.write_text('[["t::x", ["smoke"]], "malformed-row", ["t::y", ["e2e"]]]', encoding="utf-8")
    parsed = sup_mod._parse_collect_dump(bad)
    assert parsed == [("t::x", ("smoke",)), ("t::y", ("e2e",))]
