import asyncio
import sys
from unittest.mock import AsyncMock

from pytest import raises

if sys.version_info < (3, 11):
    from exceptiongroup import BaseExceptionGroup

from aioservicekit.groups import TaskGroup


async def test_initial_state():
    group = TaskGroup()
    assert len(group.__tasks__) == 0
    assert len(group.__uncanceliable_tasks__) == 0
    assert len(group.__errors__) == 0
    assert group.__error_tolerance__ is False


async def test_create_task():
    async def dummy_coro():
        return 42

    group = TaskGroup()
    task = group.create_task(dummy_coro())

    assert isinstance(task, asyncio.Task)
    assert task in group.__tasks__
    assert task not in group.__uncanceliable_tasks__

    result = await task
    assert result == 42
    assert task not in group.__tasks__  # Task should be removed after completion


async def test_create_uncancelable_task():
    async def dummy_coro():
        return 42

    group = TaskGroup()
    task = group.create_task(dummy_coro(), canceliable=False)

    assert isinstance(task, asyncio.Task)
    assert task not in group.__tasks__
    assert task in group.__uncanceliable_tasks__

    result = await task
    assert result == 42
    assert task not in group.__uncanceliable_tasks__


async def test_error_handling_without_tolerance():
    error_handler = AsyncMock()

    async def failing_coro():
        raise ValueError("test error")

    async def good_coro():
        await asyncio.sleep(0.1)
        return 42

    group = TaskGroup(error_tolerance=False)
    group.on_error.add_listener(error_handler)

    group.create_task(failing_coro())
    good_task = group.create_task(good_coro())

    with raises(BaseExceptionGroup) as exc_info:
        await group.wait()

    assert len(exc_info.value.exceptions) == 1
    assert isinstance(exc_info.value.exceptions[0], ValueError)
    assert str(exc_info.value.exceptions[0]) == "test error"

    error_handler.assert_awaited_once()
    assert good_task.cancelled()


async def test_error_handling_with_tolerance():
    error_handler = AsyncMock()

    async def failing_coro():
        raise ValueError("test error")

    async def good_coro():
        await asyncio.sleep(0.1)
        return 42

    group = TaskGroup(error_tolerance=True)
    group.on_error.add_listener(error_handler)

    group.create_task(failing_coro())
    good_task = group.create_task(good_coro())

    await group.wait()  # Should not raise

    error_handler.assert_awaited_once()
    assert not good_task.cancelled()
    assert good_task.result() == 42


async def test_context_manager():
    results = []

    async def task_coro():
        await asyncio.sleep(0.1)
        results.append(42)

    async with TaskGroup() as group:
        group.create_task(task_coro())
        group.create_task(task_coro())

    assert results == [42, 42]


async def test_cancel():
    cancel_count = 0

    async def cancelable_coro():
        nonlocal cancel_count
        try:
            await asyncio.sleep(999)
        except asyncio.CancelledError:
            cancel_count += 1
            raise

    async def uncancelable_coro():
        await asyncio.sleep(1)
        return 42

    group = TaskGroup()

    # Create mix of cancelable and uncancelable tasks
    group.create_task(cancelable_coro())
    group.create_task(cancelable_coro())
    uncancelable_task = group.create_task(uncancelable_coro(), canceliable=False)
    await asyncio.sleep(0)  # Run tasks

    group.cancel()
    await group.wait()

    assert cancel_count == 2  # Both cancelable tasks should have been cancelled
    assert uncancelable_task.result() == 42  # Uncancelable task should complete


async def test_reset_errors():
    async def failing_coro():
        raise ValueError("test error")

    group = TaskGroup(error_tolerance=True)
    group.create_task(failing_coro())

    await group.wait()
    assert len(group.errors) == 0  # Error tolerance means errors aren't collected

    # Manually add an error and test reset
    group.__errors__.append(ValueError())
    assert len(group.errors) == 1

    group.reset_errors()
    assert len(group.errors) == 0


async def test_aexit_with_exception():
    """Test that __aexit__ re-raises exception after waiting"""

    async def task_coro():
        await asyncio.sleep(0.1)
        return 42

    group = TaskGroup()
    group.create_task(task_coro())

    with raises(ValueError):
        async with group:
            raise ValueError("test exception")


async def test_task_exception_not_cancelled_error():
    """Test __on_task_done__ handles getting exception that is not CancelledError"""

    async def failing_coro():
        raise RuntimeError("test error")

    group = TaskGroup(error_tolerance=False)
    error_handler = AsyncMock()
    group.on_error.add_listener(error_handler)

    group.create_task(failing_coro())
    await asyncio.sleep(0.1)
