"""Tests for the pytest collection subprocess worker."""

from __future__ import annotations

import importlib
import pickle
import sys
from pathlib import Path
from unittest.mock import MagicMock

import pytest


@pytest.fixture(autouse=True)
def _patch_argv(monkeypatch, tmp_path):
    """Ensure sys.argv has enough entries for module-level reads."""
    monkeypatch.setattr(
        sys,
        "argv",
        [
            "_discovery_worker.py",
            str(tmp_path),
            str(tmp_path),
            str(tmp_path / "out.pkl"),
        ],
    )


@pytest.fixture
def _worker_mod():
    """Import the discovery worker module (needs patched argv)."""
    mod_name = "codeflash_python.analysis._discovery_worker"
    mod = (
        importlib.reload(sys.modules[mod_name])
        if mod_name in sys.modules
        else importlib.import_module(mod_name)
    )
    # pytest is only imported under __main__ in the real script,
    # but the plugin callbacks reference it. Inject it.
    mod.pytest = pytest
    return mod


class TestParsePytestCollectionResults:
    """Tests for parse_pytest_collection_results."""

    def _make_item(
        self,
        path: str,
        name: str,
        cls: type | None = None,
        parent_name: str | None = None,
    ) -> MagicMock:
        item = MagicMock()
        item.path = Path(path)
        item.name = name
        item.cls = cls
        if cls is not None:
            item.parent.name = parent_name or cls.__name__
        return item

    def test_top_level_function(self, _worker_mod) -> None:
        """Top-level test function has test_class=None."""
        parse = _worker_mod.parse_pytest_collection_results
        items = [self._make_item("tests/test_a.py", "test_foo")]

        result = parse(items)

        assert 1 == len(result)
        assert "tests/test_a.py" == result[0]["test_file"]
        assert "test_foo" == result[0]["test_function"]
        assert result[0]["test_class"] is None

    def test_method_in_class(self, _worker_mod) -> None:
        """Method inside a test class gets the class name."""
        parse = _worker_mod.parse_pytest_collection_results

        class MyTestClass:
            pass

        items = [
            self._make_item(
                "tests/test_b.py",
                "test_bar",
                cls=MyTestClass,
                parent_name="MyTestClass",
            ),
        ]

        result = parse(items)

        assert 1 == len(result)
        assert "MyTestClass" == result[0]["test_class"]
        assert "test_bar" == result[0]["test_function"]

    def test_multiple_items(self, _worker_mod) -> None:
        """Multiple items are all parsed."""
        parse = _worker_mod.parse_pytest_collection_results
        items = [
            self._make_item("tests/test_a.py", "test_one"),
            self._make_item("tests/test_a.py", "test_two"),
            self._make_item("tests/test_b.py", "test_three"),
        ]

        result = parse(items)

        assert 3 == len(result)
        functions = [r["test_function"] for r in result]
        assert ["test_one", "test_two", "test_three"] == functions

    def test_empty_list(self, _worker_mod) -> None:
        """Empty input returns empty output."""
        parse = _worker_mod.parse_pytest_collection_results
        assert [] == parse([])


class TestPytestCollectionPlugin:
    """Tests for PytestCollectionPlugin."""

    def test_benchmark_items_get_skip_marker(self, _worker_mod) -> None:
        """Items with 'benchmark' in fixturenames are marked as skip."""
        plugin = _worker_mod.PytestCollectionPlugin()

        item_with_benchmark = MagicMock()
        item_with_benchmark.fixturenames = ["benchmark", "tmp_path"]

        item_without_benchmark = MagicMock()
        item_without_benchmark.fixturenames = ["tmp_path"]

        items = [item_with_benchmark, item_without_benchmark]
        plugin.pytest_collection_modifyitems(items)

        item_with_benchmark.add_marker.assert_called_once()
        marker = item_with_benchmark.add_marker.call_args[0][0]
        assert "skip" == marker.name

        item_without_benchmark.add_marker.assert_not_called()

    def test_no_benchmark_items_unchanged(self, _worker_mod) -> None:
        """Items without benchmark fixture are not modified."""
        plugin = _worker_mod.PytestCollectionPlugin()

        item = MagicMock()
        item.fixturenames = ["tmp_path", "capsys"]

        plugin.pytest_collection_modifyitems([item])

        item.add_marker.assert_not_called()

    def test_collection_finish_writes_pickle(
        self,
        _worker_mod,
        tmp_path,
    ) -> None:
        """pytest_collection_finish writes results to pickle file."""
        pickle_path = tmp_path / "results.pkl"

        _worker_mod.pickle_path = str(pickle_path)
        _worker_mod.collected_tests = []

        plugin = _worker_mod.PytestCollectionPlugin()

        session = MagicMock()
        # Use real objects (not MagicMock) for items so they
        # don't contaminate collected_tests with unpicklable mocks.
        # parse_pytest_collection_results runs before pickle.dump,
        # converting items to plain dicts. But the raw items are
        # appended to the global collected_tests list, so we need
        # items whose .path/.name/.cls attributes are real.
        item = MagicMock()
        item.path = Path("tests/test_x.py")
        item.name = "test_example"
        item.cls = None
        item.parent.name = None
        session.items = [item]
        session.config.rootdir = Path("/project")
        session.config.exitstatus = 0

        plugin.pytest_collection_finish(session)

        assert pickle_path.exists()
        with pickle_path.open("rb") as f:
            exit_code, tests, rootdir = pickle.load(f)

        assert 1 == len(tests)
        assert "test_example" == tests[0]["test_function"]
        assert Path("/project") == rootdir

    def test_collection_finish_accumulates_items(
        self,
        _worker_mod,
        tmp_path,
    ) -> None:
        """Multiple collection rounds accumulate items."""
        pickle_path = tmp_path / "results.pkl"

        _worker_mod.pickle_path = str(pickle_path)
        _worker_mod.collected_tests = []

        plugin = _worker_mod.PytestCollectionPlugin()

        item1 = MagicMock()
        item1.path = Path("tests/test_a.py")
        item1.name = "test_one"
        item1.cls = None

        item2 = MagicMock()
        item2.path = Path("tests/test_b.py")
        item2.name = "test_two"
        item2.cls = None

        session = MagicMock()
        session.items = [item1, item2]
        session.config.rootdir = Path("/project")
        session.config.exitstatus = 0

        plugin.pytest_collection_finish(session)

        with pickle_path.open("rb") as f:
            _, tests, _ = pickle.load(f)

        assert 2 == len(tests)
