"""Tests for timeout protection utilities for background tasks.

Validates that :func:`~app.tasks.timeout_utils.run_with_timeout` correctly
enforces timeouts on async tasks and logs appropriate warnings.
"""

from __future__ import annotations

import asyncio
from unittest.mock import AsyncMock, patch

import pytest


class TestRunWithTimeout:
    """Tests for :func:`~app.tasks.timeout_utils.run_with_timeout`."""

    @pytest.mark.asyncio
    async def test_run_with_timeout_completes_quickly(self) -> None:
        """``run_with_timeout`` must complete and return the result when task finishes quickly."""
        from app.tasks.timeout_utils import run_with_timeout

        async def _quick_task() -> str:
            return "success"

        result = await run_with_timeout("test_task", _quick_task(), timeout_seconds=10)
        assert result == "success"

    @pytest.mark.asyncio
    async def test_run_with_timeout_raises_timeout_error(self) -> None:
        """``run_with_timeout`` must raise TimeoutError when task exceeds timeout."""
        from app.tasks.timeout_utils import run_with_timeout

        async def _slow_task() -> None:
            await asyncio.sleep(5)

        with pytest.raises(TimeoutError):
            await run_with_timeout("slow_task", _slow_task(), timeout_seconds=0.1)

    @pytest.mark.asyncio
    async def test_run_with_timeout_logs_timeout_event(self) -> None:
        """``run_with_timeout`` must log a warning when timeout occurs."""
        from app.tasks.timeout_utils import run_with_timeout

        async def _slow_task() -> None:
            await asyncio.sleep(5)

        with patch("app.tasks.timeout_utils.log") as mock_log:
            with pytest.raises(TimeoutError):
                await run_with_timeout("slow_task", _slow_task(), timeout_seconds=0.1)

        # Verify timeout was logged
        timeout_calls = [
            c for c in mock_log.warning.call_args_list if c[0][0] == "task_timeout"
        ]
        assert len(timeout_calls) == 1
        call_kwargs = timeout_calls[0][1]
        assert call_kwargs["task_name"] == "slow_task"
        assert call_kwargs["timeout_seconds"] == 0.1
        assert call_kwargs["elapsed_seconds"] >= 0.1

    @pytest.mark.asyncio
    async def test_run_with_timeout_logs_approaching_timeout(self) -> None:
        """``run_with_timeout`` must log warning when task uses >80% of timeout."""
        from app.tasks.timeout_utils import run_with_timeout

        async def _medium_task() -> None:
            await asyncio.sleep(0.25)

        with patch("app.tasks.timeout_utils.log") as mock_log:
            await run_with_timeout("medium_task", _medium_task(), timeout_seconds=0.3)

        # Verify approaching timeout warning was logged (task used >80% of timeout)
        approaching_calls = [
            c
            for c in mock_log.warning.call_args_list
            if c[0][0] == "task_approaching_timeout"
        ]
        assert len(approaching_calls) == 1
        call_kwargs = approaching_calls[0][1]
        assert call_kwargs["task_name"] == "medium_task"
        assert call_kwargs["timeout_seconds"] == 0.3
        assert call_kwargs["usage_percent"] > 80

    @pytest.mark.asyncio
    async def test_run_with_timeout_no_warning_when_well_under_timeout(self) -> None:
        """``run_with_timeout`` must not log warning when task completes well before timeout."""
        from app.tasks.timeout_utils import run_with_timeout

        async def _quick_task() -> None:
            await asyncio.sleep(0.01)

        with patch("app.tasks.timeout_utils.log") as mock_log:
            await run_with_timeout("quick_task", _quick_task(), timeout_seconds=1.0)

        # Verify no approaching timeout warning was logged
        approaching_calls = [
            c
            for c in mock_log.warning.call_args_list
            if c[0][0] == "task_approaching_timeout"
        ]
        assert len(approaching_calls) == 0

    @pytest.mark.asyncio
    async def test_run_with_timeout_logs_elapsed_time(self) -> None:
        """``run_with_timeout`` must include elapsed time in timeout log."""
        from app.tasks.timeout_utils import run_with_timeout

        async def _slow_task() -> None:
            await asyncio.sleep(0.2)

        with patch("app.tasks.timeout_utils.log") as mock_log:
            with pytest.raises(TimeoutError):
                await run_with_timeout("slow_task", _slow_task(), timeout_seconds=0.1)

        timeout_calls = [
            c for c in mock_log.warning.call_args_list if c[0][0] == "task_timeout"
        ]
        call_kwargs = timeout_calls[0][1]
        assert "elapsed_seconds" in call_kwargs
        assert call_kwargs["elapsed_seconds"] >= 0.1

    @pytest.mark.asyncio
    async def test_run_with_timeout_returns_correct_type(self) -> None:
        """``run_with_timeout`` must preserve the return type of the coroutine."""
        from app.tasks.timeout_utils import run_with_timeout

        async def _task_returns_int() -> int:
            return 42

        result = await run_with_timeout("int_task", _task_returns_int(), timeout_seconds=10)
        assert isinstance(result, int)
        assert result == 42

    @pytest.mark.asyncio
    async def test_run_with_timeout_task_exception_propagates(self) -> None:
        """``run_with_timeout`` must propagate exceptions from the task (not timeout)."""
        from app.tasks.timeout_utils import run_with_timeout

        async def _failing_task() -> None:
            raise ValueError("Task failed")

        with pytest.raises(ValueError, match="Task failed"):
            await run_with_timeout("failing_task", _failing_task(), timeout_seconds=10)
