"""Tests for screen capture tools."""

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

import numpy as np
import pytest
from fastmcp import Client


async def test_screenshot_returns_metadata_and_image(client: Client):
    result = await client.call_tool("vnc_screenshot", {"session_id": "testhost:5900"})
    # First content block is JSON metadata
    meta = json.loads(result.content[0].text)
    assert meta["session_id"] == "testhost:5900"
    assert meta["width"] == 1920
    assert meta["height"] == 1080
    assert meta["png_size_bytes"] > 0
    # Second content block is the image
    assert result.content[1].type == "image"


async def test_screenshot_with_max_width(client: Client):
    result = await client.call_tool("vnc_screenshot", {
        "session_id": "testhost:5900",
        "max_width": 800,
    })
    meta = json.loads(result.content[0].text)
    assert meta["scaled_width"] == 800
    assert "note" in meta  # warns about coordinate mismatch


async def test_screen_info(client: Client):
    result = await client.call_tool("vnc_screen_info", {"session_id": "testhost:5900"})
    data = json.loads(result.content[0].text)
    assert data["width"] == 1920
    assert data["height"] == 1080
    assert data["desktop_name"] == "Test Desktop"


async def test_screenshot_nonexistent_session(client: Client):
    with pytest.raises(Exception, match="No session"):
        await client.call_tool("vnc_screenshot", {"session_id": "nope"})


async def test_screenshot_timeout(client: Client):
    """C2: Screenshot timeout produces ToolError instead of hanging forever."""
    from mcvnc._state import _state

    session = _state.sessions["testhost:5900"]
    original = session.client.screenshot

    async def _hang():
        await asyncio.sleep(999)

    session.client.screenshot = _hang

    with patch("mcvnc._screen.SCREENSHOT_TIMEOUT", 0.1):
        with pytest.raises(Exception, match="timed out"):
            await client.call_tool("vnc_screenshot", {"session_id": "testhost:5900"})

    # Restore for other tests
    session.client.screenshot = original


# ── Region screenshot tests ──────────────────────────────────────────


async def test_region_screenshot_happy_path(client: Client):
    """Region screenshot returns metadata with region and desktop_size dicts."""
    # First take a full screenshot to populate video.data
    await client.call_tool("vnc_screenshot", {"session_id": "testhost:5900"})

    result = await client.call_tool("vnc_screenshot_region", {
        "session_id": "testhost:5900",
        "x": 100, "y": 100, "width": 200, "height": 100,
    })
    meta = json.loads(result.content[0].text)
    assert meta["region"] == {"x": 100, "y": 100, "width": 200, "height": 100}
    assert meta["desktop_size"] == {"width": 1920, "height": 1080}
    assert meta["png_size_bytes"] > 0
    assert result.content[1].type == "image"


async def test_region_screenshot_with_max_width(client: Client):
    """Region screenshot respects max_width scaling."""
    result = await client.call_tool("vnc_screenshot_region", {
        "session_id": "testhost:5900",
        "x": 0, "y": 0, "width": 800, "height": 600,
        "max_width": 400,
    })
    meta = json.loads(result.content[0].text)
    assert meta["scaled_width"] == 400
    assert meta["scaled_height"] == 300
    assert "note" in meta


async def test_region_screenshot_exceeds_bounds(client: Client):
    """Region extending past desktop edge raises ToolError."""
    with pytest.raises(Exception, match="exceeds desktop bounds"):
        await client.call_tool("vnc_screenshot_region", {
            "session_id": "testhost:5900",
            "x": 1800, "y": 0, "width": 200, "height": 100,
        })


async def test_region_screenshot_negative_x(client: Client):
    """Negative x coordinate raises ToolError."""
    with pytest.raises(Exception, match="non-negative"):
        await client.call_tool("vnc_screenshot_region", {
            "session_id": "testhost:5900",
            "x": -10, "y": 0, "width": 100, "height": 100,
        })


async def test_region_screenshot_zero_width(client: Client):
    """Zero-dimension region raises ToolError."""
    with pytest.raises(Exception, match="must be positive"):
        await client.call_tool("vnc_screenshot_region", {
            "session_id": "testhost:5900",
            "x": 0, "y": 0, "width": 0, "height": 100,
        })


async def test_region_screenshot_timeout(client: Client):
    """Region screenshot timeout produces ToolError."""
    from mcvnc._state import _state

    session = _state.sessions["testhost:5900"]
    original = session.client.screenshot

    async def _hang(*args, **kwargs):
        await asyncio.sleep(999)

    session.client.screenshot = _hang

    with patch("mcvnc._screen.SCREENSHOT_TIMEOUT", 0.1):
        with pytest.raises(Exception, match="timed out"):
            await client.call_tool("vnc_screenshot_region", {
                "session_id": "testhost:5900",
                "x": 0, "y": 0, "width": 100, "height": 100,
            })

    session.client.screenshot = original


# ── Wait-for-change tests ───────────────────────────────────────────


