"""Integration tests for specter_cli.proxy — run_remote, _ensure_remote_dir, main.

These test the proxy's full flow with mocked subprocess calls (no real SSH).
Covers the untested functions identified in QA review:
- _ensure_remote_dir: SSH mkdir construction
- run_remote: full proxy flow (config → sync → exec)
- main(): CLI entry point argv handling
"""

from __future__ import annotations

import subprocess
from unittest.mock import MagicMock, patch

import pytest

from specter_cli.proxy import _ensure_remote_dir, main, run_remote

# --- Fixtures ---


@pytest.fixture
def remote_config(tmp_path, monkeypatch):
    """Set up a valid remote config for testing.

    The proxy runs from the venv wrapper where VIRTUAL_ENV is always set,
    so we simulate that by writing config to a fake venv.
    """
    monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
    fake_venv = tmp_path / "fakevenv"
    fake_venv.mkdir()
    monkeypatch.setenv("VIRTUAL_ENV", str(fake_venv))
    config = {
        "host": "10.0.0.1",
        "user": "root",
        "ssh_port": 10473,
        "session_id": "sess_abc123",
        "gpu_type": "h100",
        "workspace": "~/.specter/workspace",
    }

    from specter_cli.config import save_remote_config

    save_remote_config(
        host=config["host"],
        user=config["user"],
        ssh_port=config["ssh_port"],
        session_id=config["session_id"],
        gpu_type=config["gpu_type"],
        venv_path=str(fake_venv),
    )
    return config


@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')")
    (project / "src").mkdir()
    (project / "src" / "model.py").write_text("# model")
    return project


# --- TestEnsureRemoteDir ---


class TestEnsureRemoteDir:
    """Test _ensure_remote_dir SSH mkdir construction."""

    @patch("specter_cli.proxy.subprocess.run")
    @patch("specter_cli.proxy.ssh_base_args")
    def test_basic_mkdir(self, mock_ssh_args, mock_run):
        """Should construct an SSH mkdir -p command."""
        mock_ssh_args.return_value = [
            "-o",
            "ControlMaster=auto",
            "root@10.0.0.1",
        ]
        mock_run.return_value = MagicMock(returncode=0)

        _ensure_remote_dir("10.0.0.1", "root", 22, None, "~/.specter/workspace")

        mock_run.assert_called_once()
        cmd = mock_run.call_args[0][0]
        assert cmd[0] == "ssh"
        assert cmd[-1] == "mkdir -p ~/.specter/workspace"

    @patch("specter_cli.proxy.subprocess.run")
    @patch("specter_cli.proxy.ssh_base_args")
    def test_custom_port_and_key(self, mock_ssh_args, mock_run):
        """Port and key should be forwarded to ssh_base_args."""
        mock_ssh_args.return_value = ["-p", "10473", "root@10.0.0.1"]
        mock_run.return_value = MagicMock(returncode=0)

        _ensure_remote_dir("10.0.0.1", "root", 10473, "/home/user/.ssh/id_ed25519", "/opt/work")

        mock_ssh_args.assert_called_once_with(
            "10.0.0.1", "root", port=10473, key_path="/home/user/.ssh/id_ed25519"
        )

    @patch("specter_cli.proxy.subprocess.run")
    @patch("specter_cli.proxy.ssh_base_args")
    def test_timeout_30_seconds(self, mock_ssh_args, mock_run):
        """subprocess.run should have timeout=30."""
        mock_ssh_args.return_value = ["root@10.0.0.1"]
        mock_run.return_value = MagicMock(returncode=0)

        _ensure_remote_dir("10.0.0.1", "root", 22, None, "~/workspace")

        _, kwargs = mock_run.call_args
        assert kwargs["timeout"] == 30

    @patch("specter_cli.proxy.subprocess.run")
    @patch("specter_cli.proxy.ssh_base_args")
    def test_tilde_not_quoted(self, mock_ssh_args, mock_run):
        """Remote path with ~ should NOT be quoted (let remote shell expand)."""
        mock_ssh_args.return_value = ["root@10.0.0.1"]
        mock_run.return_value = MagicMock(returncode=0)

        _ensure_remote_dir("10.0.0.1", "root", 22, None, "~/.specter/workspace")

        cmd = mock_run.call_args[0][0]
        # The mkdir command should use the raw path, not shlex.quoted.
        assert "mkdir -p ~/.specter/workspace" in cmd[-1]
        assert "'" not in cmd[-1]  # Not quoted


# --- TestRunRemote ---


