"""Tests for MCP Server — Observation Tools (M8)."""

import json

import pytest

from textual_mcp.session import AppSession


async def _launch_simple_session() -> AppSession:
    """Helper: create and launch an AppSession with SimpleApp."""
    from tests.apps.simple_app import SimpleApp

    session = AppSession("test-obs")
    await session.launch(SimpleApp())
    return session


# ---------------------------------------------------------------------------
# Test 8.1: textual_snapshot returns widget tree text with refs
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_snapshot_returns_tree_with_refs():
    """textual_snapshot should return JSON with a 'tree' field containing refs."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        tools = await mcp.list_tools()
        snapshot_tool = next(t for t in tools if t.name == "textual_snapshot")
        assert snapshot_tool is not None

        result = await mcp.call_tool(
            "textual_snapshot", {"session_id": session.session_id}
        )
        text = result[0][0].text
        data = json.loads(text)
        assert "tree" in data
        assert "[ref=" in data["tree"]
        assert "Screen" in data["tree"]
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.2: textual_snapshot includes focused_ref when a widget has focus
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_snapshot_includes_focused_ref():
    """textual_snapshot should include focused_ref when a widget has focus."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        # Tab to focus the Input
        await session.pilot.press("tab")
        await session.pilot.pause()

        result = await mcp.call_tool(
            "textual_snapshot", {"session_id": session.session_id}
        )
        data = json.loads(result[0][0].text)
        assert data["focused_ref"] is not None
        assert isinstance(data["focused_ref"], int)
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.3: textual_snapshot includes active key bindings
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_snapshot_includes_bindings():
    """textual_snapshot should include active key bindings."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        result = await mcp.call_tool(
            "textual_snapshot", {"session_id": session.session_id}
        )
        data = json.loads(result[0][0].text)
        assert "bindings" in data
        assert isinstance(data["bindings"], list)
        # Should have at least some default bindings (e.g., ctrl+c, ctrl+q)
        if len(data["bindings"]) > 0:
            b = data["bindings"][0]
            assert "key" in b
            assert "description" in b
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.4: textual_snapshot includes error list from collector
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_snapshot_includes_errors():
    """textual_snapshot should include errors from the error collector."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        result = await mcp.call_tool(
            "textual_snapshot", {"session_id": session.session_id}
        )
        data = json.loads(result[0][0].text)
        assert "errors" in data
        assert isinstance(data["errors"], list)
        # No errors expected in SimpleApp
        assert data["errors"] == []
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.5: textual_snapshot with max_depth limits tree depth
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_snapshot_max_depth():
    """textual_snapshot with max_depth should limit the tree depth."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        result = await mcp.call_tool(
            "textual_snapshot",
            {"session_id": session.session_id, "max_depth": 0},
        )
        data = json.loads(result[0][0].text)
        tree_lines = data["tree"].strip().splitlines()
        # max_depth=0 should only show Screen
        assert len(tree_lines) == 1
        assert "Screen" in tree_lines[0]
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.6: textual_snapshot with invalid session_id returns error
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_snapshot_invalid_session():
    """textual_snapshot with an invalid session_id should return an error."""
    from textual_mcp.server import mcp

    result = await mcp.call_tool(
        "textual_snapshot", {"session_id": "nonexistent"}
    )
    data = json.loads(result[0][0].text)
    assert "error" in data


# ---------------------------------------------------------------------------
# Test 8.7: textual_screenshot format="text" returns plaintext
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_screenshot_text_format():
    """textual_screenshot with format='text' should return plaintext."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        result = await mcp.call_tool(
            "textual_screenshot",
            {"session_id": session.session_id, "format": "text"},
        )
        data = json.loads(result[0][0].text)
        assert "content" in data
        # Should be plain text, not SVG
        assert "<svg" not in data["content"]
        assert isinstance(data["content"], str)
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.8: textual_screenshot format="svg" returns SVG string
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_screenshot_svg_format():
    """textual_screenshot with format='svg' should return an SVG string."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        result = await mcp.call_tool(
            "textual_screenshot",
            {"session_id": session.session_id, "format": "svg"},
        )
        data = json.loads(result[0][0].text)
        assert "content" in data
        assert "<svg" in data["content"]
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.9: textual_query with valid CSS selector returns matching widgets
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_query_valid_selector():
    """textual_query with a valid selector should return matching widgets."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        result = await mcp.call_tool(
            "textual_query",
            {"session_id": session.session_id, "selector": "Button"},
        )
        data = json.loads(result[0][0].text)
        assert "widgets" in data
        assert len(data["widgets"]) >= 1
        btn = data["widgets"][0]
        assert btn["type"] == "Button"
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.10: textual_query with no matches returns empty list
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_query_no_matches():
    """textual_query with a non-matching selector should return empty list."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        result = await mcp.call_tool(
            "textual_query",
            {"session_id": session.session_id, "selector": "#nonexistent"},
        )
        data = json.loads(result[0][0].text)
        assert data["widgets"] == []
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.11: textual_query returns widget type, id, classes, and properties
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_query_returns_widget_details():
    """textual_query should return type, id, classes, and type-specific properties."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        result = await mcp.call_tool(
            "textual_query",
            {"session_id": session.session_id, "selector": "Input"},
        )
        data = json.loads(result[0][0].text)
        assert len(data["widgets"]) >= 1
        w = data["widgets"][0]
        assert w["type"] == "Input"
        assert w["id"] == "text-input"
        assert "classes" in w
        assert "properties" in w
        assert "placeholder" in w["properties"]
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.12: textual_get_screen_stack returns current screen stack
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_screen_stack():
    """textual_get_screen_stack should return the screen stack."""
    from textual_mcp.server import mcp, _session_manager

    session = await _launch_simple_session()
    _session_manager._sessions[session.session_id] = session

    try:
        result = await mcp.call_tool(
            "textual_get_screen_stack",
            {"session_id": session.session_id},
        )
        data = json.loads(result[0][0].text)
        assert "screens" in data
        assert len(data["screens"]) >= 1
        screen = data["screens"][0]
        assert "name" in screen
        assert "type" in screen
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.13: textual_get_screen_stack shows modal screens
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_screen_stack_modal():
    """textual_get_screen_stack should indicate modal screens."""
    from textual.app import App, ComposeResult
    from textual.screen import ModalScreen
    from textual.widgets import Static, Button

    class MyModal(ModalScreen):
        def compose(self) -> ComposeResult:
            yield Static("Modal content")
            yield Button("Close", id="close")

    class ModalApp(App):
        def compose(self) -> ComposeResult:
            yield Button("Open Modal", id="open")

        def on_button_pressed(self, event: Button.Pressed) -> None:
            if event.button.id == "open":
                self.push_screen(MyModal())

    session = AppSession("test-modal")
    await session.launch(ModalApp())
    from textual_mcp.server import mcp, _session_manager

    _session_manager._sessions[session.session_id] = session

    try:
        # Push the modal screen
        await session.pilot.click("#open")
        await session.pilot.pause()

        result = await mcp.call_tool(
            "textual_get_screen_stack",
            {"session_id": session.session_id},
        )
        data = json.loads(result[0][0].text)
        assert len(data["screens"]) >= 2
        # The modal screen should have is_modal=True
        modal_screen = data["screens"][-1]
        assert modal_screen["is_modal"] is True
    finally:
        await session.stop()
        _session_manager._sessions.pop(session.session_id, None)


# ---------------------------------------------------------------------------
# Test 8.14: all observation tools have readOnlyHint=true annotation
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_observation_tools_readonly_hint():
    """All observation tools should have readOnlyHint=True annotation."""
    from textual_mcp.server import mcp

    tools = await mcp.list_tools()
    observation_tools = [
        t
        for t in tools
        if t.name
        in {
            "textual_snapshot",
            "textual_screenshot",
            "textual_query",
            "textual_get_screen_stack",
        }
    ]
    assert len(observation_tools) == 4, (
        f"Expected 4 observation tools, got {[t.name for t in observation_tools]}"
    )
    for t in observation_tools:
        assert t.annotations is not None, f"{t.name} missing annotations"
        assert t.annotations.readOnlyHint is True, (
            f"{t.name} should have readOnlyHint=True"
        )
