"""Tests for dependency synchronisation (specter_cli.deps).

Covers: create_remote_venv, get_local_python_version,
get_remote_python_version, check_python_version, and
the main() entry point.

The pyproject.toml-based sync tests live in test_smart_sync.py.
"""

from __future__ import annotations

import subprocess
from unittest.mock import MagicMock, patch

import pytest

from specter_cli.deps import (
    REMOTE_VENV,
    check_python_version,
    create_remote_venv,
    get_local_python_version,
    get_remote_python_version,
)

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


class TestSyncDepsMain:
    """deps.main() — entry point for the wrapper's pip hook."""

    @patch("specter_cli.deps.sync_deps", return_value=True)
    @patch(
        "specter_cli.config.load_remote_config",
        return_value={
            "host": "1.2.3.4",
            "user": "root",
            "ssh_port": 22,
        },
    )
    @patch.dict("os.environ", {"VIRTUAL_ENV": "/home/user/.venv"})
    @patch("specter_cli.deps.Path")
    def test_main_syncs_when_configured(
        self,
        mock_path_cls: MagicMock,
        _config: MagicMock,
        mock_sync: MagicMock,
    ) -> None:
        from specter_cli.deps import main

        mock_path_instance = MagicMock()
        mock_path_instance.is_dir.return_value = True
        mock_path_cls.return_value = mock_path_instance

        main()

        mock_sync.assert_called_once()

    @patch("specter_cli.deps.sync_deps")
    @patch("specter_cli.config.load_remote_config", return_value={})
    def test_main_no_config_skips(
        self,
        _config: MagicMock,
        mock_sync: MagicMock,
    ) -> None:
        from specter_cli.deps import main

        main()

        mock_sync.assert_not_called()

    @patch("specter_cli.deps.sync_deps")
    @patch(
        "specter_cli.config.load_remote_config",
        return_value={"host": "1.2.3.4", "user": "root"},
    )
    @patch.dict("os.environ", {}, clear=True)
    def test_main_no_venv_skips(
        self,
        _config: MagicMock,
        mock_sync: MagicMock,
    ) -> None:
        from specter_cli.deps import main

        main()

        mock_sync.assert_not_called()

    @patch("specter_cli.deps.sync_deps", side_effect=Exception("SSH failed"))
    @patch(
        "specter_cli.config.load_remote_config",
        return_value={
            "host": "1.2.3.4",
            "user": "root",
            "ssh_port": 22,
        },
    )
    @patch.dict("os.environ", {"VIRTUAL_ENV": "/home/user/.venv"})
    @patch("specter_cli.deps.Path")
    def test_main_handles_errors_gracefully(
        self,
        mock_path_cls: MagicMock,
        _config: MagicMock,
        _sync: MagicMock,
    ) -> None:
        """sync failure should not crash — just print a warning."""
        from specter_cli.deps import main

        mock_path_instance = MagicMock()
        mock_path_instance.is_dir.return_value = True
        mock_path_cls.return_value = mock_path_instance

        # Should not raise — errors are caught and logged.
        main()


class TestCreateRemoteVenv:
    """create_remote_venv creates a venv on the remote pod via SSH."""

    @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:
        mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")

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

        # First call: create venv. Second call: install uv (non-fatal).
        assert mock_run.call_count >= 1
        cmd = mock_run.call_args_list[0][0][0]
        ssh_cmd_str = " ".join(cmd)
        assert "python3 -m venv --system-site-packages ~/.specter/venv" in ssh_cmd_str
        assert result == REMOTE_VENV

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_failure_raises(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="venv failed")

        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_custom_venv_path(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")

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

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

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_custom_port_and_key(self, mock_run: MagicMock, mock_ssh_base: MagicMock) -> None:
        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")

        # First call is create_remote_venv, second is install_remote_uv.
        assert mock_ssh_base.call_args_list[0] == (
            ("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_tilde_not_quoted(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        """Tilde in venv path should not be quoted — remote shell expands it."""
        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]
        # The venv path appears as the last part of the SSH command string.
        ssh_remote_cmd = cmd[-1]
        assert "~/.specter/venv" in ssh_remote_cmd
        assert "'" not in ssh_remote_cmd  # Not quoted


class TestGetLocalPythonVersion:
    """get_local_python_version returns 'major.minor' from sys.version_info."""

    def test_returns_major_minor(self) -> None:
        import sys

        expected = f"{sys.version_info.major}.{sys.version_info.minor}"
        assert get_local_python_version() == expected

    @patch("specter_cli.deps.sys")
    def test_format(self, mock_sys: MagicMock) -> None:
        mock_sys.version_info.major = 3
        mock_sys.version_info.minor = 12
        assert get_local_python_version() == "3.12"


class TestGetRemotePythonVersion:
    """get_remote_python_version SSHs to remote and returns version string."""

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_returns_version_on_success(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        mock_run.return_value = MagicMock(returncode=0, stdout="3.12\n")

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

        assert result == "3.12"

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_returns_none_on_failure(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="error")

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

        assert result is None

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_returns_none_on_timeout(self, mock_run: MagicMock, _ssh_base: MagicMock) -> None:
        import subprocess

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

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

        assert result is None

    @patch(_SSH_BASE, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch(_SUBPROCESS_RUN)
    def test_passes_custom_port_and_key(
        self, mock_run: MagicMock, mock_ssh_base: MagicMock
    ) -> None:
        mock_run.return_value = MagicMock(returncode=0, stdout="3.11\n")

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


class TestCheckPythonVersion:
    """check_python_version compares local and remote versions."""

    @patch("specter_cli.deps.get_remote_python_version", return_value="3.12")
    @patch("specter_cli.deps.get_local_python_version", return_value="3.12")
    def test_matching_versions(self, _local: MagicMock, _remote: MagicMock) -> None:
        local, remote = check_python_version(host="1.2.3.4", user="root")

        assert local == "3.12"
        assert remote == "3.12"

    @patch("specter_cli.deps.get_remote_python_version", return_value="3.12")
    @patch("specter_cli.deps.get_local_python_version", return_value="3.11")
    def test_mismatched_versions(self, _local: MagicMock, _remote: MagicMock) -> None:
        local, remote = check_python_version(host="1.2.3.4", user="root")

        assert local == "3.11"
        assert remote == "3.12"

    @patch("specter_cli.deps.get_remote_python_version", return_value=None)
    @patch("specter_cli.deps.get_local_python_version", return_value="3.12")
    def test_remote_unavailable(self, _local: MagicMock, _remote: MagicMock) -> None:
        local, remote = check_python_version(host="1.2.3.4", user="root")

        assert local == "3.12"
        assert remote is None
