"""
Tests for runtime/adapters/websocket.py — WebSocketAdapter.

Covers:
    - introspect returns the three WebSocket tools (connect, send_message, listen)
    - execute routes to the correct async operation via _run_tool
    - execute returns error dict for unknown tool names
    - send_message requires a message parameter
    - send/listen raise when not connected
    - connect wires through to websockets.connect
    - send_message wires through to connection.send
    - listen collects messages up to count, respects timeout
"""

from __future__ import annotations

import asyncio
from typing import Any, Dict, List
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from runtime.adapters.websocket import WebSocketAdapter


# ── Fixtures ─────────────────────────────────────────────────────────────


@pytest.fixture(autouse=True)
def _event_loop():
    """Ensure an event loop exists for the thread (Python 3.12+ removed the
    implicit one). The adapter's _run_tool calls get_event_loop().run_until_complete."""
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    yield
    loop.close()
    asyncio.set_event_loop(None)


@pytest.fixture
def adapter() -> WebSocketAdapter:
    """Return a fresh WebSocketAdapter pointed at a dummy URL."""
    return WebSocketAdapter("ws://localhost:9999/test")


# ── Introspect ───────────────────────────────────────────────────────────


class TestIntrospect:
    def test_returns_three_tools(self, adapter: WebSocketAdapter) -> None:
        info = adapter.introspect()
        names = [t["name"] for t in info["tools"]]
        assert names == ["connect", "send_message", "listen"]

    def test_includes_version(self, adapter: WebSocketAdapter) -> None:
        info = adapter.introspect()
        assert "version" in info
        assert isinstance(info["version"], str)

    def test_includes_source_url(self, adapter: WebSocketAdapter) -> None:
        info = adapter.introspect()
        assert info["source"] == "ws://localhost:9999/test"

    def test_tools_are_stable_across_calls(self, adapter: WebSocketAdapter) -> None:
        first = adapter.introspect()
        second = adapter.introspect()
        assert first["tools"] is not second["tools"]  # list copy
        assert first["tools"] == second["tools"]


# ── Execute routing ──────────────────────────────────────────────────────


class TestExecuteRouting:
    def test_unknown_tool_returns_error(self, adapter: WebSocketAdapter) -> None:
        result = adapter.execute("nonexistent", {})
        assert result["status"] == "error"
        assert "Unknown tool" in result["result"]

    def test_connect_routes_to_connect(self, adapter: WebSocketAdapter) -> None:
        mock_conn = AsyncMock()
        fake_ws = MagicMock()
        fake_ws.connect = AsyncMock(return_value=mock_conn)

        with patch("runtime.adapters.websocket._import_websockets", return_value=fake_ws):
            result = adapter.execute("connect", {})

        assert result["status"] == "ok"
        assert result["result"]["connected"] is True
        assert result["result"]["url"] == "ws://localhost:9999/test"
        fake_ws.connect.assert_awaited_once_with("ws://localhost:9999/test")

    def test_send_message_routes_to_send(self, adapter: WebSocketAdapter) -> None:
        mock_conn = AsyncMock()
        fake_ws = MagicMock()
        fake_ws.connect = AsyncMock(return_value=mock_conn)

        with patch("runtime.adapters.websocket._import_websockets", return_value=fake_ws):
            adapter.execute("connect", {})
            result = adapter.execute("send_message", {"message": "hello"})

        assert result["status"] == "ok"
        assert result["result"]["sent"] is True
        assert result["result"]["message"] == "hello"
        mock_conn.send.assert_awaited_once_with("hello")

    def test_listen_routes_to_listen(self, adapter: WebSocketAdapter) -> None:
        mock_conn = AsyncMock()
        mock_conn.recv = AsyncMock(side_effect=["msg1", "msg2"])
        fake_ws = MagicMock()
        fake_ws.connect = AsyncMock(return_value=mock_conn)

        with patch("runtime.adapters.websocket._import_websockets", return_value=fake_ws):
            adapter.execute("connect", {})
            result = adapter.execute("listen", {"count": "2", "timeout": "5"})

        assert result["status"] == "ok"
        assert result["result"] == ["msg1", "msg2"]

    def test_exception_in_tool_returns_error(self, adapter: WebSocketAdapter) -> None:
        fake_ws = MagicMock()
        fake_ws.connect = AsyncMock(side_effect=ConnectionRefusedError("refused"))

        with patch("runtime.adapters.websocket._import_websockets", return_value=fake_ws):
            result = adapter.execute("connect", {})

        assert result["status"] == "error"
        assert "refused" in result["result"]


