from __future__ import annotations

import asyncio
import datetime
import signal
import time
import warnings
from asyncio import Event
from collections import Counter
from contextlib import suppress
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, patch

import pytest

from litegram import Bot
from litegram.dispatcher.dispatcher import Dispatcher
from litegram.dispatcher.event.bases import UNHANDLED, SkipHandler
from litegram.dispatcher.router import Router
from litegram.methods import GetMe, GetUpdates, SendMessage, TelegramMethod
from litegram.types import (
    BusinessConnection,
    BusinessMessagesDeleted,
    CallbackQuery,
    Chat,
    ChatBoost,
    ChatBoostRemoved,
    ChatBoostSourceGiveaway,
    ChatBoostUpdated,
    ChatJoinRequest,
    ChatMemberMember,
    ChatMemberUpdated,
    ChosenInlineResult,
    InlineQuery,
    Message,
    MessageReactionCountUpdated,
    MessageReactionUpdated,
    PaidMediaPurchased,
    Poll,
    PollAnswer,
    PollOption,
    PreCheckoutQuery,
    ReactionCount,
    ReactionTypeCustomEmoji,
    ShippingAddress,
    ShippingQuery,
    Update,
    User,
)

if TYPE_CHECKING:
    from litegram.types.error_event import ErrorEvent
    from tests.mocked_bot import MockedBot


async def simple_message_handler(message: Message):
    await asyncio.sleep(0.2)
    return message.answer("ok")


async def invalid_message_handler(message: Message):
    await asyncio.sleep(0.2)
    raise Exception(42)


RAW_UPDATE = {
    "update_id": 42,
    "message": {
        "message_id": 42,
        "date": 1582324717,
        "text": "test",
        "chat": {"id": 42, "type": "private"},
        "from": {"id": 42, "is_bot": False, "first_name": "Test"},
    },
}
UPDATE = Update(**RAW_UPDATE)


