"""Unit tests for VS Code extension notifications."""

from __future__ import annotations

import json
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from gfp_mcp.notify import (
    _resolve_ws_port,
    notify_extension,
    notify_open_simulation,
    notify_show_check_results,
    notify_show_gds,
)
from gfp_mcp.registry import ServerInfo, ServerRegistry

# --- Registry ws_port tests ---


class TestServerInfoWsPort:
    """Tests for ws_port support in ServerInfo."""

    def test_ws_port_default_none(self) -> None:
        info = ServerInfo(port=8787, pid=1234, project_path="/p", project_name="proj")
        assert info.ws_port is None

    def test_ws_port_set(self) -> None:
        info = ServerInfo(
            port=8787, pid=1234, project_path="/p", project_name="proj", ws_port=7777
        )
        assert info.ws_port == 7777

    def test_ws_port_to_dict_present(self) -> None:
        info = ServerInfo(
            port=8787, pid=1234, project_path="/p", project_name="proj", ws_port=7777
        )
        d = info.to_dict()
        assert d["ws_port"] == 7777

    def test_ws_port_to_dict_absent(self) -> None:
        info = ServerInfo(port=8787, pid=1234, project_path="/p", project_name="proj")
        d = info.to_dict()
        assert "ws_port" not in d

    def test_ws_port_from_dict_present(self) -> None:
        data = {
            "port": 8787,
            "pid": 1234,
            "project_path": "/p",
            "project_name": "proj",
            "ws_port": 7777,
        }
        info = ServerInfo.from_dict(data)
        assert info.ws_port == 7777

    def test_ws_port_from_dict_absent(self) -> None:
        data = {
            "port": 8787,
            "pid": 1234,
            "project_path": "/p",
            "project_name": "proj",
        }
        info = ServerInfo.from_dict(data)
        assert info.ws_port is None

    def test_ws_port_roundtrip(self) -> None:
        info = ServerInfo(
            port=8787, pid=1234, project_path="/p", project_name="proj", ws_port=7777
        )
        restored = ServerInfo.from_dict(info.to_dict())
        assert restored.ws_port == 7777

    def test_ws_port_from_registry_file(self) -> None:
        """Test reading ws_port from a registry JSON file."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            temp_path = Path(f.name)
            json.dump(
                {
                    "servers": {
                        "8787": {
                            "port": 8787,
                            "pid": 99999,
                            "project_path": "/path/to/proj",
                            "project_name": "proj",
                            "ws_port": 7777,
                        }
                    }
                },
                f,
            )

        try:
            registry = ServerRegistry(registry_path=temp_path)
            servers = registry.list_servers(include_dead=True)
            assert len(servers) == 1
            assert servers[0].ws_port == 7777
        finally:
            temp_path.unlink()


# --- _resolve_ws_port tests ---


class TestResolveWsPort:
    """Tests for _resolve_ws_port."""

    def test_resolve_by_project_name(self) -> None:
        registry = MagicMock(spec=ServerRegistry)
        server = MagicMock(spec=ServerInfo)
        server.ws_port = 7777
        registry.get_server_by_project.return_value = server

        assert _resolve_ws_port("my-project", registry) == 7777
        registry.get_server_by_project.assert_called_once_with("my-project")

    def test_resolve_single_server_no_project(self) -> None:
        registry = MagicMock(spec=ServerRegistry)
        server = MagicMock(spec=ServerInfo)
        server.ws_port = 7777
        registry.list_servers.return_value = [server]

        assert _resolve_ws_port(None, registry) == 7777

    def test_resolve_multiple_servers_no_project(self) -> None:
        registry = MagicMock(spec=ServerRegistry)
        registry.list_servers.return_value = [MagicMock(), MagicMock()]

        assert _resolve_ws_port(None, registry) is None

    def test_resolve_no_servers(self) -> None:
        registry = MagicMock(spec=ServerRegistry)
        registry.list_servers.return_value = []

        assert _resolve_ws_port(None, registry) is None

    def test_resolve_project_not_found(self) -> None:
        registry = MagicMock(spec=ServerRegistry)
        registry.get_server_by_project.return_value = None

        assert _resolve_ws_port("unknown", registry) is None

    def test_resolve_ws_port_none_on_server(self) -> None:
        registry = MagicMock(spec=ServerRegistry)
        server = MagicMock(spec=ServerInfo)
        server.ws_port = None
        registry.get_server_by_project.return_value = server

        assert _resolve_ws_port("proj", registry) is None


# --- notify_extension tests ---


class TestNotifyExtension:
    """Tests for notify_extension."""

    @pytest.mark.anyio
    async def test_no_ws_port_returns_false(self) -> None:
        registry = MagicMock(spec=ServerRegistry)
        registry.list_servers.return_value = []

        result = await notify_extension(None, {"what": "showGds"}, registry)
        assert result is False

    @pytest.mark.anyio
    async def test_successful_send(self) -> None:
        registry = MagicMock(spec=ServerRegistry)
        server = MagicMock(spec=ServerInfo)
        server.ws_port = 7777
        registry.list_servers.return_value = [server]

        mock_ws = AsyncMock()
        mock_connect = AsyncMock()
        mock_connect.__aenter__ = AsyncMock(return_value=mock_ws)
        mock_connect.__aexit__ = AsyncMock(return_value=False)

        with patch("gfp_mcp.notify.websockets.connect", return_value=mock_connect):
            result = await notify_extension(
                None, {"what": "showGds", "gds": "/path/to/file.gds"}, registry
            )

        assert result is True
        mock_ws.send.assert_called_once()
        sent_msg = json.loads(mock_ws.send.call_args[0][0])
        assert sent_msg["what"] == "showGds"
        assert sent_msg["gds"] == "/path/to/file.gds"

    @pytest.mark.anyio
    async def test_connection_failure_returns_false(self) -> None:
        registry = MagicMock(spec=ServerRegistry)
        server = MagicMock(spec=ServerInfo)
        server.ws_port = 7777
        registry.list_servers.return_value = [server]

        with patch(
            "gfp_mcp.notify.websockets.connect", side_effect=ConnectionRefusedError
        ):
            result = await notify_extension(None, {"what": "showGds"}, registry)

        assert result is False

    @pytest.mark.anyio
    async def test_creates_default_registry_when_none_provided(self) -> None:
        """Test that passing no registry doesn't crash (creates default registry)."""
        # Mock ServerRegistry to return no servers
        with patch("gfp_mcp.registry.ServerRegistry") as mock_registry_class:
            mock_registry = MagicMock(spec=ServerRegistry)
            mock_registry.list_servers.return_value = []
            mock_registry_class.return_value = mock_registry

            # With no WS servers in registry, this should return False gracefully
            result = await notify_extension(None, {"what": "showGds"})
            assert result is False


