"""Test Home Assistant timeout handler."""

import asyncio
from contextlib import suppress
import time

import pytest

from homeassistant.core import HomeAssistant
from homeassistant.util.timeout import TimeoutManager


async def test_simple_global_timeout() -> None:
    """Test a simple global timeout."""
    timeout = TimeoutManager()

    with pytest.raises(TimeoutError):
        async with timeout.async_timeout(0.1):
            await asyncio.sleep(0.3)


async def test_simple_global_timeout_with_executor_job(hass: HomeAssistant) -> None:
    """Test a simple global timeout with executor job."""
    timeout = TimeoutManager()

    with pytest.raises(TimeoutError):
        async with timeout.async_timeout(0.1):
            await hass.async_add_executor_job(time.sleep, 0.2)


async def test_simple_global_timeout_freeze() -> None:
    """Test a simple global timeout freeze."""
    timeout = TimeoutManager()

    async with timeout.async_timeout(0.2), timeout.async_freeze():
        await asyncio.sleep(0.3)


async def test_simple_global_timeout_cancel_message() -> None:
    """Test a simple global timeout cancel message."""
    timeout = TimeoutManager()

    with suppress(TimeoutError):
        async with timeout.async_timeout(0.1, cancel_message="Test"):
            with pytest.raises(
                asyncio.CancelledError, match="Global task timeout: Test"
            ):
                await asyncio.sleep(0.3)


async def test_simple_zone_timeout_freeze_inside_executor_job(
    hass: HomeAssistant,
) -> None:
    """Test a simple zone timeout freeze inside an executor job."""
    timeout = TimeoutManager()

    def _some_sync_work():
        with timeout.freeze("recorder"):
            time.sleep(0.3)

    async with (
        timeout.async_timeout(1.0),
        timeout.async_timeout(0.2, zone_name="recorder"),
    ):
        await hass.async_add_executor_job(_some_sync_work)


async def test_simple_global_timeout_freeze_inside_executor_job(
    hass: HomeAssistant,
) -> None:
    """Test a simple global timeout freeze inside an executor job."""
    timeout = TimeoutManager()

    def _some_sync_work():
        with timeout.freeze():
            time.sleep(0.3)

    async with timeout.async_timeout(0.2):
        await hass.async_add_executor_job(_some_sync_work)


async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job(
    hass: HomeAssistant,
) -> None:
    """Test a simple global timeout freeze inside an executor job."""
    timeout = TimeoutManager()

    def _some_sync_work():
        with timeout.freeze("recorder"):
            time.sleep(0.3)

    async with (
        timeout.async_timeout(0.1),
        timeout.async_timeout(0.2, zone_name="recorder"),
    ):
        await hass.async_add_executor_job(_some_sync_work)


async def test_mix_global_timeout_freeze_and_zone_freeze_different_order(
    hass: HomeAssistant,
) -> None:
    """Test global timeout freeze in executor job before timeout set."""
    timeout = TimeoutManager()

    def _some_sync_work():
        with timeout.freeze("recorder"):
            time.sleep(0.4)

    async with timeout.async_timeout(0.1):
        hass.async_add_executor_job(_some_sync_work)
        async with timeout.async_timeout(0.2, zone_name="recorder"):
            await asyncio.sleep(0.3)


async def test_mix_global_timeout_freeze_and_zone_freeze_other_zone_inside_executor_job(
    hass: HomeAssistant,
) -> None:
    """Test a simple global timeout freeze other zone inside an executor job."""
    timeout = TimeoutManager()

    def _some_sync_work():
        with timeout.freeze("not_recorder"):
            time.sleep(0.3)

    with pytest.raises(TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.1):
            async with (
                timeout.async_timeout(0.2, zone_name="recorder"),
                timeout.async_timeout(0.2, zone_name="not_recorder"),
            ):
                await hass.async_add_executor_job(_some_sync_work)


async def test_mix_global_timeout_freeze_and_zone_freeze_executor_2nd_outside_zone(
    hass: HomeAssistant,
) -> None:
    """Test global timeout freeze in executor with second job outside zone."""
    timeout = TimeoutManager()

    def _some_sync_work():
        with timeout.freeze("recorder"):
            time.sleep(0.3)

    with pytest.raises(TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.1):
            async with timeout.async_timeout(0.2, zone_name="recorder"):
                await hass.async_add_executor_job(_some_sync_work)
            await hass.async_add_executor_job(time.sleep, 0.2)


async def test_simple_global_timeout_freeze_with_executor_job(
    hass: HomeAssistant,
) -> None:
    """Test a simple global timeout freeze with executor job."""
    timeout = TimeoutManager()

    async with timeout.async_timeout(0.2), timeout.async_freeze():
        await hass.async_add_executor_job(time.sleep, 0.3)


async def test_simple_global_timeout_does_not_leak_upward(
    hass: HomeAssistant,
) -> None:
    """Test a global timeout does not leak upward."""
    timeout = TimeoutManager()
    current_task = asyncio.current_task()
    assert current_task is not None
    cancelling_inside_timeout = None

    with pytest.raises(asyncio.TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.1):
            cancelling_inside_timeout = current_task.cancelling()
            await asyncio.sleep(0.3)

    assert cancelling_inside_timeout == 0
    # After the context manager exits, the task should no longer be cancelling
    assert current_task.cancelling() == 0


