"""Tests for CLI commands (install, uninstall, status, login, logout).

Uses Click's CliRunner with mocked BrokerClient and config functions.
All imports in main.py are lazy (inside function bodies), so patches target
the *source* modules, not ``specter_cli.main``.
"""

from __future__ import annotations

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

import pytest
from click.testing import CliRunner

from specter_cli.main import cli


@pytest.fixture
def runner() -> CliRunner:
    return CliRunner()


# ---------------------------------------------------------------------------
# Shared patch helpers
# ---------------------------------------------------------------------------

_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"
_LOAD_CONFIG = "specter_cli.config.load_remote_config"
_CLEAR_CONFIG = "specter_cli.config.clear_remote_config"
_REGISTER_SESSION = "specter_cli.config.register_session"
_DEREGISTER_SESSION = "specter_cli.config.deregister_session"
_DETECT_VENV = "specter_cli.venv.detect_venv"
_PATCH_VENV = "specter_cli.venv.patch_venv"
_RESTORE_VENV = "specter_cli.venv.restore_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 returned by detect_venv in install tests.
_FAKE_VENV = Path("/home/user/project/.venv")


# ---------------------------------------------------------------------------
# install — happy path (venv scope)
# ---------------------------------------------------------------------------


class TestInstallHappyPath:
    """specter install <gpu> — provision succeeds, session reaches ready."""

    @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_install_provisions_and_saves_config(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        _ssh_key: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        mock_save_config: MagicMock,
        mock_register: MagicMock,
        _detect_venv: MagicMock,
        _patch_venv: MagicMock,
        _create_remote_venv: MagicMock,
        _check_py_version: MagicMock,
        mock_sync_deps: MagicMock,
        _sleep: MagicMock,
        runner: CliRunner,
    ) -> None:
        client = mock_broker_cls.return_value
        client.provision.return_value = {
            "session_id": "sess_abc123",
            "status": "provisioning",
        }
        # First poll: provisioning, second: ready.
        client.get_session.side_effect = [
            {"status": "provisioning"},
            {
                "status": "ready",
                "ssh": {"host": "1.2.3.4", "port": 22, "user": "root"},
                "gpu_name": "NVIDIA GeForce RTX 5090",
            },
        ]

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

        assert result.exit_code == 0, result.output
        assert "5090 installed in" in result.output
        assert "Installing 5090" in result.output
        assert "Dependencies synced" in result.output

        # Verify provision was called correctly.
        client.provision.assert_called_once_with(
            gpu_type="5090",
            ssh_public_key="ssh-ed25519 AAAA test",
            gpu_count=1,
            disk_gb=20,
            image=None,
        )

        # Verify config was saved to venv scope (no use_global).
        mock_save_config.assert_called_once_with(
            host="1.2.3.4",
            user="root",
            ssh_port=22,
            session_id="sess_abc123",
            gpu_type="5090",
            venv_path=str(_FAKE_VENV),
            use_global=False,
            python_local="3.12",
            python_remote="3.12",
            remote_venv="~/.specter/venv",
        )

        # Verify session was registered.
        mock_register.assert_called_once_with(
            venv_path=str(_FAKE_VENV),
            session_id="sess_abc123",
            gpu_type="5090",
        )

        # Verify deps were synced.
        mock_sync_deps.assert_called_once_with(
            _FAKE_VENV,
            host="1.2.3.4",
            user="root",
            port=22,
        )

        # Verify client was closed.
        client.close.assert_called_once()

    @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_install_with_custom_options(
        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_remote_venv: MagicMock,
        _check_py_version: MagicMock,
        _sync_deps: MagicMock,
        _sleep: MagicMock,
        runner: CliRunner,
    ) -> None:
        client = mock_broker_cls.return_value
        client.provision.return_value = {"session_id": "sess_xyz", "status": "provisioning"}
        client.get_session.return_value = {
            "status": "ready",
            "ssh": {"host": "5.6.7.8", "port": 43237, "user": "root"},
        }

        result = runner.invoke(
            cli,
            ["install", "h100", "--gpu-count", "2", "--disk", "50", "--image", "custom:latest"],
        )

        assert result.exit_code == 0, result.output

        client.provision.assert_called_once_with(
            gpu_type="h100",
            ssh_public_key="ssh-ed25519 AAAA test",
            gpu_count=2,
            disk_gb=50,
            image="custom:latest",
        )

        mock_save_config.assert_called_once_with(
            host="5.6.7.8",
            user="root",
            ssh_port=43237,
            session_id="sess_xyz",
            gpu_type="h100",
            venv_path=str(_FAKE_VENV),
            use_global=False,
            python_local="3.12",
            python_remote="3.12",
            remote_venv="~/.specter/venv",
        )

    @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_install_polls_multiple_times(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        _ssh_key: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        _save_config: MagicMock,
        _register: MagicMock,
        _detect_venv: MagicMock,
        _patch_venv: MagicMock,
        _create_remote_venv: MagicMock,
        _check_py_version: MagicMock,
        _sync_deps: MagicMock,
        _sleep: MagicMock,
        runner: CliRunner,
    ) -> None:
        client = mock_broker_cls.return_value
        client.provision.return_value = {"session_id": "sess_poll", "status": "provisioning"}
        # Simulate 5 polls before ready.
        client.get_session.side_effect = [
            {"status": "provisioning"},
            {"status": "provisioning"},
            {"status": "provisioning"},
            {"status": "provisioning"},
            {
                "status": "ready",
                "ssh": {"host": "10.0.0.1", "port": 22, "user": "root"},
            },
        ]

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

        assert result.exit_code == 0, result.output
        assert client.get_session.call_count == 5


