"""Unit tests for remote test suites (runner mocked)."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

from tests.remote.runner import BackgroundProcess, CommandResult, DeviceInfo
from tests.remote.suites import SUITE_REGISTRY
from tests.remote.suites.base import BaseSuite, StepStatus, SuiteResult
from tests.remote.suites.clean import CleanSuite
from tests.remote.suites.doctor import DoctorSuite
from tests.remote.suites.full import FullSuite
from tests.remote.suites.install import InstallSuite
from tests.remote.suites.serve import ServeSuite
from tests.remote.suites.uninstall import UninstallSuite
from tests.remote.suites.upgrade import UpgradeSuite

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _mock_runner(
    shell: str = "bash",
    os_name: str = "linux",
    command_results: dict[str, CommandResult] | None = None,
) -> MagicMock:
    """Create a mock RemoteTestRunner."""
    runner = MagicMock()
    runner.device = DeviceInfo(
        hostname="test.local",
        username="user",
        shell=shell,
        os=os_name,
    )

    default_result = CommandResult(
        command="default",
        exit_code=0,
        stdout="ok",
        stderr="",
        duration_s=0.1,
    )

    if command_results:

        async def run_cmd(cmd: str, timeout: float = 60.0) -> CommandResult:
            for key, result in command_results.items():
                if key in cmd:
                    return result
            return default_result

        runner.run_command = AsyncMock(side_effect=run_cmd)
    else:
        runner.run_command = AsyncMock(return_value=default_result)

    runner.run_background = AsyncMock(return_value=BackgroundProcess(command="llmhosts serve", pid=12345))
    runner.stop_background = AsyncMock(
        return_value=CommandResult(command="stop", exit_code=0, stdout="", stderr="", duration_s=0.1)
    )

    return runner


# ---------------------------------------------------------------------------
# Suite registry tests
# ---------------------------------------------------------------------------


class TestSuiteRegistry:
    def test_all_suites_registered(self) -> None:
        expected = {"clean", "install", "doctor", "serve", "inference", "upgrade", "uninstall", "full"}
        assert set(SUITE_REGISTRY.keys()) == expected

    def test_all_suites_are_base_suite(self) -> None:
        for name, cls in SUITE_REGISTRY.items():
            assert issubclass(cls, BaseSuite), f"{name} is not a BaseSuite subclass"


# ---------------------------------------------------------------------------
# Base suite tests
# ---------------------------------------------------------------------------


class TestBaseSuite:
    async def test_run_captures_timing(self) -> None:
        suite = CleanSuite()
        runner = _mock_runner(
            command_results={
                "pip uninstall": CommandResult("pip uninstall llmhosts -y", 0, "done", "", 1.0),
                "rm -rf": CommandResult("rm -rf", 0, "", "", 0.1),
                "llmhosts --version": CommandResult("llmhosts --version", 127, "", "not found", 0.1),
            }
        )

        result = await suite.run(runner, device_name="test-device")
        assert result.suite_name == "clean"
        assert result.device_name == "test-device"
        assert result.total_duration_s >= 0
        assert result.started_at
        assert result.finished_at

    async def test_run_handles_crash(self) -> None:
        """If the suite crashes, it records an error step instead of raising."""

        class CrashingSuite(BaseSuite):
            name = "crash"
            description = "Always crashes"

            async def execute(self, runner: MagicMock, result: SuiteResult) -> None:
                raise RuntimeError("boom")

        suite = CrashingSuite()
        runner = _mock_runner()

        result = await suite.run(runner, device_name="test")
        assert not result.success
        assert result.errors == 1
        assert "boom" in result.steps[0].error_message


# ---------------------------------------------------------------------------
# Clean suite tests
# ---------------------------------------------------------------------------


class TestCleanSuite:
    async def test_clean_success(self) -> None:
        runner = _mock_runner(
            command_results={
                "pip uninstall": CommandResult("pip uninstall", 0, "done", "", 1.0),
                "rm -rf": CommandResult("rm", 0, "", "", 0.1),
                "llmhosts --version": CommandResult("llmhosts --version", 127, "", "not found", 0.1),
            }
        )
        result = await CleanSuite().run(runner)
        assert result.success
        assert result.passed == 3

    async def test_clean_windows_uses_test_path(self) -> None:
        runner = _mock_runner(
            shell="powershell",
            os_name="windows",
            command_results={
                "pip uninstall": CommandResult("pip", 0, "", "", 0.5),
                "Test-Path": CommandResult("test-path", 0, "", "", 0.1),
                "llmhosts --version": CommandResult("ver", 127, "", "", 0.1),
            },
        )
        await CleanSuite().run(runner)
        # Verify the Windows-specific command was used
        calls = [call.args[0] for call in runner.run_command.call_args_list]
        assert any("Test-Path" in c for c in calls)


# ---------------------------------------------------------------------------
# Install suite tests
# ---------------------------------------------------------------------------


class TestInstallSuite:
    async def test_install_success(self) -> None:
        runner = _mock_runner(
            command_results={
                "pip install": CommandResult("pip install", 0, "Successfully installed llmhosts-0.5.0", "", 10.0),
                "--version": CommandResult("llmhosts --version", 0, "llmhosts 0.5.0", "", 0.5),
                "--help": CommandResult("llmhosts --help", 0, "Usage: llmhosts [OPTIONS]", "", 0.3),
            }
        )
        result = await InstallSuite().run(runner)
        assert result.success
        assert result.passed == 3

    async def test_install_pip_failure_stops_early(self) -> None:
        runner = _mock_runner(
            command_results={
                "pip install": CommandResult("pip install", 1, "", "error: no matching", 5.0),
            }
        )
        result = await InstallSuite().run(runner)
        assert not result.success
        assert result.failed == 1
        assert result.passed == 0  # Stops after pip install fails


# ---------------------------------------------------------------------------
# Doctor suite tests
# ---------------------------------------------------------------------------


class TestDoctorSuite:
    async def test_doctor_success(self) -> None:
        runner = _mock_runner(
            command_results={
                "llmhosts doctor": CommandResult(
                    "doctor",
                    0,
                    "GPU Detection [OK]\nBackend [OK]\nConfig [PASS]\n",
                    "",
                    3.0,
                ),
            }
        )
        result = await DoctorSuite().run(runner)
        assert result.success

    async def test_doctor_detects_traceback(self) -> None:
        runner = _mock_runner(
            command_results={
                "llmhosts doctor": CommandResult(
                    "doctor",
                    0,
                    "Traceback (most recent call last):\n  File...\nException: bad",
                    "",
                    1.0,
                ),
            }
        )
        result = await DoctorSuite().run(runner)
        # Even though exit_code=0, traceback detected means a fail step
        has_traceback_fail = any(s.name == "no_crash" and s.status == StepStatus.FAILED for s in result.steps)
        assert has_traceback_fail


# ---------------------------------------------------------------------------
# Serve suite tests
# ---------------------------------------------------------------------------


class TestServeSuite:
    async def test_serve_success(self) -> None:
        call_count = 0

        async def mock_run_command(cmd: str, timeout: float = 60.0) -> CommandResult:
            nonlocal call_count
            call_count += 1
            if "health" in cmd or "Invoke-WebRequest" in cmd:
                return CommandResult(cmd, 0, '{"status":"ok"}', "", 0.2)
            if "models" in cmd:
                return CommandResult(cmd, 0, '{"data":[{"id":"llama3:8b"}]}', "", 0.3)
            return CommandResult(cmd, 0, "ok", "", 0.1)

        runner = _mock_runner()
        runner.run_command = AsyncMock(side_effect=mock_run_command)

        result = await ServeSuite().run(runner)
        # Should have start, health, models, and stop steps
        step_names = [s.name for s in result.steps]
        assert "start_server" in step_names
        assert "health_check" in step_names
        assert "model_discovery" in step_names
        assert "stop_server" in step_names


# ---------------------------------------------------------------------------
# Uninstall suite tests
# ---------------------------------------------------------------------------


class TestUninstallSuite:
    async def test_uninstall_success(self) -> None:
        runner = _mock_runner(
            command_results={
                "pip uninstall": CommandResult("pip uninstall", 0, "Uninstalled llmhosts", "", 2.0),
                "--version": CommandResult("llmhosts --version", 127, "", "not found", 0.1),
            }
        )
        result = await UninstallSuite().run(runner)
        assert result.success
        assert result.passed == 3  # uninstall + verify removed + preserve note


# ---------------------------------------------------------------------------
# Upgrade suite tests
# ---------------------------------------------------------------------------


class TestUpgradeSuite:
    async def test_upgrade_success(self) -> None:
        version_call_count = 0

        async def mock_run(cmd: str, timeout: float = 60.0) -> CommandResult:
            nonlocal version_call_count
            if "--version" in cmd:
                version_call_count += 1
                if version_call_count == 1:
                    return CommandResult(cmd, 0, "llmhosts 0.4.0", "", 0.3)
                return CommandResult(cmd, 0, "llmhosts 0.5.0", "", 0.3)
            if "pip install --upgrade" in cmd:
                return CommandResult(cmd, 0, "Upgraded to 0.5.0", "", 8.0)
            if "doctor" in cmd:
                return CommandResult(cmd, 0, "All checks passed", "", 3.0)
            return CommandResult(cmd, 0, "ok", "", 0.1)

        runner = _mock_runner()
        runner.run_command = AsyncMock(side_effect=mock_run)

        result = await UpgradeSuite().run(runner)
        assert result.success
        assert result.passed == 4  # record, upgrade, verify, doctor


# ---------------------------------------------------------------------------
# Full suite tests
# ---------------------------------------------------------------------------


class TestFullSuite:
    async def test_full_suite_fail_fast(self) -> None:
        """Full suite should stop after first sub-suite failure by default."""
        # Make install fail so subsequent suites are skipped
        runner = _mock_runner(
            command_results={
                "pip uninstall": CommandResult("pip", 0, "", "", 0.1),
                "rm -rf": CommandResult("rm", 0, "", "", 0.1),
                "llmhosts --version": CommandResult("ver", 127, "", "not found", 0.1),
                "pip install llmhosts": CommandResult("pip install", 1, "", "error", 5.0),
            }
        )

        result = await FullSuite().run(runner)
        assert not result.success

        # Should have clean steps + install failure + fail_fast skip, but NOT doctor/serve steps
        step_names = [s.name for s in result.steps]
        assert any("clean" in n for n in step_names)
        assert any("install" in n for n in step_names)
        assert "fail_fast" in step_names
        # Doctor should not have run
        assert not any("doctor/run_doctor" in n for n in step_names)

    async def test_full_suite_continue_on_failure(self) -> None:
        """With continue_on_failure, should run all sub-suites."""
        runner = _mock_runner()  # All commands succeed

        result = await FullSuite(continue_on_failure=True).run(runner)
        # All sub-suites should have executed
        step_names = [s.name for s in result.steps]
        assert any("clean" in n for n in step_names)
        assert any("install" in n for n in step_names)
        assert any("doctor" in n for n in step_names)
        assert any("serve" in n for n in step_names)
        assert any("inference" in n for n in step_names)
        assert any("upgrade" in n for n in step_names)
        assert any("uninstall" in n for n in step_names)