async def test_simple_global_timeout_does_swallow_cancellation(
    hass: HomeAssistant,
) -> None:
    """Test a global timeout does not swallow cancellation."""
    timeout = TimeoutManager()
    current_task = asyncio.current_task()
    assert current_task is not None
    cancelling_inside_timeout = None

    async def task_with_timeout() -> None:
        nonlocal cancelling_inside_timeout
        new_task = asyncio.current_task()
        assert new_task is not None
        with pytest.raises(asyncio.TimeoutError):  # noqa: PT012
            cancelling_inside_timeout = new_task.cancelling()
            async with timeout.async_timeout(0.1):
                await asyncio.sleep(0.3)

    # After the context manager exits, the task should no longer be cancelling
    assert current_task.cancelling() == 0

    task = asyncio.create_task(task_with_timeout())
    await asyncio.sleep(0)
    task.cancel()
    assert task.cancelling() == 1

    assert cancelling_inside_timeout == 0
    # Cancellation should not leak into the current task
    assert current_task.cancelling() == 0
    # Cancellation should not be swallowed if the task is cancelled
    # and it also times out
    await asyncio.sleep(0)
    with pytest.raises(asyncio.CancelledError):
        await task
    assert task.cancelling() == 1


async def test_simple_global_timeout_freeze_reset() -> None:
    """Test a simple global timeout freeze reset."""
    timeout = TimeoutManager()

    with pytest.raises(TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.2):
            async with timeout.async_freeze():
                await asyncio.sleep(0.1)
            await asyncio.sleep(0.2)


async def test_simple_zone_timeout() -> None:
    """Test a simple zone timeout."""
    timeout = TimeoutManager()

    with pytest.raises(TimeoutError):
        async with timeout.async_timeout(0.1, "test"):
            await asyncio.sleep(0.3)


async def test_simple_zone_timeout_cancel_message() -> None:
    """Test a simple zone timeout cancel message."""
    timeout = TimeoutManager()

    with suppress(TimeoutError):
        async with timeout.async_timeout(0.1, "test", cancel_message="Test"):
            with pytest.raises(asyncio.CancelledError, match="Zone timeout: Test"):
                await asyncio.sleep(0.3)


async def test_simple_zone_timeout_does_not_leak_upward(
    hass: HomeAssistant,
) -> None:
    """Test a zone timeout does not leak upward."""
    timeout = TimeoutManager()
    current_task = asyncio.current_task()
    assert current_task is not None
    cancelling_inside_timeout = None

    with pytest.raises(asyncio.TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.1, "test"):
            cancelling_inside_timeout = current_task.cancelling()
            await asyncio.sleep(0.3)

    assert cancelling_inside_timeout == 0
    # After the context manager exits, the task should no longer be cancelling
    assert current_task.cancelling() == 0


async def test_simple_zone_timeout_does_swallow_cancellation(
    hass: HomeAssistant,
) -> None:
    """Test a zone timeout does not swallow cancellation."""
    timeout = TimeoutManager()
    current_task = asyncio.current_task()
    assert current_task is not None
    cancelling_inside_timeout = None

    async def task_with_timeout() -> None:
        nonlocal cancelling_inside_timeout
        new_task = asyncio.current_task()
        assert new_task is not None
        with pytest.raises(asyncio.TimeoutError):  # noqa: PT012
            async with timeout.async_timeout(0.1, "test"):
                cancelling_inside_timeout = current_task.cancelling()
                await asyncio.sleep(0.3)

    # After the context manager exits, the task should no longer be cancelling
    assert current_task.cancelling() == 0

    task = asyncio.create_task(task_with_timeout())
    await asyncio.sleep(0)
    task.cancel()
    assert task.cancelling() == 1

    # Cancellation should not leak into the current task
    assert cancelling_inside_timeout == 0
    assert current_task.cancelling() == 0
    # Cancellation should not be swallowed if the task is cancelled
    # and it also times out
    await asyncio.sleep(0)
    with pytest.raises(asyncio.CancelledError):
        await task
    assert task.cancelling() == 1


async def test_multiple_zone_timeout() -> None:
    """Test a simple zone timeout."""
    timeout = TimeoutManager()

    with pytest.raises(TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.1, "test"):
            async with timeout.async_timeout(0.5, "test"):
                await asyncio.sleep(0.3)


async def test_different_zone_timeout() -> None:
    """Test a simple zone timeout."""
    timeout = TimeoutManager()

    with pytest.raises(TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.1, "test"):
            async with timeout.async_timeout(0.5, "other"):
                await asyncio.sleep(0.3)


async def test_simple_zone_timeout_freeze() -> None:
    """Test a simple zone timeout freeze."""
    timeout = TimeoutManager()

    async with timeout.async_timeout(0.2, "test"), timeout.async_freeze("test"):
        await asyncio.sleep(0.3)