# ---------------------------------------------------------------------------
# install --global — happy path
# ---------------------------------------------------------------------------


class TestInstallGlobalHappyPath:
    """specter install --global <gpu> — global scope install."""

    @patch(_TIME_SLEEP)
    @patch(_CHECK_PY_VERSION, return_value=("3.12", "3.12"))
    @patch(_CREATE_REMOTE_VENV, return_value="~/.specter/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_install_global_skips_venv_and_deps(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        _ssh_key: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        mock_save_config: MagicMock,
        mock_register: MagicMock,
        _create_remote_venv: MagicMock,
        _check_py_version: MagicMock,
        _sleep: MagicMock,
        runner: CliRunner,
    ) -> None:
        client = mock_broker_cls.return_value
        client.provision.return_value = {"session_id": "sess_global", "status": "provisioning"}
        client.get_session.return_value = {
            "status": "ready",
            "ssh": {"host": "1.2.3.4", "port": 22, "user": "root"},
        }

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

        assert result.exit_code == 0, result.output
        assert "h100 installed in system Python" in result.output
        # No venv detection, no dep sync for global.
        assert "Installing h100 into system Python" in result.output
        assert "Dependencies synced" not in result.output

        # Config saved with use_global=True, no venv_path.
        mock_save_config.assert_called_once()
        call_kwargs = mock_save_config.call_args[1]
        assert call_kwargs["use_global"] is True
        assert call_kwargs["venv_path"] is None

        # No session registered (global installs skip registry).
        mock_register.assert_not_called()


# ---------------------------------------------------------------------------
# install — error path
# ---------------------------------------------------------------------------