class TestRunRemote:
    """Test run_remote() full proxy flow."""

    def test_no_config_returns_1(self, tmp_path, monkeypatch):
        """When no remote config exists, run_remote should return 1."""
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter-empty"))
        result = run_remote(["python", "train.py"])
        assert result == 1

    @patch("specter_cli.proxy.workspace_hash", return_value=None)
    @patch("specter_cli.ssh_config.subprocess.Popen")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    def test_happy_path(
        self,
        mock_find_root,
        mock_ensure_dir,
        mock_rsync,
        mock_popen,
        _ws_hash,
        remote_config,
        project_dir,
    ):
        """Full flow: config → sync → exec should return remote exit code."""
        mock_find_root.return_value = project_dir
        mock_popen_instance = MagicMock()
        mock_popen_instance.returncode = 0
        mock_popen_instance.wait.return_value = 0
        mock_popen.return_value = mock_popen_instance

        with patch("specter_cli.proxy.Path.cwd", return_value=project_dir):
            result = run_remote(["python", "train.py"])

        assert result == 0
        mock_ensure_dir.assert_called_once()
        mock_rsync.assert_called_once()
        mock_popen.assert_called_once()

    @patch("specter_cli.ssh_config.subprocess.Popen")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    def test_replaces_python_with_python3(
        self, mock_find_root, mock_ensure_dir, mock_rsync, mock_popen, remote_config, project_dir
    ):
        """argv[0]='python' should become 'python3' in the remote command."""
        mock_find_root.return_value = project_dir
        mock_popen_instance = MagicMock()
        mock_popen_instance.returncode = 0
        mock_popen_instance.wait.return_value = 0
        mock_popen.return_value = mock_popen_instance

        with patch("specter_cli.proxy.Path.cwd", return_value=project_dir):
            run_remote(["python", "train.py"])

        # The SSH command should contain "python3" not "python".
        ssh_cmd = mock_popen.call_args[0][0]
        remote_cmd = ssh_cmd[-1]  # Last arg is the remote command string.
        assert "python3" in remote_cmd

    @patch("specter_cli.ssh_config.subprocess.Popen")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    def test_nonzero_exit_code(
        self, mock_find_root, mock_ensure_dir, mock_rsync, mock_popen, remote_config, project_dir
    ):
        """Remote process exit code should be forwarded."""
        mock_find_root.return_value = project_dir
        mock_popen_instance = MagicMock()
        mock_popen_instance.returncode = 42
        mock_popen_instance.wait.return_value = 42
        mock_popen.return_value = mock_popen_instance

        with patch("specter_cli.proxy.Path.cwd", return_value=project_dir):
            result = run_remote(["python", "train.py"])

        assert result == 42

    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    def test_rsync_failure_returns_1(
        self, mock_find_root, mock_ensure_dir, mock_rsync, remote_config, project_dir
    ):
        """If rsync fails, run_remote should return 1 without SSH exec."""
        mock_find_root.return_value = project_dir
        mock_rsync.side_effect = subprocess.CalledProcessError(
            1, "rsync", stderr="connection refused"
        )

        with patch("specter_cli.proxy.Path.cwd", return_value=project_dir):
            result = run_remote(["python", "train.py"])

        assert result == 1

    @patch("specter_cli.ssh_config.subprocess.Popen")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    def test_env_forwarding(
        self,
        mock_find_root,
        mock_ensure_dir,
        mock_rsync,
        mock_popen,
        remote_config,
        project_dir,
        monkeypatch,
    ):
        """Environment variables should be forwarded in the remote command."""
        monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "0,1")
        mock_find_root.return_value = project_dir
        mock_popen_instance = MagicMock()
        mock_popen_instance.returncode = 0
        mock_popen_instance.wait.return_value = 0
        mock_popen.return_value = mock_popen_instance

        with patch("specter_cli.proxy.Path.cwd", return_value=project_dir):
            run_remote(["python", "train.py"])

        ssh_cmd = mock_popen.call_args[0][0]
        remote_cmd = ssh_cmd[-1]
        assert "CUDA_VISIBLE_DEVICES" in remote_cmd
        assert "0,1" in remote_cmd

    @patch("specter_cli.ssh_config.subprocess.Popen")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    def test_uses_ssh_port_from_config(
        self, mock_find_root, mock_ensure_dir, mock_rsync, mock_popen, remote_config, project_dir
    ):
        """SSH port from remote config should be used."""
        mock_find_root.return_value = project_dir
        mock_popen_instance = MagicMock()
        mock_popen_instance.returncode = 0
        mock_popen_instance.wait.return_value = 0
        mock_popen.return_value = mock_popen_instance

        with patch("specter_cli.proxy.Path.cwd", return_value=project_dir):
            run_remote(["python", "train.py"])

        # SSH should use port 10473 from the config.
        ssh_cmd = mock_popen.call_args[0][0]
        # Port appears in the SSH args.
        assert "10473" in " ".join(str(a) for a in ssh_cmd)

    @patch("specter_cli.ssh_config.subprocess.Popen")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    def test_remote_command_activates_venv(
        self, mock_find_root, mock_ensure_dir, mock_rsync, mock_popen, remote_config, project_dir
    ):
        """Remote command should include venv activation before user command."""
        mock_find_root.return_value = project_dir
        mock_popen_instance = MagicMock()
        mock_popen_instance.returncode = 0
        mock_popen_instance.wait.return_value = 0
        mock_popen.return_value = mock_popen_instance

        with patch("specter_cli.proxy.Path.cwd", return_value=project_dir):
            run_remote(["python", "train.py"])

        ssh_cmd = mock_popen.call_args[0][0]
        remote_cmd = ssh_cmd[-1]
        # Venv activation should appear before the user command.
        assert "source ~/.specter/venv/bin/activate" in remote_cmd
        activate_pos = remote_cmd.index("source")
        python_pos = remote_cmd.index("python3")
        assert activate_pos < python_pos

    @patch("specter_cli.ssh_config.subprocess.Popen")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    def test_popen_returncode_none_returns_1(
        self, mock_find_root, mock_ensure_dir, mock_rsync, mock_popen, remote_config, project_dir
    ):
        """If proc.returncode is None (killed), run_remote should return 1."""
        mock_find_root.return_value = project_dir
        mock_popen_instance = MagicMock()
        mock_popen_instance.returncode = None
        mock_popen_instance.wait.return_value = None
        mock_popen.return_value = mock_popen_instance

        with patch("specter_cli.proxy.Path.cwd", return_value=project_dir):
            result = run_remote(["python", "train.py"])

        assert result == 1