async def test_simple_zone_timeout_freeze_without_timeout() -> None:
    """Test a simple zone timeout freeze on a zone that does not have a timeout set."""
    timeout = TimeoutManager()

    async with timeout.async_timeout(0.1, "test"), timeout.async_freeze("test"):
        await asyncio.sleep(0.3)


async def test_simple_zone_timeout_freeze_reset() -> None:
    """Test a simple zone timeout freeze reset."""
    timeout = TimeoutManager()

    with pytest.raises(TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.2, "test"):
            async with timeout.async_freeze("test"):
                await asyncio.sleep(0.1)
            await asyncio.sleep(0.2, "test")


async def test_mix_zone_timeout_freeze_and_global_freeze() -> None:
    """Test a mix zone timeout freeze and global freeze."""
    timeout = TimeoutManager()

    async with (
        timeout.async_timeout(0.2, "test"),
        timeout.async_freeze("test"),
        timeout.async_freeze(),
    ):
        await asyncio.sleep(0.3)


async def test_mix_global_and_zone_timeout_freeze() -> None:
    """Test a mix zone timeout freeze and global freeze."""
    timeout = TimeoutManager()

    async with (
        timeout.async_timeout(0.2, "test"),
        timeout.async_freeze(),
        timeout.async_freeze("test"),
    ):
        await asyncio.sleep(0.3)


async def test_mix_zone_timeout_freeze() -> None:
    """Test a mix zone timeout global freeze."""
    timeout = TimeoutManager()

    async with timeout.async_timeout(0.2, "test"), timeout.async_freeze():
        await asyncio.sleep(0.3)


async def test_mix_zone_timeout() -> None:
    """Test a mix zone timeout global."""
    timeout = TimeoutManager()

    async with timeout.async_timeout(0.1):
        with suppress(TimeoutError):
            async with timeout.async_timeout(0.2, "test"):
                await asyncio.sleep(0.4)


async def test_mix_zone_timeout_trigger_global() -> None:
    """Test a mix zone timeout global with trigger it."""
    timeout = TimeoutManager()

    with pytest.raises(TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.1):
            with suppress(TimeoutError):
                async with timeout.async_timeout(0.1, "test"):
                    await asyncio.sleep(0.3)

            await asyncio.sleep(0.3)


async def test_mix_zone_timeout_trigger_global_cool_down() -> None:
    """Test a mix zone timeout global with trigger it with cool_down."""
    timeout = TimeoutManager()

    async with timeout.async_timeout(0.1, cool_down=0.3):
        with suppress(TimeoutError):
            async with timeout.async_timeout(0.1, "test"):
                await asyncio.sleep(0.3)

        await asyncio.sleep(0.2)

    # Cleanup lingering (cool_down) task after test is done
    await asyncio.sleep(0.3)


async def test_simple_zone_timeout_freeze_without_timeout_cleanup(
    hass: HomeAssistant,
) -> None:
    """Test a simple zone timeout freeze on a zone that does not have a timeout set."""
    timeout = TimeoutManager()

    async def background():
        async with timeout.async_freeze("test"):
            await asyncio.sleep(0.4)

    async with timeout.async_timeout(0.1):
        hass.async_create_task(background())
        await asyncio.sleep(0.2)


async def test_simple_zone_timeout_freeze_without_timeout_cleanup2(
    hass: HomeAssistant,
) -> None:
    """Test a simple zone timeout freeze on a zone that does not have a timeout set."""
    timeout = TimeoutManager()

    async def background():
        async with timeout.async_freeze("test"):
            await asyncio.sleep(0.2)

    with pytest.raises(TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.1):
            hass.async_create_task(background())
            await asyncio.sleep(0.3)


async def test_simple_zone_timeout_freeze_without_timeout_exception() -> None:
    """Test a simple zone timeout freeze on a zone that does not have a timeout set."""
    timeout = TimeoutManager()

    with pytest.raises(TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.1):
            with suppress(RuntimeError):
                async with timeout.async_freeze("test"):
                    raise RuntimeError

            await asyncio.sleep(0.4)


async def test_simple_zone_timeout_zone_with_timeout_exception() -> None:
    """Test a simple zone timeout freeze on a zone that does not have a timeout set."""
    timeout = TimeoutManager()

    with pytest.raises(TimeoutError):  # noqa: PT012
        async with timeout.async_timeout(0.1):
            with suppress(RuntimeError):
                async with timeout.async_timeout(0.3, "test"):
                    raise RuntimeError

            await asyncio.sleep(0.3)


async def test_multiple_global_freezes(hass: HomeAssistant) -> None:
    """Test multiple global freezes."""
    timeout = TimeoutManager()

    async def background(delay: float) -> None:
        async with timeout.async_freeze():
            await asyncio.sleep(delay)

    async with timeout.async_timeout(0.1):
        task = hass.async_create_task(background(0.2))
        async with timeout.async_freeze():
            await asyncio.sleep(0.1)
    await task

    async with timeout.async_timeout(0.1):
        task = hass.async_create_task(background(0.2))
        async with timeout.async_freeze():
            await asyncio.sleep(0.3)
    await task
