"""Tests for CLI usage limit error handling.

Covers:
1. Provision-time 403 — friendly messages for usage_limit_reached and account_blocked
2. Post-SSH-255 — _get_termination_reason queries broker for reason
3. specter status — termination_reason displayed when session is terminated
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from specter_cli.broker_client import BrokerError

# ---------------------------------------------------------------------------
# Provision-time error handling (main.py cmd_install)
# ---------------------------------------------------------------------------

_BROKER_CLIENT = "specter_cli.broker_client.BrokerClient"
_BROKER_ERROR = "specter_cli.main.BrokerError"
_GET_BROKER_URL = "specter_cli.broker_client.get_broker_url"


class TestProvisionUsageLimitError:
    """BrokerError with error='usage_limit_reached' should show friendly message."""

    def test_usage_limit_reached_message(self, capsys: pytest.CaptureFixture[str]) -> None:
        """Provision returning 403/usage_limit_reached shows GPU usage limit message."""
        from click.testing import CliRunner

        from specter_cli.main import cli

        runner = CliRunner()
        with (
            patch("specter_cli.auth.require_auth", return_value="sk-test"),
            patch("specter_cli.auth.read_ssh_public_key", return_value="ssh-ed25519 AAAA"),
            patch("specter_cli.venv.detect_venv", return_value="/tmp/fakevenv"),
            patch("specter_cli.config.has_active_session", return_value=False),
            patch("specter_cli.broker_client.get_broker_url", return_value="https://broker.test"),
            patch("specter_cli.broker_client.BrokerClient") as mock_client_cls,
        ):
            mock_client = MagicMock()
            mock_client_cls.return_value = mock_client
            mock_client.provision.side_effect = BrokerError(
                403, "usage_limit_reached", "GPU usage limit reached"
            )

            result = runner.invoke(cli, ["install", "h100"], env={"VIRTUAL_ENV": "/tmp/fakevenv"})

        assert result.exit_code != 0
        assert "usage limit reached" in result.output.lower()
        assert "dashboard" in result.output.lower() or "admin" in result.output.lower()

    def test_account_blocked_message(self) -> None:
        """Provision returning 403/account_blocked shows suspension message."""
        from click.testing import CliRunner

        from specter_cli.main import cli

        runner = CliRunner()
        with (
            patch("specter_cli.auth.require_auth", return_value="sk-test"),
            patch("specter_cli.auth.read_ssh_public_key", return_value="ssh-ed25519 AAAA"),
            patch("specter_cli.venv.detect_venv", return_value="/tmp/fakevenv"),
            patch("specter_cli.config.has_active_session", return_value=False),
            patch("specter_cli.broker_client.get_broker_url", return_value="https://broker.test"),
            patch("specter_cli.broker_client.BrokerClient") as mock_client_cls,
        ):
            mock_client = MagicMock()
            mock_client_cls.return_value = mock_client
            mock_client.provision.side_effect = BrokerError(
                403, "account_blocked", "Account suspended"
            )

            result = runner.invoke(cli, ["install", "h100"], env={"VIRTUAL_ENV": "/tmp/fakevenv"})

        assert result.exit_code != 0
        assert "suspended" in result.output.lower()
        assert "admin" in result.output.lower()

    def test_concurrent_limit_reached_message(self) -> None:
        """Provision returning 403/concurrent_limit_reached shows session limit message."""
        from click.testing import CliRunner

        from specter_cli.main import cli

        runner = CliRunner()
        with (
            patch("specter_cli.auth.require_auth", return_value="sk-test"),
            patch("specter_cli.auth.read_ssh_public_key", return_value="ssh-ed25519 AAAA"),
            patch("specter_cli.venv.detect_venv", return_value="/tmp/fakevenv"),
            patch("specter_cli.config.has_active_session", return_value=False),
            patch("specter_cli.broker_client.get_broker_url", return_value="https://broker.test"),
            patch("specter_cli.broker_client.BrokerClient") as mock_client_cls,
        ):
            mock_client = MagicMock()
            mock_client_cls.return_value = mock_client
            mock_client.provision.side_effect = BrokerError(
                403, "concurrent_limit_reached", "Concurrent sessions exceeded"
            )

            result = runner.invoke(cli, ["install", "h100"], env={"VIRTUAL_ENV": "/tmp/fakevenv"})

        assert result.exit_code != 0
        assert "concurrent" in result.output.lower() or "too many" in result.output.lower()
        assert "specter uninstall" in result.output.lower()

    def test_generic_broker_error_passthrough(self) -> None:
        """Unknown BrokerError types should still display the error message."""
        from click.testing import CliRunner

        from specter_cli.main import cli

        runner = CliRunner()
        with (
            patch("specter_cli.auth.require_auth", return_value="sk-test"),
            patch("specter_cli.auth.read_ssh_public_key", return_value="ssh-ed25519 AAAA"),
            patch("specter_cli.venv.detect_venv", return_value="/tmp/fakevenv"),
            patch("specter_cli.config.has_active_session", return_value=False),
            patch("specter_cli.broker_client.get_broker_url", return_value="https://broker.test"),
            patch("specter_cli.broker_client.BrokerClient") as mock_client_cls,
        ):
            mock_client = MagicMock()
            mock_client_cls.return_value = mock_client
            mock_client.provision.side_effect = BrokerError(
                500, "internal_error", "Something went wrong"
            )

            result = runner.invoke(cli, ["install", "h100"], env={"VIRTUAL_ENV": "/tmp/fakevenv"})

        assert result.exit_code != 0
        assert "Something went wrong" in result.output


# ---------------------------------------------------------------------------
# Post-SSH-255 termination reason (_get_termination_reason in proxy.py)
# ---------------------------------------------------------------------------

_GET_TOKEN = "specter_cli.config.get_token"
_PROXY_BROKER_CLIENT = "specter_cli.broker_client.BrokerClient"
_PROXY_GET_BROKER_URL = "specter_cli.broker_client.get_broker_url"


class TestGetTerminationReason:
    """Test _get_termination_reason() in proxy.py."""

    def test_returns_usage_limit(self) -> None:
        """Should return 'usage_limit' when broker reports it."""
        from specter_cli.proxy import _get_termination_reason

        with (
            patch(_GET_TOKEN, return_value="sk-test"),
            patch(_PROXY_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_PROXY_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.get_session.return_value = {
                "status": "terminated",
                "termination_reason": "usage_limit",
            }

            reason = _get_termination_reason({"session_id": "sess_123"})

        assert reason == "usage_limit"

    def test_returns_admin_block(self) -> None:
        """Should return 'admin_block' when broker reports it."""
        from specter_cli.proxy import _get_termination_reason

        with (
            patch(_GET_TOKEN, return_value="sk-test"),
            patch(_PROXY_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_PROXY_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.get_session.return_value = {
                "status": "terminated",
                "termination_reason": "admin_block",
            }

            reason = _get_termination_reason({"session_id": "sess_123"})

        assert reason == "admin_block"

    def test_returns_none_for_active_session(self) -> None:
        """Active session should return None (not terminated)."""
        from specter_cli.proxy import _get_termination_reason

        with (
            patch(_GET_TOKEN, return_value="sk-test"),
            patch(_PROXY_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_PROXY_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.get_session.return_value = {
                "status": "ready",
                "termination_reason": None,
            }

            reason = _get_termination_reason({"session_id": "sess_123"})

        assert reason is None

    def test_returns_none_when_no_session_id(self) -> None:
        """Missing session_id in config should return None."""
        from specter_cli.proxy import _get_termination_reason

        assert _get_termination_reason({}) is None
        assert _get_termination_reason({"host": "1.2.3.4"}) is None

    def test_returns_none_when_no_token(self) -> None:
        """No auth token should return None (non-fatal)."""
        from specter_cli.proxy import _get_termination_reason

        with patch(_GET_TOKEN, return_value=None):
            reason = _get_termination_reason({"session_id": "sess_123"})

        assert reason is None

    def test_returns_none_on_broker_error(self) -> None:
        """Broker errors should return None (non-fatal, fall back to generic)."""
        from specter_cli.proxy import _get_termination_reason

        with (
            patch(_GET_TOKEN, return_value="sk-test"),
            patch(_PROXY_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_PROXY_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.get_session.side_effect = BrokerError(
                404, "not_found", "Session not found"
            )

            reason = _get_termination_reason({"session_id": "sess_old"})

        assert reason is None

    def test_returns_none_on_network_error(self) -> None:
        """Network errors should return None (non-fatal)."""
        from specter_cli.proxy import _get_termination_reason

        with (
            patch(_GET_TOKEN, return_value="sk-test"),
            patch(_PROXY_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_PROXY_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.get_session.side_effect = ConnectionError("broker unreachable")

            reason = _get_termination_reason({"session_id": "sess_123"})

        assert reason is None

    def test_closes_client(self) -> None:
        """Client should be closed even on success."""
        from specter_cli.proxy import _get_termination_reason

        with (
            patch(_GET_TOKEN, return_value="sk-test"),
            patch(_PROXY_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_PROXY_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.get_session.return_value = {
                "status": "terminated",
                "termination_reason": "idle_timeout",
            }

            _get_termination_reason({"session_id": "sess_123"})

        mock_client.close.assert_called_once()


# ---------------------------------------------------------------------------
# SSH-255 exit code with termination reason (proxy.py run_remote)
# ---------------------------------------------------------------------------

_EXEC_SSH = "specter_cli.proxy.exec_ssh_passthrough"
_LOAD_CONFIG = "specter_cli.proxy.load_remote_config"
_FIND_ROOT = "specter_cli.proxy.find_project_root"
_ENSURE_DIR = "specter_cli.proxy._ensure_remote_dir"
_RSYNC = "specter_cli.proxy.rsync_workspace"
_WORKSPACE_HASH = "specter_cli.proxy.workspace_hash"
_GET_REASON = "specter_cli.proxy._get_termination_reason"

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


class TestRunRemoteTerminationMessages:
    """run_remote() should show specific messages based on termination_reason after SSH 255."""

    def _run_with_exit_code(self, exit_code: int, reason: str | None, tmp_path) -> str:
        """Helper: run run_remote with a mocked SSH process and capture stderr."""
        from specter_cli.proxy import run_remote

        project_dir = tmp_path / "project"
        project_dir.mkdir()
        (project_dir / ".git").mkdir()

        with (
            patch(_WORKSPACE_HASH, return_value=None),
            patch(_LOAD_CONFIG, return_value=_SAMPLE_CONFIG),
            patch(_FIND_ROOT, return_value=project_dir),
            patch(_ENSURE_DIR),
            patch(_RSYNC),
            patch(_EXEC_SSH, return_value=exit_code),
            patch(_GET_REASON, return_value=reason),
            patch("specter_cli.proxy.Path.cwd", return_value=project_dir),
            patch("specter_cli.proxy.sys.stdin") as mock_stdin,
        ):
            mock_stdin.isatty.return_value = False

            import io
            import sys

            captured = io.StringIO()
            old_stderr = sys.stderr
            sys.stderr = captured
            try:
                run_remote(["python", "train.py"])
            finally:
                sys.stderr = old_stderr

            return captured.getvalue()

    def test_usage_limit_message(self, tmp_path) -> None:
        """SSH 255 + usage_limit reason should show limit-specific message."""
        output = self._run_with_exit_code(255, "usage_limit", tmp_path)
        assert "usage limit" in output.lower()

    def test_admin_block_message(self, tmp_path) -> None:
        """SSH 255 + admin_block reason should show suspension message."""
        output = self._run_with_exit_code(255, "admin_block", tmp_path)
        assert "suspended" in output.lower()

    def test_admin_pause_message(self, tmp_path) -> None:
        """SSH 255 + admin_pause reason should show pause message."""
        output = self._run_with_exit_code(255, "admin_pause", tmp_path)
        assert "paused" in output.lower()

    def test_idle_timeout_message(self, tmp_path) -> None:
        """SSH 255 + idle_timeout reason should show timeout message."""
        output = self._run_with_exit_code(255, "idle_timeout", tmp_path)
        assert "idle timeout" in output.lower()

    def test_generic_message_on_none_reason(self, tmp_path) -> None:
        """SSH 255 with no reason should fall back to generic message."""
        output = self._run_with_exit_code(255, None, tmp_path)
        assert "connection to gpu lost" in output.lower()

    def test_no_message_on_normal_exit(self, tmp_path) -> None:
        """Normal exit codes should not show any termination message."""
        output = self._run_with_exit_code(0, None, tmp_path)
        assert "connection to gpu lost" not in output.lower()
        assert "usage limit" not in output.lower()