class TestInstallErrorPath:
    """specter install <gpu> — provisioning fails."""

    @patch(_DETECT_VENV, return_value=None)
    def test_install_no_venv_exits(
        self,
        _detect_venv: MagicMock,
        runner: CliRunner,
    ) -> None:
        result = runner.invoke(cli, ["install", "5090"])

        assert result.exit_code == 1
        assert "No active virtual environment" in result.output
        assert "--global" in result.output  # Hint about --global.

    @patch(_TIME_SLEEP)
    @patch(_DETECT_VENV, return_value=_FAKE_VENV)
    @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_install_error_shows_message_and_exits(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        _ssh_key: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        _detect_venv: MagicMock,
        _sleep: MagicMock,
        runner: CliRunner,
    ) -> None:
        client = mock_broker_cls.return_value
        client.provision.return_value = {"session_id": "sess_fail", "status": "provisioning"}
        client.get_session.return_value = {
            "status": "error",
            "error_message": "Failed to get pod with public IP after 3 attempts",
        }

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

        assert result.exit_code == 1
        assert "Installation failed" in result.output
        assert "Failed to get pod with public IP" in result.output
        client.close.assert_called_once()

    @patch(_TIME_SLEEP)
    @patch(_DETECT_VENV, return_value=_FAKE_VENV)
    @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_install_error_with_no_message(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        _ssh_key: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        _detect_venv: MagicMock,
        _sleep: MagicMock,
        runner: CliRunner,
    ) -> None:
        client = mock_broker_cls.return_value
        client.provision.return_value = {"session_id": "sess_fail2", "status": "provisioning"}
        client.get_session.return_value = {"status": "error"}

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

        assert result.exit_code == 1
        assert "unknown error" in result.output

    @patch(_DETECT_VENV, return_value=_FAKE_VENV)
    @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_install_broker_error_shows_message(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        _ssh_key: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        _detect_venv: MagicMock,
        runner: CliRunner,
    ) -> None:
        from specter_cli.broker_client import BrokerError

        client = mock_broker_cls.return_value
        client.provision.side_effect = BrokerError(
            400, "invalid_gpu_type", "Unknown GPU type 'fake'"
        )

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

        assert result.exit_code == 1
        assert "fake" in result.output
        assert "specter gpus" in result.output
        client.close.assert_called_once()

    @patch(_TIME_SLEEP)
    @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_install_venv_creation_failure_exits(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        _ssh_key: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        _save_config: MagicMock,
        _detect_venv: MagicMock,
        _create_remote_venv: MagicMock,
        _check_py_version: MagicMock,
        _sleep: MagicMock,
        runner: CliRunner,
    ) -> None:
        """create_remote_venv failure should abort install (hard fatal)."""
        client = mock_broker_cls.return_value
        client.provision.return_value = {"session_id": "sess_venv_fail", "status": "provisioning"}
        client.get_session.return_value = {
            "status": "ready",
            "ssh": {"host": "1.2.3.4", "port": 22, "user": "root"},
        }

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

        assert result.exit_code == 1
        assert "Failed to set up GPU environment" in result.output

    @patch(_DETECT_VENV, return_value=_FAKE_VENV)
    @patch(_READ_SSH_KEY)
    @patch(_REQUIRE_AUTH, return_value="sk-test-1234")
    @patch(_HAS_SESSION, return_value=False)
    def test_install_no_ssh_key_exits(
        self,
        _has_session: MagicMock,
        _auth: MagicMock,
        mock_ssh_key: MagicMock,
        _detect_venv: MagicMock,
        runner: CliRunner,
    ) -> None:
        mock_ssh_key.side_effect = FileNotFoundError("No SSH public key found")

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

        assert result.exit_code == 1
        assert "No SSH public key found" in result.output


# ---------------------------------------------------------------------------
# uninstall — auto-detects scope
# ---------------------------------------------------------------------------