# --- notify_show_gds tests ---


class TestNotifyShowGds:
    """Tests for notify_show_gds."""

    @pytest.mark.anyio
    async def test_sends_one_message_per_cell(self) -> None:
        with patch("gfp_mcp.notify.notify_extension", new_callable=AsyncMock) as mock:
            mock.return_value = True
            await notify_show_gds(None, ["cell_a", "cell_b"], "/proj")

        assert mock.call_count == 2
        msg1 = mock.call_args_list[0][0][1]
        msg2 = mock.call_args_list[1][0][1]
        assert msg1 == {"what": "showGds", "gds": "/proj/build/gds/cell_a.gds"}
        assert msg2 == {"what": "showGds", "gds": "/proj/build/gds/cell_b.gds"}

    @pytest.mark.anyio
    async def test_empty_cells_sends_nothing(self) -> None:
        with patch("gfp_mcp.notify.notify_extension", new_callable=AsyncMock) as mock:
            await notify_show_gds(None, [], "/proj")

        mock.assert_not_called()


# --- notify_open_simulation tests ---


class TestNotifyOpenSimulation:
    """Tests for notify_open_simulation."""

    @pytest.mark.anyio
    async def test_sends_correct_message(self) -> None:
        with patch("gfp_mcp.notify.notify_extension", new_callable=AsyncMock) as mock:
            mock.return_value = True
            await notify_open_simulation("proj", "mzi", "sim-123")

        mock.assert_called_once_with(
            "proj",
            {
                "what": "openSimulation",
                "name": "mzi",
                "simulationId": "sim-123",
            },
            None,
        )

    @pytest.mark.anyio
    async def test_simulation_id_none(self) -> None:
        with patch("gfp_mcp.notify.notify_extension", new_callable=AsyncMock) as mock:
            mock.return_value = True
            await notify_open_simulation("proj", "mzi", None)

        msg = mock.call_args[0][1]
        assert msg["simulationId"] is None


# --- notify_show_check_results tests ---


class TestNotifyShowCheckResults:
    """Tests for notify_show_check_results."""

    @pytest.mark.anyio
    @pytest.mark.parametrize("check_type", ["DRC", "Connectivity", "LVS"])
    async def test_sends_correct_message(self, check_type: str) -> None:
        with patch("gfp_mcp.notify.notify_extension", new_callable=AsyncMock) as mock:
            mock.return_value = True
            await notify_show_check_results("proj", check_type, "my_cell")

        mock.assert_called_once_with(
            "proj",
            {
                "what": "showCheckResults",
                "checkType": check_type,
                "cellName": "my_cell",
            },
            None,
        )


# --- Config UI_NOTIFICATIONS tests ---


class TestUINotificationsConfig:
    """Tests for UI_NOTIFICATIONS config setting."""

    def test_default_is_true(self) -> None:
        from gfp_mcp.config import MCPConfig

        assert MCPConfig.UI_NOTIFICATIONS is True

    def test_env_var_false(self) -> None:
        with patch.dict("os.environ", {"GFP_MCP_UI_NOTIFICATIONS": "false"}):
            # MCPConfig is evaluated at import time, so we need to reload
            import importlib

            import gfp_mcp.config

            importlib.reload(gfp_mcp.config)
            assert gfp_mcp.config.MCPConfig.UI_NOTIFICATIONS is False

            # Restore
            importlib.reload(gfp_mcp.config)

    def test_env_var_0(self) -> None:
        with patch.dict("os.environ", {"GFP_MCP_UI_NOTIFICATIONS": "0"}):
            import importlib

            import gfp_mcp.config

            importlib.reload(gfp_mcp.config)
            assert gfp_mcp.config.MCPConfig.UI_NOTIFICATIONS is False

            importlib.reload(gfp_mcp.config)


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
