"""Tests for async_utils."""

from __future__ import annotations

import asyncio
import time
from concurrent.futures import ThreadPoolExecutor
from unittest import mock

import pytest

from app.utils.async_utils import logged_task, run_blocking


async def test_run_blocking_executes_callable_in_thread() -> None:
    """run_blocking should execute the provided callable and return its result."""

    def blocking_add(x: int, y: int) -> int:
        return x + y

    result = await run_blocking(blocking_add, 3, 4)
    assert result == 7


async def test_run_blocking_accepts_custom_executor() -> None:
    """run_blocking should use a provided executor when one is passed."""

    def blocking_value() -> str:
        return "ok"

    executor = ThreadPoolExecutor(max_workers=1)
    try:
        result = await run_blocking(blocking_value, executor=executor)
        assert result == "ok"
    finally:
        executor.shutdown(wait=True)


async def test_run_blocking_does_not_block_event_loop() -> None:
    """A blocking callable should not stall other async tasks."""

    def sleep_block() -> str:
        time.sleep(0.05)
        return "done"

    task = asyncio.create_task(run_blocking(sleep_block))
    # Yield control to ensure the event loop can run other tasks while the
    # blocking work executes in the thread pool.
    await asyncio.sleep(0)
    result = await task
    assert result == "done"


async def test_logged_task_awaits_coroutine() -> None:
    """logged_task should await and complete the coroutine."""

    async def dummy_coro() -> str:
        await asyncio.sleep(0)
        return "result"

    with mock.patch("app.utils.async_utils.log") as mock_log:
        await logged_task(dummy_coro(), "test_task")
        mock_log.exception.assert_not_called()


async def test_logged_task_catches_exception_and_logs() -> None:
    """logged_task should catch exceptions and log them with task_name context."""

    class CustomError(Exception):
        pass

    async def failing_coro() -> None:
        raise CustomError("task failed")

    with mock.patch("app.utils.async_utils.log") as mock_log:
        await logged_task(failing_coro(), "failing_task")
        mock_log.exception.assert_called_once_with(
            "background_task_failed",
            task_name="failing_task",
        )


async def test_logged_task_with_asyncio_create_task() -> None:
    """logged_task should work correctly when wrapped with asyncio.create_task."""

    results: list[str] = []

    async def background_work() -> None:
        await asyncio.sleep(0.01)
        results.append("done")

    with mock.patch("app.utils.async_utils.log"):
        task = asyncio.create_task(
            logged_task(background_work(), "bg_task"),
            name="bg_task",
        )
        await task
        assert results == ["done"]


async def test_logged_task_preserves_exception_info() -> None:
    """logged_task should preserve traceback when logging the exception."""

    async def failing_coro() -> None:
        raise ValueError("original error message")

    with mock.patch("app.utils.async_utils.log") as mock_log:
        await logged_task(failing_coro(), "test_task")
        mock_log.exception.assert_called_once()
        # Verify the exception context is logged (exception captures
        # the traceback automatically)
        args, kwargs = mock_log.exception.call_args
        assert args[0] == "background_task_failed"
        assert kwargs["task_name"] == "test_task"

