"""Unit tests for the remote test runner (asyncssh mocked)."""

from __future__ import annotations

import asyncio
from dataclasses import dataclass
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from tests.remote.runner import (
    BackgroundProcess,
    CommandResult,
    DeviceInfo,
    RemoteTestRunner,
    SSHConnectionError,
)

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def bash_device() -> DeviceInfo:
    return DeviceInfo(
        hostname="test-server.local",
        username="testuser",
        shell="bash",
        os="linux",
        ssh_key="~/.ssh/test_key",
    )


@pytest.fixture()
def ps_device() -> DeviceInfo:
    return DeviceInfo(
        hostname="alienware.local",
        username="testuser",
        shell="powershell",
        os="windows",
        ssh_key="~/.ssh/alienware_key",
    )


@dataclass
class FakeSSHResult:
    """Minimal mock for asyncssh.SSHCompletedProcess."""

    exit_status: int
    stdout: str
    stderr: str


def _make_mock_conn(
    run_results: list[FakeSSHResult] | None = None,
    default_result: FakeSSHResult | None = None,
) -> MagicMock:
    """Create a mock SSH connection."""
    conn = MagicMock()
    conn.close = MagicMock()

    if run_results is not None:
        results_iter = iter(run_results)

        async def run_side_effect(*args: Any, **kwargs: Any) -> FakeSSHResult:
            return next(results_iter)

        conn.run = AsyncMock(side_effect=run_side_effect)
    elif default_result is not None:
        conn.run = AsyncMock(return_value=default_result)
    else:
        conn.run = AsyncMock(return_value=FakeSSHResult(0, "ok\n", ""))

    return conn


# ---------------------------------------------------------------------------
# CommandResult tests
# ---------------------------------------------------------------------------


class TestCommandResult:
    def test_success_when_exit_zero(self) -> None:
        r = CommandResult(command="echo hi", exit_code=0, stdout="hi", stderr="", duration_s=0.1)
        assert r.success is True

    def test_failure_when_exit_nonzero(self) -> None:
        r = CommandResult(command="bad", exit_code=1, stdout="", stderr="err", duration_s=0.1)
        assert r.success is False

    def test_failure_when_timed_out(self) -> None:
        r = CommandResult(command="sleep", exit_code=0, stdout="", stderr="", duration_s=60.0, timed_out=True)
        assert r.success is False


# ---------------------------------------------------------------------------
# Shell wrapping tests
# ---------------------------------------------------------------------------


