"""Tests for v0.1.5 uv remote mirroring and forwarding.

Covers:
1. install_remote_uv() — installs uv in remote venv, non-fatal on failure
2. uv_forward() — remote-first uv forwarding with workspace rsync
3. _uv_sync_remote() — proxy-time uv sync, non-fatal
4. Proxy uv.lock detection gate — only syncs for uv-managed projects
5. bin/uv shim lifecycle — created by patch_venv, removed by restore_venv
"""

from __future__ import annotations

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

import pytest

_SUBPROCESS_RUN = "specter_cli.deps.subprocess.run"
_SSH_BASE = "specter_cli.deps.ssh_base_args"
_LOAD_CONFIG = "specter_cli.config.load_remote_config"

_PROXY_SUBPROCESS_RUN = "specter_cli.proxy.subprocess.run"
_PROXY_SSH_BASE = "specter_cli.proxy.ssh_base_args"

_SAMPLE_CONFIG = {
    "host": "1.2.3.4",
    "user": "root",
    "ssh_port": 22,
    "remote_venv": "~/.specter/venv",
    "workspace": "~/.specter/workspace",
}


# ── install_remote_uv ──────────────────────────────────────────────────


class TestInstallRemoteUv:
    """install_remote_uv() installs uv into remote venv via pip."""

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_happy_path(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """SSH pip install uv succeeds — no exception raised."""
        from specter_cli.deps import install_remote_uv

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

        # Should not raise.
        install_remote_uv(host="1.2.3.4", user="root")

        mock_run.assert_called_once()
        cmd = mock_run.call_args[0][0]
        ssh_cmd_str = " ".join(cmd)
        assert "~/.specter/venv/bin/pip install -q uv" in ssh_cmd_str

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_nonfatal_on_ssh_failure(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """SSH failure (returncode=1) should NOT raise — just log warning."""
        from specter_cli.deps import install_remote_uv

        mock_run.return_value = MagicMock(
            returncode=1, stdout="", stderr="ERROR: could not install uv"
        )

        # Must not raise — non-fatal.
        install_remote_uv(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_nonfatal_on_timeout(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """SSH timeout should NOT raise — just log warning."""
        from specter_cli.deps import install_remote_uv

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

        # Must not raise — non-fatal.
        install_remote_uv(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_nonfatal_on_oserror(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """OSError (e.g. ssh binary missing) should NOT raise — just log warning."""
        from specter_cli.deps import install_remote_uv

        mock_run.side_effect = OSError("ssh not found")

        # Must not raise — non-fatal.
        install_remote_uv(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_uses_custom_venv_path(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """Should use the provided venv_path for pip location."""
        from specter_cli.deps import install_remote_uv

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

        install_remote_uv(host="1.2.3.4", user="root", venv_path="/opt/custom-venv")

        cmd = mock_run.call_args[0][0]
        ssh_cmd_str = " ".join(cmd)
        assert "/opt/custom-venv/bin/pip install -q uv" in ssh_cmd_str

    @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 install_remote_uv

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

        install_remote_uv(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_called_from_create_remote_venv(
        self, mock_run: MagicMock, _ssh_base: MagicMock
    ) -> None:
        """create_remote_venv should call install_remote_uv after venv creation."""
        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: venv creation. Second call: install uv.
        assert mock_run.call_count == 2
        uv_call = mock_run.call_args_list[1]
        uv_cmd_str = " ".join(uv_call[0][0])
        assert "pip install -q uv" in uv_cmd_str


# ── uv_forward ──────────────────────────────────────────────────────────


class TestUvForward:
    """uv_forward() — remote-first uv forwarding called by bin/uv shim."""

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch("specter_cli.ssh_config.rsync_ssh_args", return_value="ssh -o ControlMaster=auto")
    @patch("specter_cli.sync.rsync_workspace")
    @patch("specter_cli.sync.find_project_root")
    @patch(_SUBPROCESS_RUN)
    @patch(_LOAD_CONFIG, return_value=_SAMPLE_CONFIG)
    def test_happy_path(
        self,
        _config: MagicMock,
        mock_run: MagicMock,
        mock_find_root: MagicMock,
        mock_rsync: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        tmp_path: Path,
    ) -> None:
        """uv sync should rsync workspace then SSH uv on remote."""
        from specter_cli.deps import uv_forward

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

        rc = uv_forward(["sync", "--frozen"])

        assert rc == 0
        # rsync should have been called.
        mock_rsync.assert_called_once()
        # SSH should have been called.
        mock_run.assert_called_once()
        cmd = mock_run.call_args[0][0]
        ssh_cmd_str = " ".join(cmd)
        assert "uv sync --frozen" in ssh_cmd_str
        assert "UV_PROJECT_ENVIRONMENT=" in ssh_cmd_str
        assert ".specter/venv" in ssh_cmd_str

    @patch(_LOAD_CONFIG, return_value={})
    def test_no_config_returns_1(self, _config: MagicMock) -> None:
        """No active session → return 1 silently."""
        from specter_cli.deps import uv_forward

        rc = uv_forward(["sync"])

        assert rc == 1

    @patch(_LOAD_CONFIG, return_value=None)
    def test_none_config_returns_1(self, _config: MagicMock) -> None:
        """None config → return 1 silently."""
        from specter_cli.deps import uv_forward

        rc = uv_forward(["sync"])

        assert rc == 1

    @patch(_LOAD_CONFIG, return_value={"user": "root"})
    def test_config_without_host_returns_1(self, _config: MagicMock) -> None:
        """Config without 'host' key → return 1 silently."""
        from specter_cli.deps import uv_forward

        rc = uv_forward(["add", "numpy"])

        assert rc == 1

    @patch("specter_cli.ssh_config.rsync_ssh_args", return_value="ssh -o ControlMaster=auto")
    @patch(
        "specter_cli.sync.rsync_workspace",
        side_effect=subprocess.CalledProcessError(1, "rsync"),
    )
    @patch("specter_cli.sync.find_project_root")
    @patch(_LOAD_CONFIG, return_value=_SAMPLE_CONFIG)
    def test_rsync_failure_returns_1(
        self,
        _config: MagicMock,
        mock_find_root: MagicMock,
        _rsync: MagicMock,
        _rsync_ssh: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Workspace rsync failure → return 1."""
        from specter_cli.deps import uv_forward

        mock_find_root.return_value = tmp_path

        rc = uv_forward(["sync"])

        assert rc == 1

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch("specter_cli.ssh_config.rsync_ssh_args", return_value="ssh -o ControlMaster=auto")
    @patch("specter_cli.sync.rsync_workspace")
    @patch("specter_cli.sync.find_project_root")
    @patch(_SUBPROCESS_RUN)
    @patch(_LOAD_CONFIG, return_value=_SAMPLE_CONFIG)
    def test_remote_uv_failure_returns_nonzero(
        self,
        _config: MagicMock,
        mock_run: MagicMock,
        mock_find_root: MagicMock,
        _rsync: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Remote uv failure returns non-zero exit code."""
        from specter_cli.deps import uv_forward

        mock_find_root.return_value = tmp_path
        mock_run.return_value = MagicMock(
            returncode=1, stdout="", stderr="error: no pyproject.toml found"
        )

        rc = uv_forward(["sync", "--frozen"])

        assert rc == 1

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch("specter_cli.ssh_config.rsync_ssh_args", return_value="ssh -o ControlMaster=auto")
    @patch("specter_cli.sync.rsync_workspace")
    @patch("specter_cli.sync.find_project_root")
    @patch(_SUBPROCESS_RUN)
    @patch(_LOAD_CONFIG, return_value=_SAMPLE_CONFIG)
    def test_ssh_timeout_returns_1(
        self,
        _config: MagicMock,
        mock_run: MagicMock,
        mock_find_root: MagicMock,
        _rsync: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        tmp_path: Path,
    ) -> None:
        """SSH timeout returns 1."""
        from specter_cli.deps import uv_forward

        mock_find_root.return_value = tmp_path
        mock_run.side_effect = subprocess.TimeoutExpired(cmd="ssh", timeout=600)

        rc = uv_forward(["sync"])

        assert rc == 1

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch("specter_cli.ssh_config.rsync_ssh_args", return_value="ssh -o ControlMaster=auto")
    @patch("specter_cli.sync.rsync_workspace")
    @patch("specter_cli.sync.find_project_root")
    @patch(_SUBPROCESS_RUN)
    @patch(_LOAD_CONFIG, return_value=_SAMPLE_CONFIG)
    def test_ssh_exception_returns_1(
        self,
        _config: MagicMock,
        mock_run: MagicMock,
        mock_find_root: MagicMock,
        _rsync: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Generic exception during SSH returns 1."""
        from specter_cli.deps import uv_forward

        mock_find_root.return_value = tmp_path
        mock_run.side_effect = Exception("connection refused")

        rc = uv_forward(["add", "numpy"])

        assert rc == 1

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch("specter_cli.ssh_config.rsync_ssh_args", return_value="ssh -o ControlMaster=auto")
    @patch("specter_cli.sync.rsync_workspace")
    @patch("specter_cli.sync.find_project_root")
    @patch(_SUBPROCESS_RUN)
    @patch(
        _LOAD_CONFIG,
        return_value={**_SAMPLE_CONFIG, "remote_venv": "/opt/custom-venv"},
    )
    def test_uses_custom_remote_venv(
        self,
        _config: MagicMock,
        mock_run: MagicMock,
        mock_find_root: MagicMock,
        _rsync: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Should use remote_venv from config in the SSH command."""
        from specter_cli.deps import uv_forward

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

        uv_forward(["sync"])

        cmd = mock_run.call_args[0][0]
        ssh_cmd_str = " ".join(cmd)
        assert "UV_PROJECT_ENVIRONMENT=/opt/custom-venv" in ssh_cmd_str

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch("specter_cli.ssh_config.rsync_ssh_args", return_value="ssh -o ControlMaster=auto")
    @patch("specter_cli.sync.rsync_workspace")
    @patch("specter_cli.sync.find_project_root")
    @patch(_SUBPROCESS_RUN)
    @patch(_LOAD_CONFIG, return_value=_SAMPLE_CONFIG)
    def test_ssh_command_has_cd_env_uv(
        self,
        _config: MagicMock,
        mock_run: MagicMock,
        mock_find_root: MagicMock,
        _rsync: MagicMock,
        _rsync_ssh: MagicMock,
        _ssh_base: MagicMock,
        tmp_path: Path,
    ) -> None:
        """SSH command should: cd to workspace, set UV_PROJECT_ENVIRONMENT, run uv."""
        from specter_cli.deps import uv_forward

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

        uv_forward(["add", "requests"])

        cmd = mock_run.call_args[0][0]
        ssh_cmd_str = " ".join(cmd)
        assert "cd" in ssh_cmd_str
        assert "UV_PROJECT_ENVIRONMENT=" in ssh_cmd_str
        assert "uv add requests" in ssh_cmd_str


# ── uv_forward main() wiring ───────────────────────────────────────────


class TestUvForwardMain:
    """Tests for the --uv-forward CLI entry point in main()."""

    @patch("specter_cli.deps.uv_forward", return_value=0)
    def test_uv_forward_flag(self, mock_forward: MagicMock) -> None:
        """--uv-forward should call uv_forward() and exit."""
        import sys

        with patch.object(sys, "argv", ["deps", "--uv-forward", "sync", "--frozen"]):
            with pytest.raises(SystemExit) as exc_info:
                from specter_cli.deps import main

                main()

            assert exc_info.value.code == 0
            mock_forward.assert_called_once_with(["sync", "--frozen"])

    @patch("specter_cli.deps.uv_forward", return_value=1)
    def test_uv_forward_nonzero_exit(self, mock_forward: MagicMock) -> None:
        """Non-zero remote exit should propagate."""
        import sys

        with patch.object(sys, "argv", ["deps", "--uv-forward", "sync"]):
            with pytest.raises(SystemExit) as exc_info:
                from specter_cli.deps import main

                main()

            assert exc_info.value.code == 1


# ── _uv_sync_remote ────────────────────────────────────────────────────


class TestUvSyncRemote:
    """_uv_sync_remote() — proxy-time automatic uv sync on remote."""

    @patch(
        _PROXY_SSH_BASE,
        return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"],
    )
    @patch(_PROXY_SUBPROCESS_RUN)
    def test_happy_path(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """SSH uv sync succeeds — no exception raised."""
        from specter_cli.proxy import _uv_sync_remote

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

        # Should not raise.
        _uv_sync_remote(
            host="1.2.3.4",
            user="root",
            port=22,
            key_path=None,
            remote_workspace="~/.specter/workspace",
            remote_venv="~/.specter/venv",
        )

        mock_run.assert_called_once()
        cmd = mock_run.call_args[0][0]
        ssh_cmd_str = " ".join(cmd)
        assert "uv sync --frozen" in ssh_cmd_str
        assert "cd ~/.specter/workspace" in ssh_cmd_str
        assert "UV_PROJECT_ENVIRONMENT=~/.specter/venv" in ssh_cmd_str

    @patch(
        _PROXY_SSH_BASE,
        return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"],
    )
    @patch(_PROXY_SUBPROCESS_RUN)
    def test_nonfatal_on_failure(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """SSH failure (returncode=1) should NOT raise — non-fatal."""
        from specter_cli.proxy import _uv_sync_remote

        mock_run.return_value = MagicMock(
            returncode=1, stdout="", stderr="error: no pyproject.toml found"
        )

        # Must not raise.
        _uv_sync_remote(
            host="1.2.3.4",
            user="root",
            port=22,
            key_path=None,
            remote_workspace="~/.specter/workspace",
            remote_venv="~/.specter/venv",
        )

    @patch(
        _PROXY_SSH_BASE,
        return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"],
    )
    @patch(_PROXY_SUBPROCESS_RUN)
    def test_nonfatal_on_timeout(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """SSH timeout should NOT raise — non-fatal."""
        from specter_cli.proxy import _uv_sync_remote

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

        # Must not raise.
        _uv_sync_remote(
            host="1.2.3.4",
            user="root",
            port=22,
            key_path=None,
            remote_workspace="~/.specter/workspace",
            remote_venv="~/.specter/venv",
        )

    @patch(
        _PROXY_SSH_BASE,
        return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"],
    )
    @patch(_PROXY_SUBPROCESS_RUN)
    def test_nonfatal_on_oserror(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """OSError should NOT raise — non-fatal."""
        from specter_cli.proxy import _uv_sync_remote

        mock_run.side_effect = OSError("ssh binary not found")

        # Must not raise.
        _uv_sync_remote(
            host="1.2.3.4",
            user="root",
            port=22,
            key_path=None,
            remote_workspace="~/.specter/workspace",
            remote_venv="~/.specter/venv",
        )

    @patch(
        _PROXY_SSH_BASE,
        return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"],
    )
    @patch(_PROXY_SUBPROCESS_RUN)
    def test_uses_300s_timeout(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """SSH call should use 300 second timeout."""
        from specter_cli.proxy import _uv_sync_remote

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

        _uv_sync_remote(
            host="1.2.3.4",
            user="root",
            port=22,
            key_path=None,
            remote_workspace="~/.specter/workspace",
            remote_venv="~/.specter/venv",
        )

        # Check the timeout kwarg.
        _, kwargs = mock_run.call_args
        assert kwargs.get("timeout") == 300


# ── Proxy uv.lock detection gate ───────────────────────────────────────


class TestProxyUvLockGate:
    """run_remote() only calls _uv_sync_remote when uv.lock exists."""

    @patch("specter_cli.ssh_config.subprocess.Popen")
    @patch("specter_cli.proxy._uv_sync_remote")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    @patch(
        "specter_cli.proxy.load_remote_config",
        return_value=_SAMPLE_CONFIG,
    )
    def test_uv_lock_exists_triggers_sync(
        self,
        _config: MagicMock,
        mock_find_root: MagicMock,
        _ensure_dir: MagicMock,
        _rsync: MagicMock,
        mock_uv_sync: MagicMock,
        mock_popen: MagicMock,
        tmp_path: Path,
    ) -> None:
        """When uv.lock exists in project root, _uv_sync_remote is called."""
        from specter_cli.proxy import run_remote

        project_dir = tmp_path / "myproject"
        project_dir.mkdir()
        (project_dir / "uv.lock").write_text("# uv lock file\n")
        (project_dir / "pyproject.toml").write_text("[project]\nname='test'\n")

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

        mock_uv_sync.assert_called_once()

    @patch("specter_cli.ssh_config.subprocess.Popen")
    @patch("specter_cli.proxy._uv_sync_remote")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    @patch(
        "specter_cli.proxy.load_remote_config",
        return_value=_SAMPLE_CONFIG,
    )
    def test_no_uv_lock_skips_sync(
        self,
        _config: MagicMock,
        mock_find_root: MagicMock,
        _ensure_dir: MagicMock,
        _rsync: MagicMock,
        mock_uv_sync: MagicMock,
        mock_popen: MagicMock,
        tmp_path: Path,
    ) -> None:
        """When uv.lock does NOT exist, _uv_sync_remote is NOT called."""
        from specter_cli.proxy import run_remote

        project_dir = tmp_path / "myproject"
        project_dir.mkdir()
        # No uv.lock file — pip-based project.

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

        mock_uv_sync.assert_not_called()

    @patch("specter_cli.ssh_config.subprocess.Popen")
    @patch("specter_cli.proxy._uv_sync_remote")
    @patch("specter_cli.proxy.rsync_workspace")
    @patch("specter_cli.proxy._ensure_remote_dir")
    @patch("specter_cli.proxy.find_project_root")
    @patch(
        "specter_cli.proxy.load_remote_config",
        return_value=_SAMPLE_CONFIG,
    )
    def test_uv_sync_args_correct(
        self,
        _config: MagicMock,
        mock_find_root: MagicMock,
        _ensure_dir: MagicMock,
        _rsync: MagicMock,
        mock_uv_sync: MagicMock,
        mock_popen: MagicMock,
        tmp_path: Path,
    ) -> None:
        """_uv_sync_remote should receive remote_workspace (not remote_cwd)."""
        from specter_cli.proxy import run_remote

        project_dir = tmp_path / "myproject"
        project_dir.mkdir()
        (project_dir / "uv.lock").write_text("# uv lock file\n")
        sub_dir = project_dir / "src"
        sub_dir.mkdir()

        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

        # Run from a subdirectory — uv sync should still use workspace root.
        with patch("specter_cli.proxy.Path.cwd", return_value=sub_dir):
            run_remote(["python", "train.py"])

        mock_uv_sync.assert_called_once()
        call_kwargs = mock_uv_sync.call_args
        # The remote_workspace arg should be the workspace root, not a subdir.
        # Check positional or keyword args — the function signature uses positional.
        args = call_kwargs[0] if call_kwargs[0] else ()
        kwargs = call_kwargs[1] if call_kwargs[1] else {}
        # remote_workspace is the 5th positional arg.
        all_args = {
            **dict(
                zip(
                    ["host", "user", "port", "key_path", "remote_workspace", "remote_venv"],
                    args,
                    strict=False,
                )
            ),
            **kwargs,
        }
        assert all_args.get("remote_workspace") == "~/.specter/workspace"


# ── bin/uv shim lifecycle ──────────────────────────────────────────────


class TestUvShimLifecycle:
    """bin/uv shim: created by patch_venv(), removed by restore_venv()."""

    def _make_venv(self, tmp_path: Path) -> Path:
        """Create a minimal fake venv for patching."""
        venv = tmp_path / "myvenv"
        bin_dir = venv / "bin"
        bin_dir.mkdir(parents=True)
        python_bin = bin_dir / "python"
        python_bin.write_text("#!/usr/bin/env python3\n# real python\n")
        python_bin.chmod(python_bin.stat().st_mode | stat.S_IEXEC)
        python3_bin = bin_dir / "python3"
        python3_bin.symlink_to("python")
        return venv

    def test_shim_exists_after_patch(self, tmp_path: Path) -> None:
        """bin/uv shim should exist after patch_venv()."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        patch_venv(venv, "h100")

        uv_shim = venv / "bin" / "uv"
        assert uv_shim.exists(), "bin/uv shim should exist after patching"

    def test_shim_is_executable(self, tmp_path: Path) -> None:
        """bin/uv shim should be executable."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        patch_venv(venv, "h100")

        uv_shim = venv / "bin" / "uv"
        assert uv_shim.stat().st_mode & stat.S_IEXEC, "uv shim should be executable"

    def test_shim_is_bash_script(self, tmp_path: Path) -> None:
        """bin/uv shim should be a bash script."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        patch_venv(venv, "h100")

        uv_shim = venv / "bin" / "uv"
        content = uv_shim.read_text()
        assert content.startswith("#!/bin/bash"), "uv shim should be a bash script"

    def test_shim_removed_after_restore(self, tmp_path: Path) -> None:
        """bin/uv shim should be removed by restore_venv()."""
        from specter_cli.venv import patch_venv, restore_venv

        venv = self._make_venv(tmp_path)
        patch_venv(venv, "h100")

        # Verify it exists first.
        assert (venv / "bin" / "uv").exists()

        restore_venv(venv)

        assert not (venv / "bin" / "uv").exists(), "bin/uv shim should be removed after restore"

    def test_shim_catches_modifying_commands(self, tmp_path: Path) -> None:
        """Shim should have case for sync|add|remove|pip (modifying commands)."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        patch_venv(venv, "h100")

        content = (venv / "bin" / "uv").read_text()
        # The case statement should catch package-modifying commands.
        assert "sync" in content
        assert "add" in content
        assert "remove" in content
        assert "pip" in content

    def test_shim_has_real_uv_discovery(self, tmp_path: Path) -> None:
        """Shim should discover the real uv binary via PATH cleanup."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        patch_venv(venv, "h100")

        content = (venv / "bin" / "uv").read_text()
        assert "REAL_UV" in content, "Shim must discover real uv binary"
        assert "command -v uv" in content, "Shim should use 'command -v uv' for discovery"

    def test_shim_has_path_cleanup(self, tmp_path: Path) -> None:
        """Shim should clean BIN_DIR from PATH before finding real uv."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        patch_venv(venv, "h100")

        content = (venv / "bin" / "uv").read_text()
        # Two-step PATH cleanup: remove BIN_DIR: prefix and :BIN_DIR suffix.
        assert "CLEAN_PATH" in content, "Shim should have PATH cleanup"
        assert "BIN_DIR" in content

    def test_shim_has_remote_first_pattern(self, tmp_path: Path) -> None:
        """Shim should forward to remote first, then run locally."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        patch_venv(venv, "h100")

        content = (venv / "bin" / "uv").read_text()
        assert "specter_cli.deps" in content, "Shim should call specter_cli.deps"
        assert "--uv-forward" in content, "Shim should use --uv-forward flag"
        # Remote first, then local.
        assert "REMOTE_EXIT" in content
        assert "LOCAL_EXIT" in content

    def test_shim_passes_nonmodifying_to_real_uv(self, tmp_path: Path) -> None:
        """Non-modifying commands (version, lock) should pass through to real uv."""
        from specter_cli.venv import patch_venv

        venv = self._make_venv(tmp_path)
        patch_venv(venv, "h100")

        content = (venv / "bin" / "uv").read_text()
        # The default case should exec REAL_UV.
        assert "exec" in content and "REAL_UV" in content

    def test_shim_survives_patch_restore_patch_cycle(self, tmp_path: Path) -> None:
        """bin/uv shim should be recreated correctly after patch→restore→patch."""
        from specter_cli.venv import patch_venv, restore_venv

        venv = self._make_venv(tmp_path)

        patch_venv(venv, "h100")
        assert (venv / "bin" / "uv").exists()

        restore_venv(venv)
        assert not (venv / "bin" / "uv").exists()

        patch_venv(venv, "a100")
        uv_shim = venv / "bin" / "uv"
        assert uv_shim.exists()
        content = uv_shim.read_text()
        assert "#!/bin/bash" in content
        assert "--uv-forward" in content
