from __future__ import annotations

import subprocess
from pathlib import Path
from unittest.mock import patch

from codeflash_python.test_discovery.models import TestType
from codeflash_python.testing._test_runner import (
    execute_test_subprocess,
    run_behavioral_tests,
    run_benchmarking_tests,
    run_line_profile_tests,
)
from codeflash_python.testing.models import TestFile, TestFiles


def make_completed_process(
    returncode: int = 0,
    stdout: str = "",
    stderr: str = "",
) -> subprocess.CompletedProcess[str]:
    """Create a CompletedProcess with sensible defaults."""
    return subprocess.CompletedProcess(
        args=["pytest"],
        returncode=returncode,
        stdout=stdout,
        stderr=stderr,
    )


def make_test_files(
    tmp_path: Path,
    *,
    with_benchmarking: bool = False,
) -> TestFiles:
    """Create a TestFiles with one entry pointing to tmp_path files."""
    instrumented = tmp_path / "test_instrumented.py"
    instrumented.touch()
    benchmarking = None
    if with_benchmarking:
        benchmarking = tmp_path / "test_bench.py"
        benchmarking.touch()
    tf = TestFile(
        original_file_path=tmp_path / "test_orig.py",
        instrumented_behavior_file_path=instrumented,
        benchmarking_file_path=benchmarking,
        test_type=TestType.EXISTING_UNIT_TEST,
    )
    return TestFiles(test_files=[tf])


class TestExecuteTestSubprocess:
    """execute_test_subprocess subprocess invocation."""

    @patch("codeflash_python.testing._test_runner.subprocess.run")
    def test_basic_execution(self, mock_run) -> None:
        """Calls subprocess.run with correct args."""
        mock_run.return_value = make_completed_process()
        cwd = Path("/project")
        result = execute_test_subprocess(
            ["pytest", "test.py"],
            cwd=cwd,
            env=None,
            timeout=30,
        )
        mock_run.assert_called_once_with(
            ["pytest", "test.py"],
            cwd=cwd,
            env=None,
            timeout=30,
            check=False,
            text=True,
            capture_output=True,
        )
        assert 0 == result.returncode

    @patch("codeflash_python.testing._test_runner.subprocess.run")
    def test_captures_output(self, mock_run) -> None:
        """Always passes text=True and capture_output=True."""
        mock_run.return_value = make_completed_process(
            stdout="PASSED",
        )
        result = execute_test_subprocess(
            ["pytest"],
            cwd=Path("/project"),
            env=None,
        )
        call_kwargs = mock_run.call_args.kwargs
        assert call_kwargs["text"] is True
        assert call_kwargs["capture_output"] is True
        assert "PASSED" == result.stdout

    @patch("codeflash_python.testing._test_runner.subprocess.run")
    def test_default_timeout(self, mock_run) -> None:
        """Default timeout is 600 seconds."""
        mock_run.return_value = make_completed_process()
        execute_test_subprocess(
            ["pytest"],
            cwd=Path("/project"),
            env=None,
        )
        assert 600 == mock_run.call_args.kwargs["timeout"]


class TestRunBehavioralTests:
    """run_behavioral_tests command building."""

    @patch("codeflash_python.testing._test_runner.execute_test_subprocess")
    def test_builds_correct_command(
        self,
        mock_exec,
        tmp_path: Path,
    ) -> None:
        """Command includes session scope and min/max loops of 1."""
        mock_exec.return_value = make_completed_process()
        test_files = make_test_files(tmp_path)

        run_behavioral_tests(
            test_files,
            test_env={"PATH": "/bin"},
            cwd=tmp_path,
        )

        cmd = mock_exec.call_args[0][0]
        cmd_str = " ".join(cmd)
        assert "--codeflash_loops_scope=session" in cmd_str
        assert "--codeflash_min_loops=1" in cmd_str
        assert "--codeflash_max_loops=1" in cmd_str

    @patch("codeflash_python.testing._test_runner.execute_test_subprocess")
    def test_returns_result_path(
        self,
        mock_exec,
        tmp_path: Path,
    ) -> None:
        """Returns a Path for XML results as first element."""
        mock_exec.return_value = make_completed_process()
        test_files = make_test_files(tmp_path)

        result_path, _, _, _ = run_behavioral_tests(
            test_files,
            test_env={"PATH": "/bin"},
            cwd=tmp_path,
        )
        assert isinstance(result_path, Path)
        assert "pytest_results.xml" in str(result_path)

    @patch("codeflash_python.testing._test_runner.execute_test_subprocess")
    def test_blocklists_plugins(
        self,
        mock_exec,
        tmp_path: Path,
    ) -> None:
        """Blocklists benchmark, codspeed, xdist, sugar plugins."""
        mock_exec.return_value = make_completed_process()
        test_files = make_test_files(tmp_path)

        run_behavioral_tests(
            test_files,
            test_env={"PATH": "/bin"},
            cwd=tmp_path,
        )

        cmd = mock_exec.call_args[0][0]
        cmd_str = " ".join(cmd)
        assert "-p no:benchmark" in cmd_str
        assert "-p no:xdist" in cmd_str

    @patch("codeflash_python.testing._test_runner.execute_test_subprocess")
    def test_sets_pytest_plugin_env(
        self,
        mock_exec,
        tmp_path: Path,
    ) -> None:
        """Sets PYTEST_PLUGINS env var for codeflash plugin."""
        mock_exec.return_value = make_completed_process()
        test_files = make_test_files(tmp_path)

        run_behavioral_tests(
            test_files,
            test_env={"PATH": "/bin"},
            cwd=tmp_path,
        )

        env = mock_exec.call_args.kwargs["env"]
        assert "PYTEST_PLUGINS" in env