class TestUninstallCommand:
    """specter uninstall — tear down active session (auto-detect scope)."""

    @patch(_RESTORE_VENV, return_value=True)
    @patch(_DEREGISTER_SESSION)
    @patch(_CLEAR_CONFIG)
    @patch(_BROKER_URL, return_value="http://test-broker")
    @patch(_BROKER_CLIENT)
    @patch(_REQUIRE_AUTH, return_value="sk-test-1234")
    @patch(
        _LOAD_CONFIG,
        return_value={
            "session_id": "sess_active",
            "gpu_type": "5090",
            "host": "1.2.3.4",
            "venv_path": "/home/user/project/.venv",
        },
    )
    def test_uninstall_venv_scope(
        self,
        _config: MagicMock,
        _auth: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        mock_clear_config: MagicMock,
        mock_deregister: MagicMock,
        mock_restore_venv: MagicMock,
        runner: CliRunner,
        monkeypatch,
    ) -> None:
        """With VIRTUAL_ENV set, uninstall uses venv scope."""
        monkeypatch.setenv("VIRTUAL_ENV", "/home/user/project/.venv")
        client = mock_broker_cls.return_value
        client.destroy_session.return_value = {"session_id": "sess_active", "status": "terminated"}

        result = runner.invoke(cli, ["uninstall"])

        assert result.exit_code == 0, result.output
        assert "uninstalled from" in result.output
        assert "Restored venv" in result.output
        client.destroy_session.assert_called_once_with("sess_active")
        mock_clear_config.assert_called_once_with(
            venv_path="/home/user/project/.venv",
            use_global=False,
        )
        mock_deregister.assert_called_once_with("/home/user/project/.venv")
        mock_restore_venv.assert_called_once_with(Path("/home/user/project/.venv"))
        client.close.assert_called_once()

    @patch(_CLEAR_CONFIG)
    @patch(_BROKER_URL, return_value="http://test-broker")
    @patch(_BROKER_CLIENT)
    @patch(_REQUIRE_AUTH, return_value="sk-test-1234")
    @patch(
        _LOAD_CONFIG,
        return_value={
            "session_id": "sess_global",
            "gpu_type": "h100",
            "host": "1.2.3.4",
        },
    )
    def test_uninstall_global_scope(
        self,
        _config: MagicMock,
        _auth: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        mock_clear_config: MagicMock,
        runner: CliRunner,
        monkeypatch,
    ) -> None:
        """Without VIRTUAL_ENV, uninstall uses global scope."""
        monkeypatch.delenv("VIRTUAL_ENV", raising=False)
        client = mock_broker_cls.return_value
        client.destroy_session.return_value = {"session_id": "sess_global", "status": "terminated"}

        result = runner.invoke(cli, ["uninstall"])

        assert result.exit_code == 0, result.output
        assert "uninstalled from system Python" in result.output
        # Global uninstall: no venv restore.
        assert "Restored venv" not in result.output
        mock_clear_config.assert_called_once_with(
            venv_path=None,
            use_global=True,
        )

    @patch(_LOAD_CONFIG, return_value={})
    def test_uninstall_no_active_session_venv(
        self, _config: MagicMock, runner: CliRunner, monkeypatch
    ) -> None:
        """In venv with no session, shows appropriate message."""
        monkeypatch.setenv("VIRTUAL_ENV", "/home/user/.venv")

        result = runner.invoke(cli, ["uninstall"])

        assert result.exit_code == 0
        assert "No GPU installed" in result.output

    @patch(_LOAD_CONFIG, return_value={})
    def test_uninstall_no_active_session_global(
        self, _config: MagicMock, runner: CliRunner, monkeypatch
    ) -> None:
        """Outside venv with no global session, shows appropriate message."""
        monkeypatch.delenv("VIRTUAL_ENV", raising=False)

        result = runner.invoke(cli, ["uninstall"])

        assert result.exit_code == 0
        assert "No GPU installed globally" in result.output

    @patch(_CLEAR_CONFIG)
    @patch(_BROKER_URL, return_value="http://test-broker")
    @patch(_BROKER_CLIENT)
    @patch(_REQUIRE_AUTH, return_value="sk-test-1234")
    @patch(
        _LOAD_CONFIG,
        return_value={
            "session_id": "sess_fail",
            "gpu_type": "a100",
            "host": "1.2.3.4",
            "venv_path": "/home/user/project/.venv",
        },
    )
    def test_uninstall_broker_error(
        self,
        _config: MagicMock,
        _auth: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        _clear_config: MagicMock,
        runner: CliRunner,
        monkeypatch,
    ) -> None:
        from specter_cli.broker_client import BrokerError

        monkeypatch.setenv("VIRTUAL_ENV", "/home/user/project/.venv")
        client = mock_broker_cls.return_value
        client.destroy_session.side_effect = BrokerError(404, "not_found", "Unknown session")

        result = runner.invoke(cli, ["uninstall"])

        assert result.exit_code == 1
        assert "Broker error" in result.output


# ---------------------------------------------------------------------------
# status — auto-detects scope
# ---------------------------------------------------------------------------