# --- TestMain ---


class TestMain:
    """Test proxy main() entry point argv handling."""

    @patch("specter_cli.proxy.run_remote")
    @patch("specter_cli.proxy.sys")
    def test_no_args_sends_python3(self, mock_sys, mock_run_remote):
        """No args → interactive REPL → argv=['python3']."""
        mock_sys.argv = ["specter-proxy"]
        mock_run_remote.return_value = 0

        # main() calls sys.exit, so we need to catch it.
        mock_sys.exit = MagicMock()

        main()

        mock_run_remote.assert_called_once_with(["python3"])
        mock_sys.exit.assert_called_once_with(0)

    @patch("specter_cli.proxy.run_remote")
    @patch("specter_cli.proxy.sys")
    def test_script_arg_prepends_python3(self, mock_sys, mock_run_remote):
        """'train.py' → ['python3', 'train.py']."""
        mock_sys.argv = ["specter-proxy", "train.py"]
        mock_run_remote.return_value = 0
        mock_sys.exit = MagicMock()

        main()

        mock_run_remote.assert_called_once_with(["python3", "train.py"])

    @patch("specter_cli.proxy.run_remote")
    @patch("specter_cli.proxy.sys")
    def test_flag_arg_prepends_python3(self, mock_sys, mock_run_remote):
        """'-c print(1)' → ['python3', '-c', 'print(1)']."""
        mock_sys.argv = ["specter-proxy", "-c", "print(1)"]
        mock_run_remote.return_value = 0
        mock_sys.exit = MagicMock()

        main()

        mock_run_remote.assert_called_once_with(["python3", "-c", "print(1)"])

    @patch("specter_cli.proxy.run_remote")
    @patch("specter_cli.proxy.sys")
    def test_python_stays_as_first_arg(self, mock_sys, mock_run_remote):
        """'python train.py' → ['python', 'train.py'] (no double-prepend)."""
        mock_sys.argv = ["specter-proxy", "python", "train.py"]
        mock_run_remote.return_value = 0
        mock_sys.exit = MagicMock()

        main()

        mock_run_remote.assert_called_once_with(["python", "train.py"])

    @patch("specter_cli.proxy.run_remote")
    @patch("specter_cli.proxy.sys")
    def test_forwards_exit_code(self, mock_sys, mock_run_remote):
        """Exit code from run_remote should be forwarded to sys.exit."""
        mock_sys.argv = ["specter-proxy", "train.py"]
        mock_run_remote.return_value = 137
        mock_sys.exit = MagicMock()

        main()

        mock_sys.exit.assert_called_once_with(137)
