"""Unit tests for bannerlord.gabs_client — TCP JSON-RPC client."""

import asyncio
import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from bannerlord.gabs_client import GABSClient, GABSError, GABSUnavailable


# ── Connection ────────────────────────────────────────────────────────────────


class TestGABSClientConnection:
    async def test_connect_success(self):
        mock_reader = AsyncMock()
        mock_writer = MagicMock()
        mock_writer.close = MagicMock()
        mock_writer.wait_closed = AsyncMock()

        with patch(
            "bannerlord.gabs_client.asyncio.open_connection",
            return_value=(mock_reader, mock_writer),
        ):
            client = GABSClient()
            await client.connect()

        assert client.connected is True
        await client.close()

    async def test_connect_failure_degrades_gracefully(self):
        with patch(
            "bannerlord.gabs_client.asyncio.open_connection",
            side_effect=OSError("Connection refused"),
        ):
            client = GABSClient()
            await client.connect()  # must not raise

        assert client.connected is False

    async def test_connect_timeout_degrades_gracefully(self):
        with patch(
            "bannerlord.gabs_client.asyncio.open_connection",
            side_effect=asyncio.TimeoutError(),
        ):
            client = GABSClient()
            await client.connect()

        assert client.connected is False

    async def test_context_manager(self):
        mock_reader = AsyncMock()
        mock_writer = MagicMock()
        mock_writer.close = MagicMock()
        mock_writer.wait_closed = AsyncMock()

        with patch(
            "bannerlord.gabs_client.asyncio.open_connection",
            return_value=(mock_reader, mock_writer),
        ):
            async with GABSClient() as client:
                assert client.connected is True

        assert client.connected is False


# ── RPC ───────────────────────────────────────────────────────────────────────


class TestGABSClientRPC:
    def _make_connected_client(self, response_data: dict):
        """Return a client with mocked reader/writer."""
        client = GABSClient()
        client._connected = True

        raw_response = json.dumps(response_data) + "\n"
        client._reader = AsyncMock()
        client._reader.readline = AsyncMock(return_value=raw_response.encode())

        client._writer = MagicMock()
        client._writer.write = MagicMock()
        client._writer.drain = AsyncMock()

        return client

    async def test_call_returns_result(self):
        client = self._make_connected_client({"jsonrpc": "2.0", "id": 1, "result": {"foo": "bar"}})
        result = await client.call("game.getState")
        assert result == {"foo": "bar"}

    async def test_call_raises_on_error(self):
        client = self._make_connected_client(
            {"jsonrpc": "2.0", "id": 1, "error": {"code": -32601, "message": "Method not found"}}
        )
        with pytest.raises(GABSError, match="Method not found"):
            await client.call("game.nonexistent")

    async def test_call_raises_unavailable_when_not_connected(self):
        client = GABSClient()
        assert client.connected is False
        with pytest.raises(GABSUnavailable):
            await client.call("game.getState")

    async def test_sequence_increments(self):
        client = self._make_connected_client({"jsonrpc": "2.0", "id": 1, "result": {}})
        await client.call("game.getState")
        assert client._seq == 1
        client._reader.readline = AsyncMock(
            return_value=(json.dumps({"jsonrpc": "2.0", "id": 2, "result": {}}) + "\n").encode()
        )
        await client.call("game.getState")
        assert client._seq == 2

    async def test_get_state_calls_correct_method(self):
        client = self._make_connected_client(
            {"jsonrpc": "2.0", "id": 1, "result": {"campaign_day": 10}}
        )
        result = await client.get_state()
        written = client._writer.write.call_args[0][0].decode()
        payload = json.loads(written.strip())
        assert payload["method"] == "game.getState"
        assert result == {"campaign_day": 10}

    async def test_move_party_sends_target(self):
        client = self._make_connected_client(
            {"jsonrpc": "2.0", "id": 1, "result": {"moving": True}}
        )
        await client.move_party("Epicrotea")
        written = client._writer.write.call_args[0][0].decode()
        payload = json.loads(written.strip())
        assert payload["method"] == "party.move"
        assert payload["params"]["target"] == "Epicrotea"

    async def test_connection_lost_marks_disconnected(self):
        client = GABSClient()
        client._connected = True
        client._reader = AsyncMock()
        client._reader.readline = AsyncMock(side_effect=OSError("connection reset"))
        client._writer = MagicMock()
        client._writer.write = MagicMock()
        client._writer.drain = AsyncMock()

        with pytest.raises(GABSUnavailable):
            await client.call("game.getState")

        assert client.connected is False