class TestStatusCommand:
    """specter status — show session info (auto-detect scope)."""

    @patch(_BROKER_URL, return_value="http://test-broker")
    @patch(_BROKER_CLIENT)
    @patch(_REQUIRE_AUTH, return_value="sk-test-1234")
    @patch(
        _LOAD_CONFIG,
        return_value={"session_id": "sess_status", "host": "1.2.3.4"},
    )
    def test_status_shows_session_info(
        self,
        _config: MagicMock,
        _auth: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        runner: CliRunner,
        monkeypatch,
    ) -> None:
        monkeypatch.setenv("VIRTUAL_ENV", "/home/user/.venv")
        client = mock_broker_cls.return_value
        client.get_session.return_value = {
            "session_id": "sess_status",
            "status": "ready",
            "gpu_type": "5090",
            "gpu_name": "NVIDIA GeForce RTX 5090",
            "provider": "runpod",
            "ssh": {"host": "1.2.3.4", "port": 43237, "user": "root"},
            "created_at": "2026-03-06T07:00:00Z",
        }

        result = runner.invoke(cli, ["status"])

        assert result.exit_code == 0, result.output
        assert "sess_status" in result.output
        assert "ready" in result.output
        assert "5090" in result.output
        assert "1.2.3.4" in result.output
        assert "43237" in result.output
        assert "Venv" in result.output  # Scope label.
        client.close.assert_called_once()

    @patch(_BROKER_URL, return_value="http://test-broker")
    @patch(_BROKER_CLIENT)
    @patch(_REQUIRE_AUTH, return_value="sk-test-1234")
    @patch(
        _LOAD_CONFIG,
        return_value={"session_id": "sess_global", "host": "1.2.3.4"},
    )
    def test_status_global_scope(
        self,
        _config: MagicMock,
        _auth: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        runner: CliRunner,
        monkeypatch,
    ) -> None:
        """Without VIRTUAL_ENV, status auto-detects global scope."""
        monkeypatch.delenv("VIRTUAL_ENV", raising=False)
        client = mock_broker_cls.return_value
        client.get_session.return_value = {
            "session_id": "sess_global",
            "status": "ready",
            "gpu_type": "h100",
            "provider": "runpod",
        }

        result = runner.invoke(cli, ["status"])

        assert result.exit_code == 0, result.output
        assert "sess_global" in result.output
        assert "Global" in result.output  # Scope label.

    @patch(_LOAD_CONFIG, return_value={})
    def test_status_no_active_session_venv(
        self, _config: MagicMock, runner: CliRunner, monkeypatch
    ) -> None:
        monkeypatch.setenv("VIRTUAL_ENV", "/home/user/.venv")

        result = runner.invoke(cli, ["status"])

        assert result.exit_code == 0
        assert "No GPU installed" in result.output

    @patch(_LOAD_CONFIG, return_value={})
    def test_status_no_active_session_global(
        self, _config: MagicMock, runner: CliRunner, monkeypatch
    ) -> None:
        monkeypatch.delenv("VIRTUAL_ENV", raising=False)

        result = runner.invoke(cli, ["status"])

        assert result.exit_code == 0
        assert "No GPU installed globally" in result.output

    @patch(_BROKER_URL, return_value="http://test-broker")
    @patch(_BROKER_CLIENT)
    @patch(_REQUIRE_AUTH, return_value="sk-test-1234")
    @patch(
        _LOAD_CONFIG,
        return_value={"session_id": "sess_no_ssh", "host": "1.2.3.4"},
    )
    def test_status_provisioning_no_ssh(
        self,
        _config: MagicMock,
        _auth: MagicMock,
        mock_broker_cls: MagicMock,
        _broker_url: MagicMock,
        runner: CliRunner,
        monkeypatch,
    ) -> None:
        monkeypatch.setenv("VIRTUAL_ENV", "/home/user/.venv")
        client = mock_broker_cls.return_value
        client.get_session.return_value = {
            "session_id": "sess_no_ssh",
            "status": "provisioning",
            "gpu_type": "4090",
            "provider": "runpod",
            "created_at": "2026-03-06T07:00:00Z",
        }

        result = runner.invoke(cli, ["status"])

        assert result.exit_code == 0, result.output
        assert "provisioning" in result.output
        # SSH info should not appear.
        assert "SSH Host" not in result.output


