"""Tests for specter_cli.ssh_config — SSH argument construction and exec helpers.

Tests ssh_base_args, rsync_ssh_args, check_connection, stop_connection,
and exec_ssh_passthrough.
All subprocess calls are mocked — no real SSH connections.
"""

from __future__ import annotations

import signal
import subprocess
from unittest.mock import MagicMock, patch

# --- TestSshBaseArgs ---


class TestSshBaseArgs:
    """Test ssh_base_args() argument construction."""

    def test_default_port(self, tmp_path, monkeypatch):
        """Default port (22) should NOT add -p flag."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import ssh_base_args

        args = ssh_base_args("10.0.0.1", "root")
        assert "-p" not in args
        assert "root@10.0.0.1" in args

    def test_custom_port(self, tmp_path, monkeypatch):
        """Custom port should add -p flag."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import ssh_base_args

        args = ssh_base_args("10.0.0.1", "root", port=10473)
        assert "-p" in args
        idx = args.index("-p")
        assert args[idx + 1] == "10473"

    def test_key_path(self, tmp_path, monkeypatch):
        """Key path should add -i flag."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import ssh_base_args

        args = ssh_base_args("10.0.0.1", "root", key_path="/home/user/.ssh/id_ed25519")
        assert "-i" in args
        idx = args.index("-i")
        assert args[idx + 1] == "/home/user/.ssh/id_ed25519"

    def test_tty_flag(self, tmp_path, monkeypatch):
        """tty=True should add -t flag."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import ssh_base_args

        args_no_tty = ssh_base_args("10.0.0.1", "root", tty=False)
        assert "-t" not in args_no_tty

        args_tty = ssh_base_args("10.0.0.1", "root", tty=True)
        assert "-t" in args_tty

    def test_control_master_enabled(self, tmp_path, monkeypatch):
        """ControlMaster should be in the args for connection reuse."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import ssh_base_args

        args = ssh_base_args("10.0.0.1", "root")
        args_str = " ".join(args)
        assert "ControlMaster=auto" in args_str
        assert "ControlPersist=600" in args_str

    def test_strict_host_key_checking_disabled(self, tmp_path, monkeypatch):
        """StrictHostKeyChecking should be disabled (cloud pods change IPs)."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import ssh_base_args

        args = ssh_base_args("10.0.0.1", "root")
        args_str = " ".join(args)
        assert "StrictHostKeyChecking=no" in args_str

    def test_user_at_host_format(self, tmp_path, monkeypatch):
        """Last arg should be user@host."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import ssh_base_args

        args = ssh_base_args("gpu.vastai.example", "ubuntu")
        assert args[-1] == "ubuntu@gpu.vastai.example"


# --- TestRsyncSshArgs ---


class TestRsyncSshArgs:
    """Test rsync_ssh_args() for rsync -e construction."""

    def test_returns_string(self, tmp_path, monkeypatch):
        """rsync_ssh_args should return a string (for rsync -e)."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import rsync_ssh_args

        result = rsync_ssh_args()
        assert isinstance(result, str)
        assert result.startswith("ssh ")

    def test_includes_control_master(self, tmp_path, monkeypatch):
        """rsync SSH args should include ControlMaster for connection reuse."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import rsync_ssh_args

        result = rsync_ssh_args()
        assert "ControlMaster=auto" in result

    def test_custom_port(self, tmp_path, monkeypatch):
        """Custom port should be included."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import rsync_ssh_args

        result = rsync_ssh_args(port=10473)
        assert "-p" in result
        assert "10473" in result

    def test_key_path(self, tmp_path, monkeypatch):
        """Key path should be included."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import rsync_ssh_args

        result = rsync_ssh_args(key_path="/home/user/.ssh/id_rsa")
        assert "-i" in result
        assert "/home/user/.ssh/id_rsa" in result


# --- TestCheckConnection ---


class TestCheckConnection:
    """Test check_connection() SSH connectivity test."""

    @patch("specter_cli.ssh_config.subprocess.run")
    def test_success(self, mock_run, tmp_path, monkeypatch):
        """Returns True when SSH succeeds."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import check_connection

        mock_run.return_value = MagicMock(returncode=0)
        assert check_connection("10.0.0.1", "root") is True

    @patch("specter_cli.ssh_config.subprocess.run")
    def test_failure(self, mock_run, tmp_path, monkeypatch):
        """Returns False when SSH fails."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import check_connection

        mock_run.return_value = MagicMock(returncode=255)
        assert check_connection("10.0.0.1", "root") is False

    @patch("specter_cli.ssh_config.subprocess.run")
    def test_timeout(self, mock_run, tmp_path, monkeypatch):
        """Returns False on timeout."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import check_connection

        mock_run.side_effect = subprocess.TimeoutExpired(cmd="ssh", timeout=10)
        assert check_connection("10.0.0.1", "root") is False

    @patch("specter_cli.ssh_config.subprocess.run")
    def test_os_error(self, mock_run, tmp_path, monkeypatch):
        """Returns False on OS error (e.g., ssh not found)."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import check_connection

        mock_run.side_effect = OSError("ssh not found")
        assert check_connection("10.0.0.1", "root") is False

    @patch("specter_cli.ssh_config.subprocess.run")
    def test_echo_ok_command(self, mock_run, tmp_path, monkeypatch):
        """Should send 'echo ok' as the test command."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import check_connection

        mock_run.return_value = MagicMock(returncode=0)
        check_connection("10.0.0.1", "root")

        cmd = mock_run.call_args[0][0]
        assert cmd[-1] == "echo ok"