async def test_wait_detects_screen_update(client: Client):
    """Returns changed=True when serial increments and a VIDEO update arrives."""
    from asyncvnc2 import UpdateType
    from mcvnc._state import _state

    # Take initial screenshot to populate video.data
    await client.call_tool("vnc_screenshot", {"session_id": "testhost:5900"})

    session = _state.sessions["testhost:5900"]
    serial_before = session.client.video.serial

    async def _read_with_update():
        session.client.video.serial += 1
        return UpdateType.VIDEO

    session.client.read = AsyncMock(side_effect=_read_with_update)

    result = json.loads(
        (await client.call_tool("vnc_wait_for_screen_change", {
            "session_id": "testhost:5900", "timeout": 2,
        })).content[0].text
    )
    assert result["changed"] is True
    assert result["serial_before"] == serial_before
    assert result["serial_after"] > serial_before
    assert result["updates_received"] >= 1
    assert "note" not in result


async def test_wait_timeout_no_update(client: Client):
    """Returns changed=False with note when no update arrives before timeout."""
    from mcvnc._state import _state

    await client.call_tool("vnc_screenshot", {"session_id": "testhost:5900"})
    session = _state.sessions["testhost:5900"]

    # read() hangs forever -- timeout will fire
    async def _hang():
        await asyncio.sleep(999)

    session.client.read = AsyncMock(side_effect=_hang)

    result = json.loads(
        (await client.call_tool("vnc_wait_for_screen_change", {
            "session_id": "testhost:5900", "timeout": 1,
        })).content[0].text
    )
    assert result["changed"] is False
    assert "note" in result


async def test_wait_no_prior_screenshot(client: Client):
    """Calling wait without a prior screenshot raises ToolError."""
    from mcvnc._state import _state

    session = _state.sessions["testhost:5900"]
    session.client.video.data = None

    with pytest.raises(Exception, match="take a vnc_screenshot first"):
        await client.call_tool("vnc_wait_for_screen_change", {
            "session_id": "testhost:5900",
        })


async def test_wait_region_change_detected(client: Client):
    """Detects pixel changes within the watched region."""
    from asyncvnc2 import UpdateType
    from mcvnc._state import _state

    await client.call_tool("vnc_screenshot", {"session_id": "testhost:5900"})
    session = _state.sessions["testhost:5900"]

    async def _read_with_region_change():
        # Modify pixels in the watched region (0,0 -> 100x100)
        session.client.video.serial += 1
        session.client.video.data[10, 10, 0] = 42  # change a pixel in region
        return UpdateType.VIDEO

    session.client.read = AsyncMock(side_effect=_read_with_region_change)

    result = json.loads(
        (await client.call_tool("vnc_wait_for_screen_change", {
            "session_id": "testhost:5900", "timeout": 2,
            "x": 0, "y": 0, "width": 100, "height": 100,
        })).content[0].text
    )
    assert result["changed"] is True
    assert result["region"] == {"x": 0, "y": 0, "width": 100, "height": 100}


async def test_wait_region_unchanged_elsewhere(client: Client):
    """Serial changes but watched region pixels unchanged -> changed=False."""
    from asyncvnc2 import UpdateType
    from mcvnc._state import _state

    await client.call_tool("vnc_screenshot", {"session_id": "testhost:5900"})
    session = _state.sessions["testhost:5900"]

    call_count = 0

    async def _read_change_outside_region():
        nonlocal call_count
        call_count += 1
        if call_count == 1:
            # Change pixels OUTSIDE the watched region
            session.client.video.serial += 1
            session.client.video.data[500, 500, 0] = 42
            return UpdateType.VIDEO
        # Subsequent calls hang to trigger timeout
        await asyncio.sleep(999)

    session.client.read = AsyncMock(side_effect=_read_change_outside_region)

    result = json.loads(
        (await client.call_tool("vnc_wait_for_screen_change", {
            "session_id": "testhost:5900", "timeout": 1,
            "x": 0, "y": 0, "width": 100, "height": 100,
        })).content[0].text
    )
    assert result["changed"] is False
    assert result["region"] == {"x": 0, "y": 0, "width": 100, "height": 100}


async def test_wait_partial_region_params(client: Client):
    """Providing only some region params raises ToolError."""
    await client.call_tool("vnc_screenshot", {"session_id": "testhost:5900"})

    with pytest.raises(Exception, match="Region requires all four"):
        await client.call_tool("vnc_wait_for_screen_change", {
            "session_id": "testhost:5900",
            "x": 10,  # missing y, width, height
        })


async def test_wait_region_out_of_bounds(client: Client):
    """Region extending past desktop raises ToolError."""
    await client.call_tool("vnc_screenshot", {"session_id": "testhost:5900"})

    with pytest.raises(Exception, match="exceeds desktop bounds"):
        await client.call_tool("vnc_wait_for_screen_change", {
            "session_id": "testhost:5900",
            "x": 1800, "y": 0, "width": 200, "height": 100,
        })