class TestShellWrapping:
    def test_bash_command_passthrough(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        assert runner._wrap_command("ls -la") == "ls -la"

    def test_powershell_wraps_command(self, ps_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(ps_device)
        wrapped = runner._wrap_command("Get-Process")
        assert "powershell.exe" in wrapped
        assert "-NoProfile" in wrapped
        assert "Get-Process" in wrapped

    def test_powershell_escapes_double_quotes(self, ps_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(ps_device)
        wrapped = runner._wrap_command('echo "hello"')
        # Double quotes should be escaped with backtick for PowerShell
        assert '`"hello`"' in wrapped

    def test_bash_background_uses_nohup(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        wrapped = runner._wrap_background_command("llmhosts serve")
        assert "nohup" in wrapped
        assert "echo $!" in wrapped

    def test_powershell_background_uses_start_process(self, ps_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(ps_device)
        wrapped = runner._wrap_background_command("llmhosts serve")
        assert "Start-Process" in wrapped
        assert "$p.Id" in wrapped

    def test_bash_kill_command(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        cmd = runner._kill_command(12345)
        assert "kill 12345" in cmd

    def test_powershell_kill_command(self, ps_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(ps_device)
        cmd = runner._kill_command(12345)
        assert "Stop-Process" in cmd
        assert "12345" in cmd


# ---------------------------------------------------------------------------
# Connection tests
# ---------------------------------------------------------------------------


class TestConnection:
    async def test_not_connected_raises(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        with pytest.raises(SSHConnectionError, match="Not connected"):
            await runner.run_command("echo test")

    async def test_connect_success(self, bash_device: DeviceInfo) -> None:
        mock_conn = _make_mock_conn()

        with patch("asyncssh.connect", new_callable=AsyncMock, return_value=mock_conn):
            runner = RemoteTestRunner(bash_device)
            await runner.connect()
            assert runner._conn is not None

            await runner.close()
            assert runner._conn is None

    async def test_close_stops_background_processes(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        mock_conn = _make_mock_conn(default_result=FakeSSHResult(0, "", ""))
        runner._conn = mock_conn

        proc = BackgroundProcess(command="test", pid=999)
        runner._background_procs.append(proc)

        await runner.close()
        assert len(runner._background_procs) == 0
        assert runner._conn is None


# ---------------------------------------------------------------------------
# Command execution tests
# ---------------------------------------------------------------------------


class TestRunCommand:
    async def test_run_command_success(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        mock_conn = _make_mock_conn(default_result=FakeSSHResult(0, "hello world\n", ""))
        runner._conn = mock_conn

        result = await runner.run_command("echo hello world")
        assert result.success is True
        assert result.stdout == "hello world"
        assert result.exit_code == 0
        assert result.duration_s >= 0

    async def test_run_command_failure(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        mock_conn = _make_mock_conn(default_result=FakeSSHResult(1, "", "command not found\n"))
        runner._conn = mock_conn

        result = await runner.run_command("nonexistent")
        assert result.success is False
        assert result.exit_code == 1
        assert "command not found" in result.stderr

    async def test_run_command_timeout(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        mock_conn = MagicMock()

        async def slow_run(*args: Any, **kwargs: Any) -> None:
            await asyncio.sleep(10)

        mock_conn.run = slow_run
        runner._conn = mock_conn

        result = await runner.run_command("sleep 100", timeout=0.1)
        assert result.timed_out is True
        assert result.success is False
        assert result.exit_code == -1


# ---------------------------------------------------------------------------
# Background process tests
# ---------------------------------------------------------------------------


class TestBackgroundProcess:
    async def test_run_background_parses_pid(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        mock_conn = _make_mock_conn(default_result=FakeSSHResult(0, "12345\n", ""))
        runner._conn = mock_conn

        proc = await runner.run_background("llmhosts serve")
        assert proc.pid == 12345
        assert proc.command == "llmhosts serve"
        assert proc in runner._background_procs

    async def test_run_background_no_pid(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        mock_conn = _make_mock_conn(default_result=FakeSSHResult(0, "", ""))
        runner._conn = mock_conn

        proc = await runner.run_background("llmhosts serve")
        assert proc.pid is None

    async def test_stop_background_kills_process(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        mock_conn = _make_mock_conn(default_result=FakeSSHResult(0, "", ""))
        runner._conn = mock_conn

        proc = BackgroundProcess(command="test", pid=12345)
        runner._background_procs.append(proc)

        result = await runner.stop_background(proc)
        assert result.exit_code == 0
        assert proc not in runner._background_procs

    async def test_stop_background_no_pid(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        mock_conn = _make_mock_conn()
        runner._conn = mock_conn

        proc = BackgroundProcess(command="test", pid=None)
        result = await runner.stop_background(proc)
        assert result.exit_code == -1
        assert "No PID" in result.stderr


# ---------------------------------------------------------------------------
# is_alive tests
# ---------------------------------------------------------------------------


class TestIsAlive:
    async def test_alive_when_echo_works(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        mock_conn = _make_mock_conn(default_result=FakeSSHResult(0, "alive\n", ""))
        runner._conn = mock_conn

        assert await runner.is_alive() is True

    async def test_not_alive_when_not_connected(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        assert await runner.is_alive() is False

    async def test_not_alive_when_command_fails(self, bash_device: DeviceInfo) -> None:
        runner = RemoteTestRunner(bash_device)
        mock_conn = _make_mock_conn(default_result=FakeSSHResult(1, "", ""))
        runner._conn = mock_conn

        assert await runner.is_alive() is False
