"""Test fixtures for mcvnc."""

import asyncio
from contextlib import asynccontextmanager
from dataclasses import dataclass
from unittest.mock import AsyncMock, MagicMock, patch

import numpy as np
import pytest
from fastmcp import Client

from mcvnc.server import mcp


@pytest.fixture(autouse=True)
def _reset_mcp_singleton():
    """Reset event-loop-bound state on the mcp singleton.

    FastMCP creates an asyncio.Event() at construction time. pytest-asyncio
    creates a fresh loop per test, so after test 1 the Event is bound to a
    dead loop. Re-creating these objects keeps every test isolated.
    """
    _PRIVATE_ATTRS = ("_started", "_lifespan_result", "_lifespan_result_set")
    for attr in _PRIVATE_ATTRS:
        if not hasattr(mcp, attr):
            raise AttributeError(
                f"FastMCP removed {attr!r} -- fixture needs update "
                f"(fastmcp version may have changed)"
            )
    mcp._started = asyncio.Event()
    mcp._lifespan_result = None
    mcp._lifespan_result_set = False
    yield


@dataclass
class MockVideo:
    name: str = "Test Desktop"
    width: int = 1920
    height: int = 1080
    mode: str = "rgba"
    data: object = None
    serial: int = 0

    def refresh(self, x=0, y=0, width=None, height=None):
        """No-op: in real asyncvnc2, sends an incremental update request."""

    def as_rgba(self, x=0, y=0, width=None, height=None):
        """Return RGBA subarray from data (mirrors asyncvnc2.Video.as_rgba)."""
        if self.data is None:
            return None
        w = width if width is not None else self.width
        h = height if height is not None else self.height
        return self.data[y : y + h, x : x + w]

    def is_complete(self, x=0, y=0, width=None, height=None):
        """Returns True if the region is fully opaque."""
        if self.data is None:
            return False
        rgba = self.as_rgba(x, y, width, height)
        return bool(np.all(rgba[:, :, 3] == 255))


def _make_vnc_client():
    """Create a mock asyncvnc2 Client with keyboard/mouse/clipboard/video."""
    client = MagicMock()

    # Video
    client.video = MockVideo()

    # Screenshot returns RGBA numpy array and populates video.data as side effect
    pixels = np.zeros((1080, 1920, 4), dtype=np.uint8)
    pixels[:, :, 3] = 255  # fully opaque (required by is_complete check)
    # Paint a small red rectangle so tests can verify non-blank output
    pixels[100:200, 100:300, 0] = 255  # red channel

    async def _screenshot_side_effect(*args, **kwargs):
        client.video.data = pixels
        client.video.serial += 1
        # Region screenshot: return subarray if args provided
        if args:
            sx, sy = args[0], args[1]
            sw = args[2] if len(args) > 2 else pixels.shape[1]
            sh = args[3] if len(args) > 3 else pixels.shape[0]
            return pixels[sy : sy + sh, sx : sx + sw].copy()
        return pixels

    client.screenshot = AsyncMock(side_effect=_screenshot_side_effect)

    # Keyboard
    client.keyboard = MagicMock()
    client.keyboard.write = MagicMock()
    client.keyboard.press = MagicMock()
    client.keyboard.hold = MagicMock()

    # Mouse
    client.mouse = MagicMock()
    client.mouse.move = MagicMock()
    client.mouse.click = MagicMock()
    client.mouse.scroll_up = MagicMock()
    client.mouse.scroll_down = MagicMock()
    client.mouse.hold = MagicMock(return_value=MagicMock(
        __enter__=MagicMock(return_value=None),
        __exit__=MagicMock(return_value=False),
    ))

    # Clipboard
    client.clipboard = MagicMock()
    client.clipboard.text = "test clipboard content"
    client.clipboard.write = MagicMock()

    # read() returns UpdateType -- configured per-test via side_effect
    client.read = AsyncMock()

    # drain is async
    client.drain = AsyncMock()

    # writer for cleanup
    client.writer = MagicMock()
    client.writer.close = MagicMock()

    return client


@asynccontextmanager
async def _mock_connect(*args, **kwargs):
    """Mock asyncvnc2.connect() async context manager."""
    yield _make_vnc_client()


@pytest.fixture
async def client():
    """MCP client with mocked asyncvnc2 backend."""
    with patch("mcvnc._connect.asyncvnc2.connect", side_effect=_mock_connect):
        async with Client(mcp) as c:
            # Establish a session for tests that need one
            await c.call_tool("vnc_connect", {"host": "testhost", "port": 5900})
            yield c