# ---------------------------------------------------------------------------
# login / logout
# ---------------------------------------------------------------------------


class TestLoginLogout:
    """specter login / logout commands."""

    @patch("specter_cli.auth.login_with_browser")
    def test_login_calls_browser_flow(self, mock_login: MagicMock, runner: CliRunner) -> None:
        result = runner.invoke(cli, ["login"])

        assert result.exit_code == 0, result.output
        mock_login.assert_called_once()

    @patch("specter_cli.auth.login_with_browser", side_effect=Exception("auth failed"))
    def test_login_no_key_flag(self, mock_login: MagicMock, runner: CliRunner) -> None:
        """--key flag should not exist anymore."""
        result = runner.invoke(cli, ["login", "--key", "sk-test"])
        # Click should reject the unknown option
        assert result.exit_code != 0

    @patch("specter_cli.auth.logout")
    def test_logout(self, mock_logout: MagicMock, runner: CliRunner) -> None:
        result = runner.invoke(cli, ["logout"])

        assert result.exit_code == 0, result.output
        mock_logout.assert_called_once()


# ---------------------------------------------------------------------------
# ssh
# ---------------------------------------------------------------------------

_SSH_BASE_ARGS = "specter_cli.ssh_config.ssh_base_args"


class TestSshCommand:
    """specter ssh — open SSH shell to the remote GPU."""

    @patch(
        _LOAD_CONFIG,
        return_value={
            "host": "1.2.3.4",
            "user": "root",
            "ssh_port": 22,
        },
    )
    @patch(_SSH_BASE_ARGS, return_value=["-o", "StrictHostKeyChecking=no", "root@1.2.3.4"])
    @patch("os.execvp")
    def test_ssh_connects(
        self,
        mock_execvp: MagicMock,
        mock_ssh_base: MagicMock,
        _config: MagicMock,
        runner: CliRunner,
        monkeypatch,
    ) -> None:
        monkeypatch.setenv("VIRTUAL_ENV", "/home/user/.venv")

        result = runner.invoke(cli, ["ssh"])

        assert result.exit_code == 0, result.output
        mock_ssh_base.assert_called_once_with(
            "1.2.3.4", "root", port=22, key_path=None, tty=True
        )
        mock_execvp.assert_called_once_with(
            "ssh", ["ssh", "-o", "StrictHostKeyChecking=no", "root@1.2.3.4"]
        )

    @patch(
        _LOAD_CONFIG,
        return_value={
            "host": "5.6.7.8",
            "user": "ubuntu",
            "ssh_port": 2222,
            "key": "/tmp/mykey",
        },
    )
    @patch(_SSH_BASE_ARGS, return_value=["-o", "StrictHostKeyChecking=no", "ubuntu@5.6.7.8"])
    @patch("os.execvp")
    def test_ssh_custom_port_and_key(
        self,
        mock_execvp: MagicMock,
        mock_ssh_base: MagicMock,
        _config: MagicMock,
        runner: CliRunner,
        monkeypatch,
    ) -> None:
        monkeypatch.setenv("VIRTUAL_ENV", "/home/user/.venv")

        runner.invoke(cli, ["ssh"])

        mock_ssh_base.assert_called_once_with(
            "5.6.7.8", "ubuntu", port=2222, key_path="/tmp/mykey", tty=True
        )

    @patch(_LOAD_CONFIG, return_value={})
    def test_ssh_no_session_venv(
        self, _config: MagicMock, runner: CliRunner, monkeypatch
    ) -> None:
        monkeypatch.setenv("VIRTUAL_ENV", "/home/user/.venv")

        result = runner.invoke(cli, ["ssh"])

        assert result.exit_code != 0
        assert "No GPU installed" in result.output

    @patch(_LOAD_CONFIG, return_value={})
    def test_ssh_no_session_global(
        self, _config: MagicMock, runner: CliRunner, monkeypatch
    ) -> None:
        monkeypatch.delenv("VIRTUAL_ENV", raising=False)

        result = runner.invoke(cli, ["ssh"])

        assert result.exit_code != 0
        assert "No GPU installed globally" in result.output