# --- TestStopConnection ---


class TestStopConnection:
    """Test stop_connection() ControlMaster teardown."""

    @patch("specter_cli.ssh_config.subprocess.run")
    def test_sends_exit_command(self, mock_run, tmp_path, monkeypatch):
        """Should send SSH ControlMaster exit command."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import stop_connection

        mock_run.return_value = MagicMock(returncode=0)
        stop_connection("10.0.0.1", "root")

        cmd = mock_run.call_args[0][0]
        assert "-O" in cmd
        assert "exit" in cmd

    @patch("specter_cli.ssh_config.subprocess.run")
    def test_timeout_silenced(self, mock_run, tmp_path, monkeypatch):
        """Timeout during stop should not raise."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import stop_connection

        mock_run.side_effect = subprocess.TimeoutExpired(cmd="ssh", timeout=5)
        # Should not raise.
        stop_connection("10.0.0.1", "root")

    @patch("specter_cli.ssh_config.subprocess.run")
    def test_os_error_silenced(self, mock_run, tmp_path, monkeypatch):
        """OS error during stop should not raise."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.ssh_config import stop_connection

        mock_run.side_effect = OSError("socket not found")
        # Should not raise.
        stop_connection("10.0.0.1", "root")


# --- TestExecSshPassthrough ---

_POPEN = "specter_cli.ssh_config.subprocess.Popen"


class TestExecSshPassthrough:
    """Test exec_ssh_passthrough() — Popen with I/O passthrough and signal forwarding."""

    @patch(_POPEN)
    def test_popen_passthrough_io(self, mock_popen: MagicMock) -> None:
        """Popen should use stdin/stdout/stderr passthrough, start_new_session=True."""
        import sys

        from specter_cli.ssh_config import exec_ssh_passthrough

        proc = MagicMock()
        proc.returncode = 0
        proc.wait.return_value = 0
        mock_popen.return_value = proc

        exec_ssh_passthrough(["ssh", "root@host", "nvidia-smi"])

        mock_popen.assert_called_once()
        kwargs = mock_popen.call_args[1]
        assert kwargs["stdin"] is sys.stdin
        assert kwargs["stdout"] is sys.stdout
        assert kwargs["stderr"] is sys.stderr
        assert kwargs["start_new_session"] is True

    @patch(_POPEN)
    def test_passes_command_to_popen(self, mock_popen: MagicMock) -> None:
        """The full command list should be passed as the first positional arg."""
        from specter_cli.ssh_config import exec_ssh_passthrough

        proc = MagicMock()
        proc.returncode = 0
        proc.wait.return_value = 0
        mock_popen.return_value = proc

        cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "root@1.2.3.4", "nvidia-smi"]
        exec_ssh_passthrough(cmd)

        assert mock_popen.call_args[0][0] == cmd

    @patch(_POPEN)
    def test_returns_exit_code(self, mock_popen: MagicMock) -> None:
        """Should return the process exit code."""
        from specter_cli.ssh_config import exec_ssh_passthrough

        proc = MagicMock()
        proc.returncode = 42
        proc.wait.return_value = 42
        mock_popen.return_value = proc

        assert exec_ssh_passthrough(["ssh", "root@host", "cmd"]) == 42

    @patch(_POPEN)
    def test_returncode_none_returns_1(self, mock_popen: MagicMock) -> None:
        """If proc.returncode is None (killed), should return 1."""
        from specter_cli.ssh_config import exec_ssh_passthrough

        proc = MagicMock()
        proc.returncode = None
        proc.wait.return_value = None
        mock_popen.return_value = proc

        assert exec_ssh_passthrough(["ssh", "root@host", "cmd"]) == 1

    @patch(_POPEN)
    def test_returns_zero_on_success(self, mock_popen: MagicMock) -> None:
        """Exit code 0 should be returned as-is."""
        from specter_cli.ssh_config import exec_ssh_passthrough

        proc = MagicMock()
        proc.returncode = 0
        proc.wait.return_value = 0
        mock_popen.return_value = proc

        assert exec_ssh_passthrough(["ssh", "root@host", "cmd"]) == 0

    @patch(_POPEN)
    def test_signal_handlers_restored(self, mock_popen: MagicMock) -> None:
        """SIGINT and SIGTERM handlers should be restored after wait()."""
        from specter_cli.ssh_config import exec_ssh_passthrough

        proc = MagicMock()
        proc.returncode = 0
        proc.wait.return_value = 0
        mock_popen.return_value = proc

        original_int = signal.getsignal(signal.SIGINT)
        original_term = signal.getsignal(signal.SIGTERM)

        exec_ssh_passthrough(["ssh", "root@host", "cmd"])

        assert signal.getsignal(signal.SIGINT) == original_int
        assert signal.getsignal(signal.SIGTERM) == original_term

    @patch(_POPEN)
    def test_calls_proc_wait(self, mock_popen: MagicMock) -> None:
        """Should call proc.wait() to block until process exits."""
        from specter_cli.ssh_config import exec_ssh_passthrough

        proc = MagicMock()
        proc.returncode = 0
        proc.wait.return_value = 0
        mock_popen.return_value = proc

        exec_ssh_passthrough(["ssh", "root@host", "cmd"])

        proc.wait.assert_called_once()