# ── send_message validation ──────────────────────────────────────────────


class TestSendMessageValidation:
    def test_missing_message_returns_error(self, adapter: WebSocketAdapter) -> None:
        mock_conn = AsyncMock()
        fake_ws = MagicMock()
        fake_ws.connect = AsyncMock(return_value=mock_conn)

        with patch("runtime.adapters.websocket._import_websockets", return_value=fake_ws):
            adapter.execute("connect", {})
            result = adapter.execute("send_message", {})

        assert result["status"] == "error"
        assert "message" in result["result"].lower()

    def test_empty_message_returns_error(self, adapter: WebSocketAdapter) -> None:
        mock_conn = AsyncMock()
        fake_ws = MagicMock()
        fake_ws.connect = AsyncMock(return_value=mock_conn)

        with patch("runtime.adapters.websocket._import_websockets", return_value=fake_ws):
            adapter.execute("connect", {})
            result = adapter.execute("send_message", {"message": ""})

        assert result["status"] == "error"
        assert "message" in result["result"].lower()


# ── Not-connected guards ────────────────────────────────────────────────


class TestNotConnectedGuards:
    def test_send_before_connect_returns_error(self, adapter: WebSocketAdapter) -> None:
        result = adapter.execute("send_message", {"message": "hello"})
        assert result["status"] == "error"
        assert "Not connected" in result["result"]

    def test_listen_before_connect_returns_error(self, adapter: WebSocketAdapter) -> None:
        result = adapter.execute("listen", {"count": "1"})
        assert result["status"] == "error"
        assert "Not connected" in result["result"]


# ── Listen edge cases ───────────────────────────────────────────────────


class TestListenEdgeCases:
    def test_listen_defaults_to_one_message(self, adapter: WebSocketAdapter) -> None:
        mock_conn = AsyncMock()
        mock_conn.recv = AsyncMock(return_value="single")
        fake_ws = MagicMock()
        fake_ws.connect = AsyncMock(return_value=mock_conn)

        with patch("runtime.adapters.websocket._import_websockets", return_value=fake_ws):
            adapter.execute("connect", {})
            result = adapter.execute("listen", {})

        assert result["status"] == "ok"
        assert result["result"] == ["single"]

    def test_listen_stops_on_timeout(self, adapter: WebSocketAdapter) -> None:
        mock_conn = AsyncMock()
        mock_conn.recv = AsyncMock(side_effect=asyncio.TimeoutError)
        fake_ws = MagicMock()
        fake_ws.connect = AsyncMock(return_value=mock_conn)

        with patch("runtime.adapters.websocket._import_websockets", return_value=fake_ws):
            adapter.execute("connect", {})
            result = adapter.execute("listen", {"count": "5", "timeout": "0.01"})

        assert result["status"] == "ok"
        assert result["result"] == []

    def test_listen_partial_timeout(self, adapter: WebSocketAdapter) -> None:
        """Receives some messages then times out -- returns what it got."""
        mock_conn = AsyncMock()
        mock_conn.recv = AsyncMock(side_effect=["first", asyncio.TimeoutError])
        fake_ws = MagicMock()
        fake_ws.connect = AsyncMock(return_value=mock_conn)

        with patch("runtime.adapters.websocket._import_websockets", return_value=fake_ws):
            adapter.execute("connect", {})
            result = adapter.execute("listen", {"count": "3", "timeout": "0.01"})

        assert result["status"] == "ok"
        assert result["result"] == ["first"]