class TestDispatcher:
    def test_init(self):
        dp = Dispatcher()

        assert dp.update.handlers
        assert dp.update.handlers[0].callback == dp._listen_update
        assert dp.update.outer_middleware

    def test_init_args(self, bot: MockedBot):
        with pytest.raises(TypeError):
            Dispatcher(bot)  # type: ignore
        with pytest.raises(TypeError):
            Dispatcher(storage=bot)

    def test_data_bind(self):
        dp = Dispatcher()
        assert dp.get("foo") is None
        assert dp.get("foo", 42) == 42

        dp["foo"] = 1
        assert dp.workflow_data["foo"] == 1
        assert dp["foo"] == 1

        del dp["foo"]
        assert "foo" not in dp.workflow_data

    @pytest.mark.anyio
    async def test_storage_property(self, dispatcher: Dispatcher):
        assert dispatcher.storage is dispatcher.fsm.storage

    @pytest.mark.anyio
    async def test_parent_router(self, dispatcher: Dispatcher):
        with pytest.raises(RuntimeError):
            dispatcher.parent_router = Router()
        assert dispatcher.parent_router is None
        dispatcher._parent_router = Router()
        assert dispatcher.parent_router is None

    @pytest.mark.anyio
    async def test_feed_update(self, dispatcher: Dispatcher, bot: MockedBot):
        @dispatcher.message()
        async def my_handler(message: Message, **kwargs):
            assert "bot" in kwargs
            assert isinstance(kwargs["bot"], Bot)
            assert kwargs["bot"] == bot
            return message.text

        results_count = 0
        result = await dispatcher.feed_update(
            bot=bot,
            update=Update(
                update_id=42,
                message=Message(
                    message_id=42,
                    date=datetime.datetime.now(datetime.UTC),
                    text="test",
                    chat=Chat(id=42, type="private"),
                    from_user=User(id=42, is_bot=False, first_name="Test"),
                ),
            ),
        )
        results_count += 1
        assert result == "test"

    @pytest.mark.anyio
    async def test_feed_raw_update(self):
        dp = Dispatcher()
        bot = Bot("42:TEST")

        @dp.message()
        async def my_handler(message: Message):
            assert message.text == "test"
            return message.text

        result = await dp.feed_raw_update(
            bot=bot,
            update={
                "update_id": 42,
                "message": {
                    "message_id": 42,
                    "date": int(time.time()),
                    "text": "test",
                    "chat": {"id": 42, "type": "private"},
                    "from": {"id": 42, "is_bot": False, "first_name": "Test"},
                },
            },
        )
        assert result == "test"

    @pytest.mark.anyio
    async def test_listen_updates(self, bot: MockedBot):
        dispatcher = Dispatcher()
        bot.add_result_for(GetUpdates, ok=True, result=[Update(update_id=update_id) for update_id in range(42)])
        index = 0
        async for update in dispatcher._listen_updates(bot=bot):
            assert update.update_id == index
            index += 1
            if index == 42:
                break
        assert index == 42

    @pytest.mark.anyio
    async def test_listen_update_with_error(self, bot: MockedBot):
        dispatcher = Dispatcher()
        listen = dispatcher._listen_updates(bot=bot)
        bot.add_result_for(GetUpdates, ok=True, result=[Update(update_id=update_id) for update_id in range(42)])
        bot.add_result_for(GetUpdates, ok=False, error_code=500, description="restarting")
        with patch(
            "litegram.utils.backoff.Backoff.asleep",
            new_callable=AsyncMock,
        ) as mocked_asleep:
            assert isinstance(await anext(listen), Update)
            assert mocked_asleep.awaited

    @pytest.mark.anyio
    async def test_silent_call_request(self, bot: MockedBot, caplog):
        dispatcher = Dispatcher()
        bot.add_result_for(SendMessage, ok=False, error_code=400, description="Kaboom")
        await dispatcher.silent_call_request(bot, SendMessage(chat_id=42, text="test"))
        log_records = [rec.message for rec in caplog.records]
        assert len(log_records) == 1
        assert "Failed to make answer" in log_records[0]

    @pytest.mark.anyio
    async def test_process_update_empty(self, bot: MockedBot):
        dispatcher = Dispatcher()

        with pytest.warns(RuntimeWarning, match="Detected unknown update type"):
            result = await dispatcher._process_update(bot=bot, update=Update(update_id=42))
            assert not result

    @pytest.mark.anyio
    async def test_process_update_handled(self, bot: MockedBot):
        dispatcher = Dispatcher()

        @dispatcher.update()
        async def update_handler(update: Update):
            pass

        with pytest.warns(RuntimeWarning, match="Detected unknown update type"):
            assert await dispatcher._process_update(bot=bot, update=Update(update_id=42))

    @pytest.mark.parametrize(
        "event_type,update,has_chat,has_user",
        [
            pytest.param(
                "message",
                Update(
                    update_id=42,
                    message=Message(
                        message_id=42,
                        date=datetime.datetime.now(datetime.UTC),
                        text="test",
                        chat=Chat(id=42, type="private"),
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                    ),
                ),
                True,
                True,
            ),
            pytest.param(
                "edited_message",
                Update(
                    update_id=42,
                    edited_message=Message(
                        message_id=42,
                        date=datetime.datetime.now(datetime.UTC),
                        text="edited test",
                        chat=Chat(id=42, type="private"),
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                    ),
                ),
                True,
                True,
            ),
            pytest.param(
                "channel_post",
                Update(
                    update_id=42,
                    channel_post=Message(
                        message_id=42,
                        date=datetime.datetime.now(datetime.UTC),
                        text="test",
                        chat=Chat(id=-42, type="private"),
                    ),
                ),
                True,
                False,
            ),
            pytest.param(
                "edited_channel_post",
                Update(
                    update_id=42,
                    edited_channel_post=Message(
                        message_id=42,
                        date=datetime.datetime.now(datetime.UTC),
                        text="test",
                        chat=Chat(id=-42, type="private"),
                    ),
                ),
                True,
                False,
            ),
            pytest.param(
                "inline_query",
                Update(
                    update_id=42,
                    inline_query=InlineQuery(
                        id="query id",
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                        query="query",
                        offset="offser",
                    ),
                ),
                False,
                True,
            ),
            pytest.param(
                "chosen_inline_result",
                Update(
                    update_id=42,
                    chosen_inline_result=ChosenInlineResult(
                        result_id="result id",
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                        query="query",
                    ),
                ),
                False,
                True,
            ),
            pytest.param(
                "callback_query",
                Update(
                    update_id=42,
                    callback_query=CallbackQuery(
                        id="query id",
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                        chat_instance="instance",
                        data="placeholder",
                    ),
                ),
                False,
                True,
            ),
            pytest.param(
                "callback_query",
                Update(
                    update_id=42,
                    callback_query=CallbackQuery(
                        id="query id",
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                        chat_instance="instance",
                        data="placeholder",
                        message=Message(
                            message_id=42,
                            date=datetime.datetime.now(datetime.UTC),
                            text="test",
                            chat=Chat(id=42, type="private"),
                            from_user=User(id=42, is_bot=False, first_name="Test"),
                        ),
                    ),
                ),
                True,
                True,
            ),
            pytest.param(
                "shipping_query",
                Update(
                    update_id=42,
                    shipping_query=ShippingQuery(
                        id="id",
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                        invoice_payload="payload",
                        shipping_address=ShippingAddress(
                            country_code="placeholder",
                            state="placeholder",
                            city="placeholder",
                            street_line1="placeholder",
                            street_line2="placeholder",
                            post_code="placeholder",
                        ),
                    ),
                ),
                False,
                True,
            ),
            pytest.param(
                "pre_checkout_query",
                Update(
                    update_id=42,
                    pre_checkout_query=PreCheckoutQuery(
                        id="query id",
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                        currency="BTC",
                        total_amount=1,
                        invoice_payload="payload",
                    ),
                ),
                False,
                True,
            ),
            pytest.param(
                "poll",
                Update(
                    update_id=42,
                    poll=Poll(
                        id="poll id",
                        question="Q?",
                        options=[
                            PollOption(text="A1", voter_count=2),
                            PollOption(text="A2", voter_count=3),
                        ],
                        is_closed=False,
                        is_anonymous=False,
                        type="quiz",
                        allows_multiple_answers=False,
                        total_voter_count=0,
                        correct_option_id=1,
                    ),
                ),
                False,
                False,
            ),
            pytest.param(
                "poll_answer",
                Update(
                    update_id=42,
                    poll_answer=PollAnswer(
                        poll_id="poll id",
                        user=User(id=42, is_bot=False, first_name="Test"),
                        option_ids=[42],
                    ),
                ),
                False,
                True,
            ),
            pytest.param(
                "my_chat_member",
                Update(
                    update_id=42,
                    my_chat_member=ChatMemberUpdated(
                        chat=Chat(id=42, type="private"),
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                        date=datetime.datetime.now(datetime.UTC),
                        old_chat_member=ChatMemberMember(user=User(id=42, is_bot=False, first_name="Test")),
                        new_chat_member=ChatMemberMember(user=User(id=42, is_bot=False, first_name="Test")),
                    ),
                ),
                True,
                True,
            ),
            pytest.param(
                "chat_member",
                Update(
                    update_id=42,
                    chat_member=ChatMemberUpdated(
                        chat=Chat(id=42, type="private"),
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                        date=datetime.datetime.now(datetime.UTC),
                        old_chat_member=ChatMemberMember(user=User(id=42, is_bot=False, first_name="Test")),
                        new_chat_member=ChatMemberMember(user=User(id=42, is_bot=False, first_name="Test")),
                    ),
                ),
                True,
                True,
            ),
            pytest.param(
                "chat_join_request",
                Update(
                    update_id=42,
                    chat_join_request=ChatJoinRequest(
                        chat=Chat(id=-42, type="private"),
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                        user_chat_id=42,
                        date=datetime.datetime.now(datetime.UTC),
                    ),
                ),
                True,
                True,
            ),
            pytest.param(
                "message_reaction",
                Update(
                    update_id=42,
                    message_reaction=MessageReactionUpdated(
                        chat=Chat(id=-42, type="channel"),
                        message_id=12345,
                        user=User(id=42, is_bot=False, first_name="Test"),
                        date=datetime.datetime.now(datetime.UTC),
                        old_reaction=[],
                        new_reaction=[ReactionTypeCustomEmoji(custom_emoji_id="qwerty")],
                    ),
                ),
                True,
                True,
            ),
            pytest.param(
                "message_reaction_count",
                Update(
                    update_id=42,
                    message_reaction_count=MessageReactionCountUpdated(
                        chat=Chat(id=-42, type="channel"),
                        message_id=12345,
                        date=datetime.datetime.now(datetime.UTC),
                        reactions=[
                            ReactionCount(
                                type=ReactionTypeCustomEmoji(custom_emoji_id="qwerty"),
                                total_count=123,
                            ),
                        ],
                    ),
                ),
                True,
                False,
            ),
            pytest.param(
                "chat_boost",
                Update(
                    update_id=42,
                    chat_boost=ChatBoostUpdated(
                        chat=Chat(id=-42, type="channel"),
                        boost=ChatBoost(
                            boost_id="qwerty",
                            add_date=datetime.datetime.now(datetime.UTC),
                            expiration_date=datetime.datetime.now(datetime.UTC),
                            source=ChatBoostSourceGiveaway(
                                giveaway_message_id=77,
                            ),
                        ),
                    ),
                ),
                True,
                False,
            ),
            pytest.param(
                "removed_chat_boost",
                Update(
                    update_id=42,
                    removed_chat_boost=ChatBoostRemoved(
                        chat=Chat(id=-42, type="channel"),
                        boost_id="qwerty",
                        remove_date=datetime.datetime.now(datetime.UTC),
                        source=ChatBoostSourceGiveaway(
                            giveaway_message_id=77,
                        ),
                    ),
                ),
                True,
                False,
            ),
            pytest.param(
                "deleted_business_messages",
                Update(
                    update_id=42,
                    deleted_business_messages=BusinessMessagesDeleted(
                        chat=Chat(id=-42, type="private"),
                        business_connection_id="qwerty",
                        message_ids=[1, 2, 3],
                    ),
                ),
                True,
                False,
            ),
            pytest.param(
                "business_connection",
                Update(
                    update_id=42,
                    business_connection=BusinessConnection(
                        id="qwerty",
                        user=User(id=42, is_bot=False, first_name="Test"),
                        user_chat_id=42,
                        date=datetime.datetime.now(datetime.UTC),
                        can_reply=True,
                        is_enabled=True,
                    ),
                ),
                False,
                True,
            ),
            pytest.param(
                "edited_business_message",
                Update(
                    update_id=42,
                    edited_business_message=Message(
                        message_id=42,
                        date=datetime.datetime.now(datetime.UTC),
                        text="test",
                        chat=Chat(id=42, type="private"),
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                    ),
                ),
                True,
                True,
            ),
            pytest.param(
                "business_message",
                Update(
                    update_id=42,
                    business_message=Message(
                        message_id=42,
                        date=datetime.datetime.now(datetime.UTC),
                        text="test",
                        chat=Chat(id=42, type="private"),
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                    ),
                ),
                True,
                True,
            ),
            pytest.param(
                "purchased_paid_media",
                Update(
                    update_id=42,
                    purchased_paid_media=PaidMediaPurchased(
                        paid_media_payload="payload",
                        from_user=User(id=42, is_bot=False, first_name="Test"),
                    ),
                ),
                False,
                True,
            ),
        ],
    )
    @pytest.mark.anyio
    async def test_listen_update(
        self,
        event_type: str,
        update: Update,
        has_chat: bool,
        has_user: bool,
        bot: MockedBot,
    ):
        router = Dispatcher()
        observer = router.observers[event_type]

        @observer()
        async def my_handler(event: Any, **kwargs: Any):
            assert event.model_dump(exclude_defaults=True) == getattr(update, event_type).model_dump(exclude_defaults=True)
            if has_chat:
                assert kwargs["event_chat"]
            if has_user:
                assert kwargs["event_from_user"]
            return kwargs

        result = await router.feed_update(bot, update, test="PASS")
        assert isinstance(result, dict)
        assert result["event_update"].model_dump(exclude_defaults=True) == update.model_dump(exclude_defaults=True)
        assert result["event_router"] == router
        assert result["test"] == "PASS"

    @pytest.mark.anyio
    async def test_listen_unknown_update(self):
        dp = Dispatcher()
        pattern = "Detected unknown update type"
        with pytest.raises(SkipHandler), pytest.warns(RuntimeWarning, match=pattern) as record:
            await dp._listen_update(Update(update_id=42))
            if not record:
                pytest.fail("Expected 'Detected unknown update type' warning.")

    @pytest.mark.anyio
    async def test_listen_unhandled_update(self):
        dp = Dispatcher()
        observer = dp.observers["message"]

        @observer(lambda event: False)
        async def handler(event: Any):
            pass

        response = await dp._listen_update(
            Update(
                update_id=42,
                poll=Poll(
                    id="poll id",
                    question="Q?",
                    options=[
                        PollOption(text="A1", voter_count=2),
                        PollOption(text="A2", voter_count=3),
                    ],
                    is_closed=False,
                    is_anonymous=False,
                    type="quiz",
                    allows_multiple_answers=False,
                    total_voter_count=0,
                    correct_option_id=0,
                ),
            )
        )
        assert response is UNHANDLED

    @pytest.mark.anyio
    async def test_nested_router_listen_update(self, bot: MockedBot):
        dp = Dispatcher()
        router0 = Router()
        router1 = Router()
        dp.include_router(router0)
        router0.include_router(router1)
        observer = router1.message

        @observer()
        async def my_handler(event: Message, **kwargs: Any):
            return kwargs

        update = Update(
            update_id=42,
            message=Message(
                message_id=42,
                date=datetime.datetime.now(datetime.UTC),
                text="test",
                chat=Chat(id=42, type="private"),
                from_user=User(id=42, is_bot=False, first_name="Test"),
            ),
        )
        result = await dp.feed_update(bot, update, test="PASS")
        assert isinstance(result, dict)
        assert result["event_update"].model_dump(exclude_defaults=True) == update.model_dump(exclude_defaults=True)
        assert result["event_router"] == router1
        assert result["test"] == "PASS"

    @pytest.mark.anyio
    async def test_nested_router_middleware_resolution(self, bot: MockedBot):
        counter = Counter()

        def mw(type_: str, inject_data: dict):
            async def middleware(h, event, data):
                counter[type_] += 1
                data.update(inject_data)
                return await h(event, data)

            return middleware

        async def handler(event, foo, bar, baz, fizz, buzz):
            counter["child.handler"] += 1

        root = Dispatcher()
        child = Router()

        root.message.outer_middleware(mw("root.outer_middleware", {"foo": True}))
        root.message.middleware(mw("root.middleware", {"bar": None}))
        child.message.outer_middleware(mw("child.outer_middleware", {"fizz": 42}))
        child.message.middleware(mw("child.middleware", {"buzz": -42}))
        child.message.register(handler)

        root.include_router(child)
        await root.feed_update(
            bot=bot,
            update=Update(
                update_id=42,
                message=Message(
                    message_id=42,
                    date=datetime.datetime.fromtimestamp(0, datetime.UTC),
                    chat=Chat(id=-42, type="group"),
                ),
            ),
            baz=...,
        )

        assert counter["root.outer_middleware"] == 1
        assert counter["root.middleware"] == 1
        assert counter["child.outer_middleware"] == 1
        assert counter["child.middleware"] == 1
        assert counter["child.handler"] == 1

    @pytest.mark.anyio
    async def test_process_update_call_request(self, bot: MockedBot):
        dispatcher = Dispatcher()

        @dispatcher.update()
        async def message_handler(update: Update):
            return GetMe()

        dispatcher.update.handlers.reverse()

        with patch(
            "litegram.dispatcher.dispatcher.Dispatcher.silent_call_request",
            new_callable=AsyncMock,
        ) as mocked_silent_call_request:
            await dispatcher._process_update(bot=bot, update=Update(update_id=42))
            mocked_silent_call_request.assert_awaited()

    @pytest.mark.anyio
    async def test_process_update_exception(self, bot: MockedBot, caplog):
        dispatcher = Dispatcher()

        @dispatcher.update()
        async def update_handler(update: Update):
            raise Exception("Kaboom!")

        with pytest.warns(RuntimeWarning, match="Detected unknown update type"):
            assert await dispatcher._process_update(bot=bot, update=Update(update_id=42))

        assert any("Cause exception while process update" in rec.message for rec in caplog.records)

    @pytest.mark.parametrize("as_task", [True, False])
    @pytest.mark.anyio
    async def test_polling(self, bot: MockedBot, as_task: bool):
        dispatcher = Dispatcher()

        async def _mock_updates(*_):
            yield Update(update_id=42)

        with (
            patch("litegram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock) as mocked_process_update,
            patch("litegram.dispatcher.dispatcher.Dispatcher._listen_updates") as patched_listen_updates,
        ):
            patched_listen_updates.return_value = _mock_updates()
            await dispatcher._polling(bot=bot, handle_as_tasks=as_task)
            if as_task:
                pass
            else:
                mocked_process_update.assert_awaited()

    @pytest.mark.parametrize(
        "handle_as_tasks,tasks_concurrency_limit,should_create_semaphore",
        [
            (True, 10, True),
            (True, None, False),
            (False, 10, False),
            (False, None, False),
        ],
    )
    @pytest.mark.anyio
    async def test_polling_with_semaphore(
        self,
        bot: MockedBot,
        handle_as_tasks: bool,
        tasks_concurrency_limit: int,
        should_create_semaphore: bool,
    ):
        """Test that semaphore is created only when handle_as_tasks=True and tasks_concurrency_limit is not None"""
        dispatcher = Dispatcher()

        async def _mock_updates(*_):
            yield Update(update_id=42)

        with (
            patch("litegram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock),
            patch("litegram.dispatcher.dispatcher.Dispatcher._listen_updates") as patched_listen_updates,
            patch("asyncio.Semaphore") as mocked_semaphore,
        ):
            patched_listen_updates.return_value = _mock_updates()

            # Set up the mock semaphore
            if should_create_semaphore:
                mock_semaphore_instance = AsyncMock()
                mock_semaphore_instance.acquire.return_value = asyncio.Future()
                mock_semaphore_instance.acquire.return_value.set_result(None)
                mocked_semaphore.return_value = mock_semaphore_instance

            await dispatcher._polling(
                bot=bot,
                handle_as_tasks=handle_as_tasks,
                tasks_concurrency_limit=tasks_concurrency_limit,
            )

            if should_create_semaphore:
                mocked_semaphore.assert_called_once_with(tasks_concurrency_limit)
            else:
                mocked_semaphore.assert_not_called()

    @pytest.mark.anyio
    async def test_process_with_semaphore(self):
        """Test that _process_with_semaphore correctly processes updates and releases the semaphore"""
        dispatcher = Dispatcher()

        # Create a real coroutine for handle_update
        async def mock_handle_update():
            return "test result"

        # Create a mock for the semaphore
        semaphore = AsyncMock()
        semaphore.release = AsyncMock()

        # Call the _process_with_semaphore method
        await dispatcher._process_with_semaphore(mock_handle_update(), semaphore)

        # Verify that semaphore.release was called, which indicates that the coroutine was awaited
        semaphore.release.assert_called_once()

    @pytest.mark.anyio
    async def test_process_with_semaphore_exception(self):
        """Test that _process_with_semaphore releases the semaphore even if an exception occurs"""
        dispatcher = Dispatcher()

        # Create a real coroutine that raises an exception
        async def mock_handle_update_with_exception():
            raise Exception("Test exception")

        # Create a mock for the semaphore
        semaphore = AsyncMock()
        semaphore.release = AsyncMock()

        # Call the _process_with_semaphore method and expect an exception
        with pytest.raises(Exception, match="Test exception"):
            await dispatcher._process_with_semaphore(mock_handle_update_with_exception(), semaphore)

        # Verify that semaphore.release was called even though an exception occurred
        semaphore.release.assert_called_once()

    @pytest.mark.anyio
    async def test_concurrent_updates_limit(self, bot: MockedBot):
        """Test that concurrent updates are limited when using the semaphore"""
        dispatcher = Dispatcher()
        tasks_concurrency_limit = 2

        # Create a real semaphore for this test
        semaphore = asyncio.Semaphore(tasks_concurrency_limit)

        # Create a list to track when updates are processed
        processed_updates = []

        async def mock_process_update(*args, **kwargs):
            # Record that an update is being processed
            update_id = len(processed_updates)
            processed_updates.append(f"start_{update_id}")
            # Simulate some processing time
            await asyncio.sleep(0.1)
            processed_updates.append(f"end_{update_id}")
            return True

        # Create mock updates
        async def _mock_updates(*_):
            for i in range(5):  # Send 5 updates
                yield Update(update_id=i)

        with (
            patch(
                "litegram.dispatcher.dispatcher.Dispatcher._process_update",
                side_effect=mock_process_update,
            ),
            patch("litegram.dispatcher.dispatcher.Dispatcher._listen_updates") as patched_listen_updates,
            patch(
                "asyncio.Semaphore",
                return_value=semaphore,
            ),
        ):
            patched_listen_updates.return_value = _mock_updates()

            # Start polling with concurrent_updates_limit
            polling_task = asyncio.create_task(
                dispatcher._polling(bot=bot, handle_as_tasks=True, tasks_concurrency_limit=tasks_concurrency_limit)
            )

            # Wait longer for all updates to be processed
            await asyncio.sleep(0.6)

            # Cancel the polling task
            polling_task.cancel()
            with suppress(asyncio.CancelledError):
                await polling_task

            # Check that at most concurrent_updates_limit updates were being processed at the same time
            # This is a bit tricky to test precisely, but we can check that we have the expected number
            # of start/end pairs in the processed_updates list
            starts = [item for item in processed_updates if item.startswith("start_")]
            ends = [item for item in processed_updates if item.startswith("end_")]

            # We should have an equal number of starts and ends
            assert len(starts) == len(ends)

            # The semaphore should have been acquired and released the same number of times
            assert semaphore._value == tasks_concurrency_limit

    @pytest.mark.anyio
    async def test_exception_handler_catch_exceptions(self, bot: MockedBot):
        dp = Dispatcher()
        router = Router()
        dp.include_router(router)

        class CustomException(Exception):
            pass

        @router.message()
        async def message_handler(message: Message):
            raise CustomException("KABOOM")

        update = Update(
            update_id=42,
            message=Message(
                message_id=42,
                date=datetime.datetime.now(datetime.UTC),
                text="test",
                chat=Chat(id=42, type="private"),
                from_user=User(id=42, is_bot=False, first_name="Test"),
            ),
        )
        with pytest.raises(CustomException, match="KABOOM"):
            await dp.feed_update(bot, update)

        @router.errors()
        async def error_handler(event: ErrorEvent):
            return "KABOOM"

        response = await dp.feed_update(bot, update)
        assert response == "KABOOM"

        @dp.errors()
        async def root_error_handler(event: ErrorEvent):
            return event.exception

        response = await dp.feed_update(bot, update)

        assert isinstance(response, CustomException)
        assert str(response) == "KABOOM"

    @pytest.mark.anyio
    async def test_start_polling(self, bot: MockedBot):
        dispatcher = Dispatcher()
        dispatcher.workflow_data["bot"] = 42
        with pytest.raises(ValueError, match="At least one bot instance is required to start polling"):
            await dispatcher.start_polling()
        with pytest.raises(
            ValueError,
            match="Keyword argument 'bot' is not acceptable, the bot instance should be passed as positional argument",
        ):
            await dispatcher.start_polling(bot, bot=bot)

        bot.add_result_for(GetMe, ok=True, result=User(id=42, is_bot=True, first_name="The bot", username="tbot"))

        async def _mock_updates(*_):
            yield Update(update_id=42)

        with (
            patch("litegram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock) as mocked_process_update,
            patch("litegram.dispatcher.router.Router.emit_startup", new_callable=AsyncMock) as mocked_emit_startup,
            patch("litegram.dispatcher.router.Router.emit_shutdown", new_callable=AsyncMock) as mocked_emit_shutdown,
            patch("litegram.dispatcher.dispatcher.Dispatcher._listen_updates") as patched_listen_updates,
        ):
            patched_listen_updates.return_value = _mock_updates()
            await dispatcher.start_polling(bot)

            mocked_emit_startup.assert_awaited()
            mocked_process_update.assert_awaited()
            mocked_emit_shutdown.assert_awaited()
            assert dispatcher.workflow_data["bot"] == 42
            assert mocked_emit_shutdown.call_args.kwargs["bot"] == bot

    @pytest.mark.anyio
    async def test_stop_polling(self):
        dispatcher = Dispatcher()
        with pytest.raises(RuntimeError):
            await dispatcher.stop_polling()

        assert not dispatcher._stop_signal
        assert not dispatcher._stopped_signal
        with patch("asyncio.locks.Event.wait", new_callable=AsyncMock) as mocked_wait:
            async with dispatcher._running_lock:
                await dispatcher.stop_polling()
                assert not dispatcher._stop_signal

                dispatcher._stop_signal = Event()
                dispatcher._stopped_signal = Event()
                await dispatcher.stop_polling()
                assert dispatcher._stop_signal.is_set()
                mocked_wait.assert_awaited()

    @pytest.mark.anyio
    async def test_signal_stop_polling(self):
        dispatcher = Dispatcher()
        with patch("asyncio.locks.Event.set") as mocked_set:
            dispatcher._signal_stop_polling(signal.SIGINT)
            mocked_set.assert_not_called()

            async with dispatcher._running_lock:
                dispatcher._signal_stop_polling(signal.SIGINT)
                mocked_set.assert_not_called()

                dispatcher._stop_signal = Event()
                dispatcher._stopped_signal = Event()
                dispatcher._signal_stop_polling(signal.SIGINT)
                mocked_set.assert_called()

    @pytest.mark.anyio
    async def test_stop_polling_by_method(self, bot: MockedBot):
        dispatcher = Dispatcher()
        bot.add_result_for(GetMe, ok=True, result=User(id=42, is_bot=True, first_name="The bot", username="tbot"))
        running = Event()

        async def _mock_updates(*_):
            running.set()
            while True:
                yield Update(update_id=42)
                await asyncio.sleep(1)

        with (
            patch("litegram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock) as mocked_process_update,
            patch(
                "litegram.dispatcher.dispatcher.Dispatcher._listen_updates",
                return_value=_mock_updates(),
            ),
        ):
            task = asyncio.ensure_future(dispatcher.start_polling(bot))
            await running.wait()

            assert not dispatcher._stop_signal.is_set()
            assert not dispatcher._stopped_signal.is_set()

            await dispatcher.stop_polling()
            assert dispatcher._stop_signal.is_set()
            assert dispatcher._stopped_signal.is_set()
            assert not task.exception()

            mocked_process_update.assert_awaited()

    @pytest.mark.anyio
    async def test_stop_polling_by_signal(self, bot: MockedBot):
        dispatcher = Dispatcher()
        bot.add_result_for(GetMe, ok=True, result=User(id=42, is_bot=True, first_name="The bot", username="tbot"))
        running = Event()

        async def _mock_updates(*_):
            running.set()
            while True:
                yield Update(update_id=42)
                await asyncio.sleep(1)

        with (
            patch("litegram.dispatcher.dispatcher.Dispatcher._process_update", new_callable=AsyncMock) as mocked_process_update,
            patch(
                "litegram.dispatcher.dispatcher.Dispatcher._listen_updates",
                return_value=_mock_updates(),
            ),
        ):
            task = asyncio.ensure_future(dispatcher.start_polling(bot))
            await running.wait()

            assert not dispatcher._stop_signal.is_set()
            assert not dispatcher._stopped_signal.is_set()

            dispatcher._signal_stop_polling(signal.SIGINT)
            await task

            assert dispatcher._stop_signal.is_set()
            assert dispatcher._stopped_signal.is_set()
            assert not task.exception()

            mocked_process_update.assert_awaited()

    def test_run_polling(self, bot: MockedBot):
        dispatcher = Dispatcher()

        async def stop():
            await asyncio.sleep(0.5)
            await dispatcher.stop_polling()

        start_called = False

        @dispatcher.startup()
        async def startup():
            nonlocal start_called
            start_called = True
            asyncio.create_task(stop())

        original_start_polling = dispatcher.start_polling
        with patch(
            "litegram.dispatcher.dispatcher.Dispatcher.start_polling",
            side_effect=original_start_polling,
        ) as patched_start_polling:
            dispatcher.run_polling(bot)
            patched_start_polling.assert_awaited_once()

        assert start_called

    @pytest.mark.anyio
    async def test_feed_webhook_update_fast_process(self, bot: MockedBot):
        dispatcher = Dispatcher()
        dispatcher.message.register(simple_message_handler)

        response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.3)
        assert isinstance(response, TelegramMethod)

    @pytest.mark.anyio
    async def test_feed_webhook_update_slow_process(self, bot: MockedBot, recwarn):
        warnings.simplefilter("always")

        dispatcher = Dispatcher()
        dispatcher.message.register(simple_message_handler)

        with patch(
            "litegram.dispatcher.dispatcher.Dispatcher.silent_call_request",
            new_callable=AsyncMock,
        ) as mocked_silent_call_request:
            response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.1)
            assert response is None
            await asyncio.sleep(0.5)
            mocked_silent_call_request.assert_awaited()

    @pytest.mark.anyio
    async def test_feed_webhook_update_fast_process_error(self, bot: MockedBot, caplog):
        warnings.simplefilter("always")

        dispatcher = Dispatcher()
        dispatcher.message.register(invalid_message_handler)

        pattern = r"Detected slow response into webhook"
        with pytest.warns(RuntimeWarning, match=pattern) as record:
            response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.1)
            assert response is None
            await asyncio.sleep(0.5)

            assert any("Cause exception while process update" in rec.message for rec in caplog.records)

            if not record:
                pytest.fail("Expected 'Detected slow response into webhook' warning.")

    def test_specify_updates_calculation(self):
        def simple_msg_handler() -> None: ...

        def simple_callback_query_handler() -> None: ...

        def simple_poll_handler() -> None: ...

        def simple_edited_msg_handler() -> None: ...

        dispatcher = Dispatcher()
        dispatcher.message.register(simple_msg_handler)

        router1 = Router()
        router1.callback_query.register(simple_callback_query_handler)

        router2 = Router()
        router2.poll.register(simple_poll_handler)

        router21 = Router()
        router21.edited_message.register(simple_edited_msg_handler)

        useful_updates1 = dispatcher.resolve_used_update_types()

        assert sorted(useful_updates1) == sorted(["message"])

        dispatcher.include_router(router1)

        useful_updates2 = dispatcher.resolve_used_update_types()

        assert sorted(useful_updates2) == sorted(["message", "callback_query"])

        dispatcher.include_router(router2)

        useful_updates3 = dispatcher.resolve_used_update_types()

        assert sorted(useful_updates3) == sorted(["message", "callback_query", "poll"])

        router2.include_router(router21)

        useful_updates4 = dispatcher.resolve_used_update_types()

        assert sorted(useful_updates4) == sorted(["message", "callback_query", "poll", "edited_message"])

        useful_updates5 = router2.resolve_used_update_types()

        assert sorted(useful_updates5) == sorted(["poll", "edited_message"])
