"""Tests for ErrorCollector — message hook error aggregation (M3)."""

import time

import pytest

from textual_mcp.error_collector import ErrorCollector


# ---------------------------------------------------------------------------
# Test 3.1: on_message() increments messages_seen counter
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_on_message_increments_messages_seen():
    """Each call to on_message() should increment the messages_seen counter."""
    collector = ErrorCollector()
    assert collector.messages_seen == 0

    # Use the message_hook parameter of run_test() to feed messages
    from tests.apps.worker_app import WorkerApp

    async with WorkerApp().run_test(message_hook=collector.on_message) as pilot:
        await pilot.click("#start")
        await pilot.pause(delay=0.3)

    assert collector.messages_seen > 0


# ---------------------------------------------------------------------------
# Test 3.2: drain() returns collected errors and clears the buffer
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_drain_returns_and_clears():
    """drain() should return all collected errors, then clear internal buffer."""
    from tests.apps.worker_app import WorkerApp

    collector = ErrorCollector()

    try:
        async with WorkerApp().run_test(message_hook=collector.on_message) as pilot:
            await pilot.click("#fail")
            await pilot.pause(delay=0.5)
    except Exception:
        pass  # Worker failure re-raised on exit

    errors = collector.drain()
    assert len(errors) >= 1
    assert errors[0]["type"] == "worker_error"
    assert errors[0]["worker_name"] == "do_failing_work"
    assert "Worker failed" in errors[0]["error"]
    assert "timestamp" in errors[0]

    # Buffer should now be empty
    assert collector.errors == []


# ---------------------------------------------------------------------------
# Test 3.3: drain() on empty collector returns empty list
# ---------------------------------------------------------------------------
def test_drain_empty_returns_empty_list():
    """drain() on a fresh collector should return an empty list."""
    collector = ErrorCollector()
    result = collector.drain()
    assert result == []
    assert isinstance(result, list)


# ---------------------------------------------------------------------------
# Test 3.4: Worker.StateChanged with ERROR state is captured
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_worker_error_state_captured():
    """Worker.StateChanged with ERROR state should be captured with worker name and error."""
    from tests.apps.worker_app import WorkerApp

    collector = ErrorCollector()

    try:
        async with WorkerApp().run_test(message_hook=collector.on_message) as pilot:
            await pilot.click("#fail")
            await pilot.pause(delay=0.5)
    except Exception:
        pass  # Worker failure re-raised on exit

    errors = collector.drain()
    assert len(errors) >= 1
    error = errors[0]
    assert error["type"] == "worker_error"
    assert error["worker_name"] == "do_failing_work"
    assert "Worker failed" in error["error"]
    assert isinstance(error["timestamp"], float)


# ---------------------------------------------------------------------------
# Test 3.5: check_app_exception() returns None when no exception
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_check_app_exception_returns_none():
    """check_app_exception() should return None when app has no exception."""
    from tests.apps.worker_app import WorkerApp

    collector = ErrorCollector()

    async with WorkerApp().run_test() as pilot:
        result = collector.check_app_exception(pilot.app)
        assert result is None


# ---------------------------------------------------------------------------
# Test 3.6: check_app_exception() returns exception details when app crashes
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_check_app_exception_returns_details():
    """check_app_exception() should return exception type and message when app crashes."""
    from tests.apps.error_app import ErrorApp

    collector = ErrorCollector()
    app = ErrorApp()

    try:
        async with app.run_test() as pilot:
            await pilot.click("#crash-btn")
            await pilot.pause()
    except RuntimeError:
        pass  # App crash re-raised on exit

    result = collector.check_app_exception(app)
    assert result is not None
    assert result["type"] == "RuntimeError"
    assert result["message"] == "Intentional crash"


# ---------------------------------------------------------------------------
# Test 3.7: multiple errors are captured in order
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_multiple_errors_in_order():
    """Multiple worker errors should be captured in the order they occur."""
    from tests.apps.worker_app import WorkerApp

    collector = ErrorCollector()

    # First error from a real worker failure
    try:
        async with WorkerApp().run_test(message_hook=collector.on_message) as pilot:
            await pilot.click("#fail")
            await pilot.pause(delay=0.5)
    except Exception:
        pass

    # Add a second error manually to test ordering
    collector.errors.append(
        {
            "type": "worker_error",
            "worker_name": "second_worker",
            "error": "second failure",
            "timestamp": time.time(),
        }
    )

    errors = collector.drain()
    assert len(errors) >= 2
    for e in errors:
        assert e["type"] == "worker_error"
    # First error from the real worker, second from manual append
    assert errors[0]["worker_name"] == "do_failing_work"
    assert errors[-1]["worker_name"] == "second_worker"
    # Timestamps should be non-decreasing
    for i in range(1, len(errors)):
        assert errors[i]["timestamp"] >= errors[i - 1]["timestamp"]


# ---------------------------------------------------------------------------
# Test 3.8: drain() is idempotent — second call returns empty list
# ---------------------------------------------------------------------------
def test_drain_idempotent():
    """After drain(), a second drain() should return an empty list."""
    collector = ErrorCollector()
    collector.errors.append(
        {
            "type": "worker_error",
            "worker_name": "test_worker",
            "error": "test error",
            "timestamp": time.time(),
        }
    )
    first = collector.drain()
    assert len(first) == 1

    second = collector.drain()
    assert second == []

    third = collector.drain()
    assert third == []
