"""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")

        # create_remote_venv makes 2 SSH calls: venv creation + install_remote_uv.
        assert mock_run.call_count == 2
        cmd = mock_run.call_args_list[0][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")

        # First call is venv creation (second is install_remote_uv).
        cmd = mock_run.call_args_list[0][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")

        # ssh_base_args is called twice: once for venv creation, once for install_remote_uv.
        assert mock_ssh_base.call_count == 2
        mock_ssh_base.assert_any_call("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")

        # 2 calls per create_remote_venv (venv creation + install_remote_uv) x 2 invocations.
        assert mock_run.call_count == 4

    @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."""

    _FIND_ROOT = "specter_cli.sync.find_project_root"
    _RSYNC_WORKSPACE = "specter_cli.sync.rsync_workspace"

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_RSYNC_WORKSPACE)
    @patch(_SUBPROCESS_RUN)
    @patch(_FIND_ROOT)
    def test_uses_remote_venv_python(
        self,
        mock_find_root: MagicMock,
        mock_run: MagicMock,
        mock_rsync: MagicMock,
        _ssh_base: MagicMock,
        tmp_path: Path,
        fake_venv: Path,
    ) -> None:
        """Remote uv pip install should target the remote venv's python."""
        from specter_cli.deps import sync_deps

        project = tmp_path / "project"
        project.mkdir()
        (project / "pyproject.toml").write_text("[project]\nname='test'\n")
        mock_find_root.return_value = project
        mock_run.return_value = MagicMock(returncode=0, stderr="")

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

        # The SSH command should use uv pip install targeting the venv.
        ssh_cmd = mock_run.call_args[0][0]
        install_cmd = " ".join(ssh_cmd)
        assert "uv pip install" in install_cmd
        assert ".specter/venv" in install_cmd

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

        project = tmp_path / "project"
        project.mkdir()
        (project / "pyproject.toml").write_text("[project]\nname='test'\n")
        mock_find_root.return_value = project
        mock_run.return_value = MagicMock(returncode=0, stderr="")

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

        # Check subprocess call — should not contain --break-system-packages.
        ssh_cmd = mock_run.call_args[0][0]
        cmd_str = " ".join(str(a) for a in ssh_cmd)
        assert "--break-system-packages" not in cmd_str


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

    @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: 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"))
        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=22,
            session_id="sess_abc123",
            gpu_type="h100",
            remote_venv="~/.specter/venv",
            venv_path=str(fake_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.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_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"))
        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=22,
            session_id="sess_abc123",
            gpu_type="h100",
            remote_venv="~/.specter/venv",
            venv_path=str(fake_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.

    Verifies that main.py's install command correctly wires
    create_remote_venv() into the install flow.
    """

    _BROKER_CLIENT = "specter_cli.broker_client.BrokerClient"
    _BROKER_URL = "specter_cli.broker_client.get_broker_url"
    _REQUIRE_AUTH = "specter_cli.auth.require_auth"
    _READ_SSH_KEY = "specter_cli.auth.read_ssh_public_key"
    _HAS_SESSION = "specter_cli.config.has_active_session"
    _SAVE_CONFIG = "specter_cli.config.save_remote_config"
    _REGISTER_SESSION = "specter_cli.config.register_session"
    _DETECT_VENV = "specter_cli.venv.detect_venv"
    _PATCH_VENV = "specter_cli.venv.patch_venv"
    _SYNC_DEPS = "specter_cli.deps.sync_deps"
    _CHECK_PY_VERSION = "specter_cli.deps.check_python_version"
    _CREATE_REMOTE_VENV = "specter_cli.deps.create_remote_venv"
    _TIME_SLEEP = "time.sleep"
    _FAKE_VENV = Path("/home/user/project/.venv")

    def _setup_broker(self, mock_broker_cls: MagicMock) -> MagicMock:
        """Set up a broker mock that returns ready after one poll."""
        client = mock_broker_cls.return_value
        client.provision.return_value = {
            "session_id": "sess_rvenv_001",
            "status": "provisioning",
        }
        client.get_session.return_value = {
            "status": "ready",
            "ssh": {"host": "10.0.0.1", "port": 22, "user": "root"},
        }
        return client

    @patch(_TIME_SLEEP)
    @patch(_SYNC_DEPS, return_value=True)
    @patch(_CHECK_PY_VERSION, return_value=("3.12", "3.12"))
    @patch(_CREATE_REMOTE_VENV, return_value="~/.specter/venv")
    @patch(_PATCH_VENV)
    @patch(_DETECT_VENV, return_value=_FAKE_VENV)
    @patch(_REGISTER_SESSION)
    @patch(_SAVE_CONFIG)
    @patch(_BROKER_URL, return_value="http://test-broker")
    @patch(_BROKER_CLIENT)
    @patch(_READ_SSH_KEY, return_value="ssh-ed25519 AAAA test")
    @patch(_REQUIRE_AUTH, return_value="sk-test-1234")
    @patch(_HAS_SESSION, return_value=False)
    def test_create_remote_venv_called_between_version_check_and_sync(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        _ssh_key: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        _save_config: MagicMock,
        _register: MagicMock,
        _detect_venv: MagicMock,
        mock_patch_venv: MagicMock,
        mock_create_venv: MagicMock,
        mock_check_ver: MagicMock,
        mock_sync: MagicMock,
        _sleep: MagicMock,
    ) -> None:
        """create_remote_venv should be called after version check and before sync."""
        from click.testing import CliRunner

        from specter_cli.main import cli

        self._setup_broker(mock_broker_cls)

        # Track call ordering via side effects.
        call_order: list[str] = []
        mock_check_ver.side_effect = lambda **kw: (
            call_order.append("check_version") or ("3.12", "3.12")
        )
        mock_create_venv.side_effect = lambda **kw: (
            call_order.append("create_remote_venv") or "~/.specter/venv"
        )
        mock_sync.side_effect = lambda *a, **kw: call_order.append("sync_deps") or True

        runner = CliRunner()
        result = runner.invoke(cli, ["install", "h100"])

        assert result.exit_code == 0, result.output
        assert "check_version" in call_order
        assert "create_remote_venv" in call_order
        assert "sync_deps" in call_order
        assert call_order.index("check_version") < call_order.index("create_remote_venv")
        assert call_order.index("create_remote_venv") < call_order.index("sync_deps")

    @patch(_TIME_SLEEP)
    @patch(_SYNC_DEPS, return_value=True)
    @patch(_CHECK_PY_VERSION, return_value=("3.12", "3.12"))
    @patch(_CREATE_REMOTE_VENV, return_value="~/.specter/venv")
    @patch(_PATCH_VENV)
    @patch(_DETECT_VENV, return_value=_FAKE_VENV)
    @patch(_REGISTER_SESSION)
    @patch(_SAVE_CONFIG)
    @patch(_BROKER_URL, return_value="http://test-broker")
    @patch(_BROKER_CLIENT)
    @patch(_READ_SSH_KEY, return_value="ssh-ed25519 AAAA test")
    @patch(_REQUIRE_AUTH, return_value="sk-test-1234")
    @patch(_HAS_SESSION, return_value=False)
    def test_save_config_receives_remote_venv(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        _ssh_key: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        mock_save_config: MagicMock,
        _register: MagicMock,
        _detect_venv: MagicMock,
        _patch_venv: MagicMock,
        _create_venv: MagicMock,
        _check_ver: MagicMock,
        _sync: MagicMock,
        _sleep: MagicMock,
    ) -> None:
        """save_remote_config should receive the remote_venv kwarg."""
        from click.testing import CliRunner

        from specter_cli.main import cli

        self._setup_broker(mock_broker_cls)

        runner = CliRunner()
        result = runner.invoke(cli, ["install", "h100"])

        assert result.exit_code == 0, result.output
        mock_save_config.assert_called_once()
        call_kwargs = mock_save_config.call_args[1]
        assert "remote_venv" in call_kwargs
        assert call_kwargs["remote_venv"] == "~/.specter/venv"

    @patch(_TIME_SLEEP)
    @patch(_SYNC_DEPS)
    @patch(_CHECK_PY_VERSION, return_value=("3.12", "3.12"))
    @patch(_CREATE_REMOTE_VENV, side_effect=Exception("SSH connection refused"))
    @patch(_DETECT_VENV, return_value=_FAKE_VENV)
    @patch(_SAVE_CONFIG)
    @patch(_BROKER_URL, return_value="http://test-broker")
    @patch(_BROKER_CLIENT)
    @patch(_READ_SSH_KEY, return_value="ssh-ed25519 AAAA test")
    @patch(_REQUIRE_AUTH, return_value="sk-test-1234")
    @patch(_HAS_SESSION, return_value=False)
    def test_create_remote_venv_failure_aborts_install(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        _ssh_key: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        mock_save_config: MagicMock,
        _detect_venv: MagicMock,
        _create_venv: MagicMock,
        _check_ver: MagicMock,
        mock_sync: MagicMock,
        _sleep: MagicMock,
    ) -> None:
        """create_remote_venv failure should abort install — no config save, no dep sync."""
        from click.testing import CliRunner

        from specter_cli.main import cli

        self._setup_broker(mock_broker_cls)

        runner = CliRunner()
        result = runner.invoke(cli, ["install", "h100"])

        assert result.exit_code == 1
        assert "Failed to set up GPU environment" in result.output
        # Config should NOT be saved — install aborted before that point.
        mock_save_config.assert_not_called()
        # Deps should NOT be synced — install aborted before that point.
        mock_sync.assert_not_called()
