"""Tests for the ``specter list`` command.

Covers:
- No active sessions (empty table, message)
- Venv-only sessions (from sessions.json registry)
- Global-only session (from ~/.specter/remote.json)
- Mixed venv + global sessions
- $HOME replacement with ~ in venv paths
- No auth required (pure local file reads)
"""

from __future__ import annotations

from unittest.mock import patch

import pytest
from click.testing import CliRunner

from specter_cli.main import cli

_LIST_SESSIONS = "specter_cli.config.list_sessions"
_LOAD_CONFIG = "specter_cli.config.load_remote_config"


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


class TestListNoSessions:
    """specter list when no sessions are active."""

    @patch(_LOAD_CONFIG, return_value={})
    @patch(_LIST_SESSIONS, return_value=[])
    def test_empty_shows_message(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0, result.output
        assert "No active sessions" in result.output

    @patch(_LOAD_CONFIG, return_value={"gpu_type": "h100"})
    @patch(_LIST_SESSIONS, return_value=[])
    def test_global_config_without_session_id_treated_as_empty(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        """Global config missing session_id should not show a row."""
        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0, result.output
        assert "No active sessions" in result.output


class TestListVenvSessions:
    """specter list with venv-scoped sessions only."""

    @patch(_LOAD_CONFIG, return_value={})
    @patch(
        _LIST_SESSIONS,
        return_value=[
            {
                "venv_path": "/home/user/project/.venv",
                "session_id": "sess_001",
                "gpu_type": "h100",
            }
        ],
    )
    def test_single_venv_session(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0, result.output
        assert "sess_001" in result.output
        assert "h100" in result.output

    @patch(_LOAD_CONFIG, return_value={})
    @patch(
        _LIST_SESSIONS,
        return_value=[
            {
                "venv_path": "/home/user/project1/.venv",
                "session_id": "sess_001",
                "gpu_type": "h100",
            },
            {
                "venv_path": "/home/user/project2/.venv",
                "session_id": "sess_002",
                "gpu_type": "a100",
            },
            {
                "venv_path": "/home/user/project3/.venv",
                "session_id": "sess_003",
                "gpu_type": "4090",
            },
        ],
    )
    def test_multiple_venv_sessions(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0, result.output
        assert "sess_001" in result.output
        assert "sess_002" in result.output
        assert "sess_003" in result.output
        assert "h100" in result.output
        assert "a100" in result.output
        assert "4090" in result.output


class TestListGlobalSession:
    """specter list with global (system Python) session."""

    @patch(
        _LOAD_CONFIG,
        return_value={
            "session_id": "sess_global",
            "gpu_type": "h100",
        },
    )
    @patch(_LIST_SESSIONS, return_value=[])
    def test_global_session_shown(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0, result.output
        assert "sess_global" in result.output
        assert "h100" in result.output
        assert "(global)" in result.output


class TestListMixed:
    """specter list with both venv and global sessions."""

    @patch(
        _LOAD_CONFIG,
        return_value={
            "session_id": "sess_global",
            "gpu_type": "a100",
        },
    )
    @patch(
        _LIST_SESSIONS,
        return_value=[
            {
                "venv_path": "/home/user/project/.venv",
                "session_id": "sess_venv",
                "gpu_type": "h100",
            }
        ],
    )
    def test_mixed_venv_and_global(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0, result.output
        assert "sess_venv" in result.output
        assert "h100" in result.output
        assert "sess_global" in result.output
        assert "a100" in result.output
        assert "(global)" in result.output


class TestListPathShortening:
    """Verify $HOME → ~ replacement in venv paths."""

    @patch(_LOAD_CONFIG, return_value={})
    @patch(_LIST_SESSIONS, return_value=[])
    def test_home_replaced_with_tilde(
        self,
        mock_list: object,
        _config: object,
        runner: CliRunner,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Venv paths under $HOME should display with ~ prefix."""
        import os

        home = os.path.expanduser("~")
        mock_list.return_value = [
            {
                "venv_path": f"{home}/myproject/.venv",
                "session_id": "sess_home",
                "gpu_type": "4090",
            }
        ]

        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0, result.output
        assert "~/myproject/.venv" in result.output
        assert "sess_home" in result.output

    @patch(_LOAD_CONFIG, return_value={})
    @patch(
        _LIST_SESSIONS,
        return_value=[
            {
                "venv_path": "/opt/projects/ml/.venv",
                "session_id": "sess_opt",
                "gpu_type": "l40s",
            }
        ],
    )
    def test_non_home_path_unchanged(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        """Paths outside $HOME should not be modified."""
        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0, result.output
        assert "/opt/projects/ml/.venv" in result.output


class TestListNoAuth:
    """specter list should NOT require authentication."""

    @patch(_LOAD_CONFIG, return_value={})
    @patch(_LIST_SESSIONS, return_value=[])
    def test_no_auth_called(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        """list command should never call require_auth."""
        with patch("specter_cli.auth.require_auth") as mock_auth:
            result = runner.invoke(cli, ["list"])
            assert result.exit_code == 0, result.output
            mock_auth.assert_not_called()

    @patch(
        _LOAD_CONFIG,
        return_value={"session_id": "sess_noauth", "gpu_type": "h100"},
    )
    @patch(
        _LIST_SESSIONS,
        return_value=[
            {
                "venv_path": "/home/user/.venv",
                "session_id": "sess_venv",
                "gpu_type": "a100",
            }
        ],
    )
    def test_works_without_auth_token(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        """Even with sessions, no auth token should be needed."""
        with patch("specter_cli.auth.require_auth") as mock_auth:
            result = runner.invoke(cli, ["list"])
            assert result.exit_code == 0, result.output
            mock_auth.assert_not_called()


class TestListMissingFields:
    """Edge cases: sessions with missing or partial fields."""

    @patch(_LOAD_CONFIG, return_value={})
    @patch(
        _LIST_SESSIONS,
        return_value=[
            {
                "venv_path": "/home/user/.venv",
                "session_id": "sess_partial",
                # Missing gpu_type
            }
        ],
    )
    def test_missing_gpu_type_shows_dash(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        """Missing gpu_type should show a dash or default, not crash."""
        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0, result.output
        assert "sess_partial" in result.output

    @patch(_LOAD_CONFIG, return_value={})
    @patch(
        _LIST_SESSIONS,
        return_value=[
            {
                # Missing venv_path
                "session_id": "sess_nopath",
                "gpu_type": "h100",
            }
        ],
    )
    def test_missing_venv_path_shows_dash(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        """Missing venv_path should show a dash or default, not crash."""
        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0, result.output
        assert "sess_nopath" in result.output


class TestListLocalOnly:
    """specter list should be pure local — no broker queries."""

    @patch(_LOAD_CONFIG, return_value={})
    @patch(_LIST_SESSIONS, return_value=[])
    def test_no_broker_client_instantiated(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        """BrokerClient should never be created by the list command."""
        with patch("specter_cli.broker_client.BrokerClient") as mock_broker:
            result = runner.invoke(cli, ["list"])
            assert result.exit_code == 0, result.output
            mock_broker.assert_not_called()

    @patch(
        _LOAD_CONFIG,
        return_value={"session_id": "sess_local", "gpu_type": "h100"},
    )
    @patch(
        _LIST_SESSIONS,
        return_value=[
            {
                "venv_path": "/home/user/.venv",
                "session_id": "sess_v1",
                "gpu_type": "4090",
            }
        ],
    )
    def test_no_network_calls_with_sessions(
        self,
        _list: object,
        _config: object,
        runner: CliRunner,
    ) -> None:
        """Even with active sessions, list should make zero network calls."""
        with patch("specter_cli.broker_client.BrokerClient") as mock_broker:
            result = runner.invoke(cli, ["list"])
            assert result.exit_code == 0, result.output
            mock_broker.assert_not_called()


class TestListLoadConfigScope:
    """Verify list uses the correct config scope."""

    @patch(_LIST_SESSIONS, return_value=[])
    def test_load_config_called_with_use_global(
        self,
        _list: object,
        runner: CliRunner,
    ) -> None:
        """list should call load_remote_config with use_global=True to get global session."""
        with patch(_LOAD_CONFIG, return_value={}) as mock_config:
            result = runner.invoke(cli, ["list"])
            assert result.exit_code == 0, result.output
            mock_config.assert_called_once_with(use_global=True)
