"""Tests for ``specter gpus`` subcommand and invalid_gpu_type handling.

Covers:
1. ``specter gpus`` — list available GPU types from broker
2. ``specter install <invalid>`` — friendly error with ``specter gpus`` suggestion
3. BrokerClient.list_gpu_types() — response parsing
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from click.testing import CliRunner

from specter_cli.broker_client import BrokerError
from specter_cli.main import cli

_BROKER_CLIENT = "specter_cli.broker_client.BrokerClient"
_GET_BROKER_URL = "specter_cli.broker_client.get_broker_url"

_SAMPLE_GPU_TYPES = [
    {"name": "h100", "display_name": "NVIDIA H100 80GB HBM3"},
    {"name": "a100", "display_name": "NVIDIA A100 80GB PCIe"},
    {"name": "4090", "display_name": "NVIDIA GeForce RTX 4090"},
    {"name": "l40s", "display_name": "NVIDIA L40S"},
]


# ---------------------------------------------------------------------------
# specter gpus — list available GPU types
# ---------------------------------------------------------------------------


class TestSpecterGpus:
    """Tests for the ``specter gpus`` subcommand."""

    def test_displays_gpu_table(self) -> None:
        """Should display a table of available GPU types."""
        runner = CliRunner()
        with (
            patch(_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.list_gpu_types.return_value = _SAMPLE_GPU_TYPES

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

        assert result.exit_code == 0
        assert "h100" in result.output
        assert "a100" in result.output
        assert "4090" in result.output
        assert "NVIDIA H100 80GB HBM3" in result.output
        assert "specter install" in result.output

    def test_empty_list(self) -> None:
        """Should show a message when no GPU types are available."""
        runner = CliRunner()
        with (
            patch(_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.list_gpu_types.return_value = []

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

        assert result.exit_code == 0
        assert "no gpu types" in result.output.lower()

    def test_broker_error(self) -> None:
        """Should show error on broker failure."""
        runner = CliRunner()
        with (
            patch(_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.list_gpu_types.side_effect = BrokerError(
                500, "internal_error", "Something broke"
            )

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

        assert result.exit_code != 0
        assert "failed to fetch" in result.output.lower()

    def test_network_error(self) -> None:
        """Should show connection error on network failure."""
        runner = CliRunner()
        with (
            patch(_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.list_gpu_types.side_effect = ConnectionError("unreachable")

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

        assert result.exit_code != 0
        assert "could not reach" in result.output.lower()

    def test_closes_client(self) -> None:
        """Client should be closed after the call."""
        runner = CliRunner()
        with (
            patch(_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.list_gpu_types.return_value = _SAMPLE_GPU_TYPES

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

        mock_client.close.assert_called_once()


# ---------------------------------------------------------------------------
# specter install <invalid_gpu> — invalid_gpu_type error
# ---------------------------------------------------------------------------


class TestInvalidGpuTypeError:
    """Provision returning invalid_gpu_type should suggest ``specter gpus``."""

    def test_invalid_gpu_type_message(self) -> None:
        """Should show the invalid type name and suggest specter gpus."""
        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(_GET_BROKER_URL, return_value="https://broker.test"),
            patch(_BROKER_CLIENT) as mock_cls,
        ):
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.provision.side_effect = BrokerError(
                400, "invalid_gpu_type", "GPU type 'banana' is not available"
            )

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

        assert result.exit_code != 0
        assert "banana" in result.output
        assert "specter gpus" in result.output

    def test_invalid_gpu_type_with_various_names(self) -> None:
        """Should include the exact GPU name the user typed."""
        runner = CliRunner()
        for bad_name in ["v100", "titan", "xxx"]:
            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(_GET_BROKER_URL, return_value="https://broker.test"),
                patch(_BROKER_CLIENT) as mock_cls,
            ):
                mock_client = MagicMock()
                mock_cls.return_value = mock_client
                mock_client.provision.side_effect = BrokerError(
                    400, "invalid_gpu_type", f"GPU type '{bad_name}' is not available"
                )

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

            assert result.exit_code != 0
            assert bad_name in result.output
            assert "specter gpus" in result.output


# ---------------------------------------------------------------------------
# BrokerClient.list_gpu_types() unit tests
# ---------------------------------------------------------------------------


class TestBrokerClientListGpuTypes:
    """Unit tests for BrokerClient.list_gpu_types()."""

    def test_parses_gpu_types_response(self) -> None:
        """Should extract gpu_types list from response."""
        from specter_cli.broker_client import BrokerClient

        with patch("specter_cli.broker_client.httpx.Client") as mock_httpx:
            mock_response = MagicMock()
            mock_response.status_code = 200
            mock_response.json.return_value = {"gpu_types": _SAMPLE_GPU_TYPES}
            mock_httpx.return_value.get.return_value = mock_response

            client = BrokerClient("", base_url="https://broker.test")
            result = client.list_gpu_types()

        assert result == _SAMPLE_GPU_TYPES
        assert len(result) == 4

    def test_empty_gpu_types(self) -> None:
        """Should return empty list when no GPU types."""
        from specter_cli.broker_client import BrokerClient

        with patch("specter_cli.broker_client.httpx.Client") as mock_httpx:
            mock_response = MagicMock()
            mock_response.status_code = 200
            mock_response.json.return_value = {"gpu_types": []}
            mock_httpx.return_value.get.return_value = mock_response

            client = BrokerClient("", base_url="https://broker.test")
            result = client.list_gpu_types()

        assert result == []

    def test_missing_gpu_types_key(self) -> None:
        """Should return empty list when response lacks gpu_types key."""
        from specter_cli.broker_client import BrokerClient

        with patch("specter_cli.broker_client.httpx.Client") as mock_httpx:
            mock_response = MagicMock()
            mock_response.status_code = 200
            mock_response.json.return_value = {}
            mock_httpx.return_value.get.return_value = mock_response

            client = BrokerClient("", base_url="https://broker.test")
            result = client.list_gpu_types()

        assert result == []
