"""Tests for proxy SSH exit code handling, especially exit 255.

SSH exit code 255 = connection lost (network error, pod terminated, preemption).
The proxy prints a specific error message for this case. This path will be
enhanced later to distinguish preemption from pod crash from network blip.

These tests serve as both coverage and regression baseline for that future work.
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from specter_cli.proxy import run_remote


@pytest.fixture
def remote_config(tmp_path, monkeypatch):
    """Set up a valid remote config for testing."""
    monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
    fake_venv = tmp_path / "fakevenv"
    fake_venv.mkdir()
    monkeypatch.setenv("VIRTUAL_ENV", str(fake_venv))

    from specter_cli.config import save_remote_config

    save_remote_config(
        host="10.0.0.1",
        user="root",
        ssh_port=10473,
        session_id="sess_abc123",
        gpu_type="h100",
        venv_path=str(fake_venv),
    )


@pytest.fixture
def project_dir(tmp_path):
    """Create a fake project directory with a git repo."""
    project = tmp_path / "myproject"
    project.mkdir()
    (project / ".git").mkdir()
    (project / "train.py").write_text("print('hello')")
    return project


def _run_remote_with_exit_code(
    exit_code: int | None,
    remote_config,
    project_dir,
    capsys=None,
):
    """Helper: run run_remote with a mocked SSH process that returns the given exit code."""
    with (
        patch("specter_cli.proxy.workspace_hash", return_value=None),
        patch("specter_cli.proxy.find_project_root", return_value=project_dir),
        patch("specter_cli.proxy._ensure_remote_dir"),
        patch("specter_cli.proxy.rsync_workspace"),
        patch("specter_cli.ssh_config.subprocess.Popen") as mock_popen,
        patch("specter_cli.proxy.Path.cwd", return_value=project_dir),
    ):
        mock_proc = MagicMock()
        mock_proc.returncode = exit_code
        mock_proc.wait.return_value = exit_code
        mock_popen.return_value = mock_proc

        return run_remote(["python", "train.py"])


class TestSSHExitCode255:
    """SSH exit 255 = connection lost. Should print specific error message."""

    def test_returns_255(self, remote_config, project_dir):
        """run_remote should return 255 when SSH exits with 255."""
        result = _run_remote_with_exit_code(255, remote_config, project_dir)
        assert result == 255

    def test_prints_connection_lost_message(self, remote_config, project_dir, capsys):
        """Should print 'connection to GPU lost' on stderr for exit 255."""
        _run_remote_with_exit_code(255, remote_config, project_dir)
        captured = capsys.readouterr()
        assert "connection to GPU lost" in captured.err
        assert "SSH exit 255" in captured.err

    def test_suggests_specter_status(self, remote_config, project_dir, capsys):
        """Error message should suggest 'specter status' for debugging."""
        _run_remote_with_exit_code(255, remote_config, project_dir)
        captured = capsys.readouterr()
        assert "specter status" in captured.err

    def test_no_message_for_exit_0(self, remote_config, project_dir, capsys):
        """Exit 0 should not print any connection lost message."""
        _run_remote_with_exit_code(0, remote_config, project_dir)
        captured = capsys.readouterr()
        assert "connection to GPU lost" not in captured.err

    def test_no_message_for_exit_1(self, remote_config, project_dir, capsys):
        """Exit 1 (generic error) should not print connection lost message."""
        _run_remote_with_exit_code(1, remote_config, project_dir)
        captured = capsys.readouterr()
        assert "connection to GPU lost" not in captured.err

    def test_no_message_for_exit_137(self, remote_config, project_dir, capsys):
        """Exit 137 (killed) should not print connection lost message."""
        _run_remote_with_exit_code(137, remote_config, project_dir)
        captured = capsys.readouterr()
        assert "connection to GPU lost" not in captured.err


class TestExitCodePassthrough:
    """Proxy should faithfully forward exit codes from SSH."""

    @pytest.mark.parametrize(
        "exit_code,description",
        [
            (0, "success"),
            (1, "generic error"),
            (2, "misuse of shell"),
            (126, "command not executable"),
            (127, "command not found"),
            (130, "SIGINT (Ctrl-C)"),
            (137, "SIGKILL (OOM kill)"),
            (143, "SIGTERM"),
            (255, "SSH connection lost"),
        ],
    )
    def test_exit_code_forwarded(self, exit_code, description, remote_config, project_dir):
        """Exit code {exit_code} ({description}) should be forwarded."""
        result = _run_remote_with_exit_code(exit_code, remote_config, project_dir)
        assert result == exit_code

    def test_none_returncode_becomes_1(self, remote_config, project_dir):
        """If proc.returncode is None (killed before wait), return 1."""
        result = _run_remote_with_exit_code(None, remote_config, project_dir)
        assert result == 1