class TestRunBenchmarkingTests:
    """run_benchmarking_tests command building."""

    @patch("codeflash_python.testing._test_runner.execute_test_subprocess")
    def test_includes_loop_params(
        self,
        mock_exec,
        tmp_path: Path,
    ) -> None:
        """Command includes min_loops, max_loops, target_duration."""
        mock_exec.return_value = make_completed_process()
        test_files = make_test_files(
            tmp_path,
            with_benchmarking=True,
        )

        run_benchmarking_tests(
            test_files,
            test_env={"PATH": "/bin"},
            cwd=tmp_path,
            min_loops=10,
            max_loops=5000,
            target_duration_seconds=5.0,
        )

        cmd = mock_exec.call_args[0][0]
        cmd_str = " ".join(cmd)
        assert "--codeflash_min_loops=10" in cmd_str
        assert "--codeflash_max_loops=5000" in cmd_str
        assert "--codeflash_seconds=5.0" in cmd_str

    @patch("codeflash_python.testing._test_runner.execute_test_subprocess")
    def test_stability_check_flag(
        self,
        mock_exec,
        tmp_path: Path,
    ) -> None:
        """Command includes --codeflash_stability_check=true."""
        mock_exec.return_value = make_completed_process()
        test_files = make_test_files(
            tmp_path,
            with_benchmarking=True,
        )

        run_benchmarking_tests(
            test_files,
            test_env={"PATH": "/bin"},
            cwd=tmp_path,
        )

        cmd = mock_exec.call_args[0][0]
        cmd_str = " ".join(cmd)
        assert "--codeflash_stability_check=true" in cmd_str

    @patch("codeflash_python.testing._test_runner.execute_test_subprocess")
    def test_returns_result_path(
        self,
        mock_exec,
        tmp_path: Path,
    ) -> None:
        """Returns a Path for XML results as first element."""
        mock_exec.return_value = make_completed_process()
        test_files = make_test_files(
            tmp_path,
            with_benchmarking=True,
        )

        result_path, _ = run_benchmarking_tests(
            test_files,
            test_env={"PATH": "/bin"},
            cwd=tmp_path,
        )
        assert isinstance(result_path, Path)


class TestRunLineProfileTests:
    """run_line_profile_tests command building."""

    @patch("codeflash_python.testing._test_runner.execute_test_subprocess")
    def test_sets_line_profile_env(
        self,
        mock_exec,
        tmp_path: Path,
    ) -> None:
        """Sets LINE_PROFILE=1 in the test environment."""
        mock_exec.return_value = make_completed_process()
        test_files = make_test_files(
            tmp_path,
            with_benchmarking=True,
        )

        run_line_profile_tests(
            test_files,
            test_env={"PATH": "/bin"},
            cwd=tmp_path,
        )

        env = mock_exec.call_args.kwargs["env"]
        assert "1" == env["LINE_PROFILE"]

    @patch("codeflash_python.testing._test_runner.execute_test_subprocess")
    def test_single_loop(
        self,
        mock_exec,
        tmp_path: Path,
    ) -> None:
        """Line profiling runs with min_loops=1, max_loops=1."""
        mock_exec.return_value = make_completed_process()
        test_files = make_test_files(
            tmp_path,
            with_benchmarking=True,
        )

        run_line_profile_tests(
            test_files,
            test_env={"PATH": "/bin"},
            cwd=tmp_path,
        )

        cmd = mock_exec.call_args[0][0]
        cmd_str = " ".join(cmd)
        assert "--codeflash_min_loops=1" in cmd_str
        assert "--codeflash_max_loops=1" in cmd_str
