"""Tests for remote venv creation and dep sync into remote venv.

Covers the remote venv feature where:
1. `create_remote_venv()` creates a venv on the remote pod with
   `--system-site-packages` so torch/CUDA are inherited from the Docker image.
2. `sync_deps()` installs into the remote venv's pip (no --break-system-packages).
3. `proxy.py` activates the remote venv before running user commands.

These tests are designed to run against the remote venv implementation.
They will fail until the feature is implemented — that's intentional.
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

_SUBPROCESS_RUN = "specter_cli.deps.subprocess.run"
_RSYNC_SSH = "specter_cli.deps.rsync_ssh_args"
_SSH_BASE = "specter_cli.deps.ssh_base_args"

_REMOTE_VENV_PATH = "~/.specter/venv"


@pytest.fixture
def fake_venv(tmp_path: Path) -> Path:
    """Create a minimal fake venv with python and python.specter-original."""
    bin_dir = tmp_path / "bin"
    bin_dir.mkdir()
    original = bin_dir / "python.specter-original"
    original.write_text("#!/bin/bash\n")
    original.chmod(0o755)
    python = bin_dir / "python"
    python.write_text("#!/bin/bash\n# wrapper\n")
    python.chmod(0o755)
    return tmp_path


class TestCreateRemoteVenv:
    """Tests for create_remote_venv() function."""

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_creates_venv_with_system_site_packages(
        self, mock_run: MagicMock, _ssh_base: MagicMock
    ) -> None:
        """Venv should be created with --system-site-packages to inherit torch/CUDA."""
        from specter_cli.deps import create_remote_venv

        mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")

        create_remote_venv(host="1.2.3.4", user="root")

        mock_run.assert_called_once()
        cmd = mock_run.call_args[0][0]
        remote_cmd = " ".join(cmd)
        assert "--system-site-packages" in remote_cmd
        assert ".specter/venv" in remote_cmd

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_uses_python3_not_versioned(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """For now, uses `python3` (system Python), not a versioned binary."""
        from specter_cli.deps import create_remote_venv

        mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")

        create_remote_venv(host="1.2.3.4", user="root")

        cmd = mock_run.call_args[0][0]
        remote_cmd = " ".join(cmd)
        assert "python3 -m venv" in remote_cmd or "python3" in remote_cmd

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_passes_port_and_key(self, mock_run: MagicMock, mock_ssh_base: MagicMock) -> None:
        """Port and key_path should be forwarded to ssh_base_args."""
        from specter_cli.deps import create_remote_venv

        mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")

        create_remote_venv(host="5.6.7.8", user="ubuntu", port=2222, key_path="/tmp/key")

        mock_ssh_base.assert_called_once_with("5.6.7.8", "ubuntu", port=2222, key_path="/tmp/key")

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_raises_on_ssh_failure(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """SSH failure during venv creation should raise (hard failure)."""
        import subprocess

        from specter_cli.deps import create_remote_venv

        mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="Connection refused")

        with pytest.raises(subprocess.CalledProcessError):
            create_remote_venv(host="1.2.3.4", user="root")

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_raises_on_timeout(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """SSH timeout during venv creation should propagate."""
        import subprocess

        from specter_cli.deps import create_remote_venv

        mock_run.side_effect = subprocess.TimeoutExpired(cmd="ssh", timeout=30)

        with pytest.raises(subprocess.TimeoutExpired):
            create_remote_venv(host="1.2.3.4", user="root")

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_idempotent_recreate(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """Creating venv when one already exists should succeed (idempotent)."""
        from specter_cli.deps import create_remote_venv

        mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")

        # Call twice — both should succeed.
        create_remote_venv(host="1.2.3.4", user="root")
        create_remote_venv(host="1.2.3.4", user="root")

        assert mock_run.call_count == 2

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_returns_venv_path(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """Should return the remote venv path."""
        from specter_cli.deps import create_remote_venv

        mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")

        result = create_remote_venv(host="1.2.3.4", user="root")

        # Should return the remote venv path string.
        assert result is not None
        assert ".specter/venv" in str(result)


class TestSyncDepsRemoteVenv:
    """Verify sync_deps uses remote venv pip, not system pip."""

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_RSYNC_SSH, return_value="ssh -o ControlMaster=auto")
    @patch(_SUBPROCESS_RUN)
    def test_uses_remote_venv_pip(
        self,
        mock_run: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        fake_venv: Path,
    ) -> None:
        """Remote pip install should use ~/.specter/venv/bin/pip, not system pip."""
        from specter_cli.deps import sync_deps

        mock_run.side_effect = [
            MagicMock(returncode=0, stdout="torch==2.3.0\nrequests==2.31.0\n"),
            MagicMock(returncode=0),  # rsync
            MagicMock(returncode=0, stderr=""),  # remote pip install
        ]

        sync_deps(fake_venv, host="1.2.3.4", user="root")

        # The SSH pip install command should reference the venv's pip.
        ssh_call = mock_run.call_args_list[2]
        ssh_cmd = ssh_call[0][0]
        install_cmd = " ".join(ssh_cmd)
        assert ".specter/venv/bin/pip" in install_cmd or "specter/venv" in install_cmd

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_RSYNC_SSH, return_value="ssh -o ControlMaster=auto")
    @patch(_SUBPROCESS_RUN)
    def test_no_break_system_packages(
        self,
        mock_run: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        fake_venv: Path,
    ) -> None:
        """--break-system-packages should NOT be present when using remote venv."""
        from specter_cli.deps import sync_deps

        mock_run.side_effect = [
            MagicMock(returncode=0, stdout="requests==2.31.0\n"),
            MagicMock(returncode=0),  # rsync
            MagicMock(returncode=0, stderr=""),  # remote pip install
        ]

        sync_deps(fake_venv, host="1.2.3.4", user="root")

        # Check all subprocess calls — none should contain --break-system-packages.
        for c in mock_run.call_args_list:
            cmd_str = " ".join(str(a) for a in c[0][0])
            assert "--break-system-packages" not in cmd_str


class TestProxyRemoteVenvActivation:
    """Verify proxy.py activates the remote venv before running commands."""

    @patch("specter_cli.proxy.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: MagicMock,
        mock_ensure_dir: MagicMock,
        mock_rsync: MagicMock,
        mock_popen: MagicMock,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Remote command should be prefixed with venv activation."""
        from specter_cli.proxy import run_remote

        # Set up remote config with remote_venv field.
        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.config import save_remote_config

        save_remote_config(
            host="10.0.0.1",
            user="root",
            ssh_port=22,
            session_id="sess_abc123",
            gpu_type="h100",
            remote_venv="~/.specter/venv",
        )

        project_dir = tmp_path / "myproject"
        project_dir.mkdir()
        (project_dir / ".git").mkdir()
        (project_dir / "train.py").write_text("print('hello')")

        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 venv activation.
        ssh_cmd = mock_popen.call_args[0][0]
        remote_cmd = ssh_cmd[-1]  # Last arg is the remote command string.
        assert "source" in remote_cmd and ".specter/venv" in remote_cmd

    @patch("specter_cli.proxy.subprocess.Popen")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    def test_venv_activation_before_cd(
        self,
        mock_find_root: MagicMock,
        mock_ensure_dir: MagicMock,
        mock_rsync: MagicMock,
        mock_popen: MagicMock,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Venv activation should come before or with the cd command."""
        from specter_cli.proxy import run_remote

        monkeypatch.setenv("SPECTER_HOME", str(tmp_path / ".specter"))
        from specter_cli.config import save_remote_config

        save_remote_config(
            host="10.0.0.1",
            user="root",
            ssh_port=22,
            session_id="sess_abc123",
            gpu_type="h100",
            remote_venv="~/.specter/venv",
        )

        project_dir = tmp_path / "myproject"
        project_dir.mkdir()
        (project_dir / ".git").mkdir()
        (project_dir / "train.py").write_text("print('hello')")

        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 and cd should both be present.
        assert "activate" in remote_cmd
        assert "cd" in remote_cmd


class TestInstallFlowRemoteVenv:
    """Tests for the install flow wiring of remote venv creation.

    These will be fleshed out once ember's implementation lands and
    we know the exact import structure / mock targets for main.py's
    lazy imports.
    """

    def test_placeholder_remote_venv_in_config(self) -> None:
        """Placeholder: install should save remote_venv path in config.

        Will be implemented once we see ember's wiring in main.py.
        The key assertions:
        - create_remote_venv() called between version check and dep sync
        - save_remote_config() receives remote_venv kwarg
        - create_remote_venv() failure aborts install (no catch/warn)
        """
        # TODO: implement once ember pushes remote venv changes to main.py
        pass
