from typing import Any

import anyio
import pytest

from yee88.telegram.api_models import (
    Chat,
    ChatMember,
    File,
    ForumTopic,
    Message,
    Update,
    User,
)
from yee88.telegram.client import BotClient, TelegramClient, TelegramRetryAfter


class FakeBot(BotClient):
    def __init__(self) -> None:
        self.calls: list[str] = []
        self.edit_calls: list[str] = []
        self.delete_calls: list[tuple[int, int]] = []
        self.topic_calls: list[tuple[int, int, str]] = []
        self.document_calls: list[
            tuple[int, str, bytes, int | None, int | None, bool | None, str | None]
        ] = []
        self.command_calls: list[
            tuple[list[dict[str, Any]], dict[str, Any] | None, str | None]
        ] = []
        self.callback_calls: list[tuple[str, str | None, bool | None]] = []
        self.chat_calls: list[int] = []
        self.chat_member_calls: list[tuple[int, int]] = []
        self.create_topic_calls: list[tuple[int, str]] = []
        self._edit_attempts = 0
        self._updates_attempts = 0
        self.retry_after: float | None = None
        self.updates_retry_after: float | None = None

    async def send_message(
        self,
        chat_id: int,
        text: str,
        reply_to_message_id: int | None = None,
        disable_notification: bool | None = False,
        message_thread_id: int | None = None,
        entities: list[dict[str, Any]] | None = None,
        parse_mode: str | None = None,
        reply_markup: dict | None = None,
        *,
        replace_message_id: int | None = None,
    ) -> Message | None:
        _ = reply_to_message_id
        _ = disable_notification
        _ = message_thread_id
        _ = entities
        _ = parse_mode
        _ = reply_markup
        _ = replace_message_id
        self.calls.append("send_message")
        return Message(message_id=1, chat=Chat(id=chat_id, type="private"))

    async def send_document(
        self,
        chat_id: int,
        filename: str,
        content: bytes,
        reply_to_message_id: int | None = None,
        message_thread_id: int | None = None,
        disable_notification: bool | None = False,
        caption: str | None = None,
    ) -> Message | None:
        self.calls.append("send_document")
        self.document_calls.append(
            (
                chat_id,
                filename,
                content,
                reply_to_message_id,
                message_thread_id,
                disable_notification,
                caption,
            )
        )
        return Message(message_id=1, chat=Chat(id=chat_id, type="private"))

    async def edit_message_text(
        self,
        chat_id: int,
        message_id: int,
        text: str,
        entities: list[dict[str, Any]] | None = None,
        parse_mode: str | None = None,
        reply_markup: dict | None = None,
        *,
        wait: bool = True,
    ) -> Message | None:
        _ = chat_id
        _ = message_id
        _ = entities
        _ = parse_mode
        _ = reply_markup
        _ = wait
        self.calls.append("edit_message_text")
        self.edit_calls.append(text)
        if self.retry_after is not None and self._edit_attempts == 0:
            self._edit_attempts += 1
            raise TelegramRetryAfter(self.retry_after)
        self._edit_attempts += 1
        return Message(message_id=message_id, chat=Chat(id=chat_id, type="private"))

    async def delete_message(
        self,
        chat_id: int,
        message_id: int,
    ) -> bool:
        self.calls.append("delete_message")
        self.delete_calls.append((chat_id, message_id))
        return True

    async def set_my_commands(
        self,
        commands: list[dict[str, Any]],
        *,
        scope: dict[str, Any] | None = None,
        language_code: str | None = None,
    ) -> bool:
        self.calls.append("set_my_commands")
        self.command_calls.append((commands, scope, language_code))
        return True

    async def get_updates(
        self,
        offset: int | None,
        timeout_s: int = 50,
        allowed_updates: list[str] | None = None,
    ) -> list[Update] | None:
        _ = offset
        _ = timeout_s
        _ = allowed_updates
        if self.updates_retry_after is not None and self._updates_attempts == 0:
            self._updates_attempts += 1
            raise TelegramRetryAfter(self.updates_retry_after)
        self._updates_attempts += 1
        return []

    async def get_file(self, file_id: str) -> File | None:
        _ = file_id
        return None

    async def download_file(self, file_path: str) -> bytes | None:
        _ = file_path
        return None

    async def close(self) -> None:
        return None

    async def get_me(self) -> User | None:
        return User(id=1)

    async def answer_callback_query(
        self,
        callback_query_id: str,
        text: str | None = None,
        show_alert: bool | None = None,
    ) -> bool:
        self.calls.append("answer_callback_query")
        self.callback_calls.append((callback_query_id, text, show_alert))
        return True

    async def edit_forum_topic(
        self, chat_id: int, message_thread_id: int, name: str
    ) -> bool:
        self.calls.append("edit_forum_topic")
        self.topic_calls.append((chat_id, message_thread_id, name))
        return True

    async def get_chat(self, chat_id: int) -> Chat | None:
        self.calls.append("get_chat")
        self.chat_calls.append(chat_id)
        return Chat(id=chat_id, type="private")

    async def get_chat_member(self, chat_id: int, user_id: int) -> ChatMember | None:
        self.calls.append("get_chat_member")
        self.chat_member_calls.append((chat_id, user_id))
        return ChatMember(status="member")

    async def create_forum_topic(self, chat_id: int, name: str) -> ForumTopic | None:
        self.calls.append("create_forum_topic")
        self.create_topic_calls.append((chat_id, name))
        return ForumTopic(message_thread_id=11)


@pytest.mark.anyio
async def test_edit_forum_topic_uses_outbox() -> None:
    bot = FakeBot()
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    result = await client.edit_forum_topic(
        chat_id=7, message_thread_id=42, name="yee88 @main"
    )

    assert result is True
    assert bot.calls == ["edit_forum_topic"]
    assert bot.topic_calls == [(7, 42, "yee88 @main")]


@pytest.mark.anyio
async def test_send_document_uses_outbox() -> None:
    bot = FakeBot()
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    result = await client.send_document(
        chat_id=5,
        filename="note.txt",
        content=b"hello",
        caption="greetings",
    )

    assert result is not None
    assert bot.calls == ["send_document"]
    assert bot.document_calls == [
        (5, "note.txt", b"hello", None, None, False, "greetings")
    ]
    await client.close()


@pytest.mark.anyio
async def test_set_my_commands_uses_outbox() -> None:
    bot = FakeBot()
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    commands = [{"command": "ping", "description": "Ping the bot"}]
    result = await client.set_my_commands(
        commands,
        scope={"type": "default"},
        language_code="en",
    )

    assert result is True
    assert bot.calls == ["set_my_commands"]
    assert bot.command_calls == [(commands, {"type": "default"}, "en")]
    await client.close()


@pytest.mark.anyio
async def test_answer_callback_query_uses_outbox() -> None:
    bot = FakeBot()
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    result = await client.answer_callback_query(
        callback_query_id="cb-1",
        text="ok",
        show_alert=True,
    )

    assert result is True
    assert bot.calls == ["answer_callback_query"]
    assert bot.callback_calls == [("cb-1", "ok", True)]
    await client.close()


@pytest.mark.anyio
async def test_get_chat_and_member_uses_outbox() -> None:
    bot = FakeBot()
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    chat = await client.get_chat(9)
    member = await client.get_chat_member(9, 42)

    assert chat is not None
    assert chat.id == 9
    assert member is not None
    assert member.status == "member"
    assert bot.calls == ["get_chat", "get_chat_member"]
    assert bot.chat_calls == [9]
    assert bot.chat_member_calls == [(9, 42)]
    await client.close()


@pytest.mark.anyio
async def test_create_forum_topic_uses_outbox() -> None:
    bot = FakeBot()
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    topic = await client.create_forum_topic(3, "status updates")

    assert topic is not None
    assert topic.message_thread_id == 11
    assert bot.calls == ["create_forum_topic"]
    assert bot.create_topic_calls == [(3, "status updates")]
    await client.close()


@pytest.mark.anyio
async def test_edits_coalesce_latest() -> None:
    class _BlockingBot(FakeBot):
        def __init__(self) -> None:
            super().__init__()
            self.edit_started = anyio.Event()
            self.release = anyio.Event()
            self._block_first = True

        async def edit_message_text(
            self,
            chat_id: int,
            message_id: int,
            text: str,
            entities: list[dict] | None = None,
            parse_mode: str | None = None,
            reply_markup: dict | None = None,
            *,
            wait: bool = True,
        ) -> Message | None:
            if self._block_first:
                self._block_first = False
                self.edit_started.set()
                await self.release.wait()
            return await super().edit_message_text(
                chat_id=chat_id,
                message_id=message_id,
                text=text,
                entities=entities,
                parse_mode=parse_mode,
                reply_markup=reply_markup,
                wait=wait,
            )

    bot = _BlockingBot()
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    await client.edit_message_text(
        chat_id=1,
        message_id=1,
        text="first",
        wait=False,
    )

    with anyio.fail_after(1):
        await bot.edit_started.wait()

    await client.edit_message_text(
        chat_id=1,
        message_id=1,
        text="second",
        wait=False,
    )
    await client.edit_message_text(
        chat_id=1,
        message_id=1,
        text="third",
        wait=False,
    )

    bot.release.set()

    with anyio.fail_after(1):
        while len(bot.edit_calls) < 2:
            await anyio.sleep(0)

    assert bot.edit_calls == ["first", "third"]


@pytest.mark.anyio
async def test_send_preempts_pending_edit() -> None:
    bot = FakeBot()
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    await client.edit_message_text(
        chat_id=1,
        message_id=1,
        text="first",
    )

    await client.edit_message_text(
        chat_id=1,
        message_id=1,
        text="progress",
        wait=False,
    )

    with anyio.fail_after(1):
        await client.send_message(chat_id=1, text="final")

    with anyio.fail_after(1):
        while len(bot.calls) < 3:
            await anyio.sleep(0)
    assert bot.calls[0] == "edit_message_text"
    assert bot.calls[1] == "send_message"
    assert bot.calls[-1] == "edit_message_text"


@pytest.mark.anyio
async def test_delete_drops_pending_edits() -> None:
    bot = FakeBot()
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    await client.edit_message_text(
        chat_id=1,
        message_id=1,
        text="first",
    )

    await client.edit_message_text(
        chat_id=1,
        message_id=1,
        text="progress",
        wait=False,
    )

    with anyio.fail_after(1):
        await client.delete_message(
            chat_id=1,
            message_id=1,
        )

    with anyio.fail_after(1):
        while not bot.delete_calls:
            await anyio.sleep(0)
    assert bot.delete_calls == [(1, 1)]
    assert bot.edit_calls == ["first"]


@pytest.mark.anyio
async def test_retry_after_retries_once() -> None:
    bot = FakeBot()
    bot.retry_after = 0.0
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    result = await client.edit_message_text(
        chat_id=1,
        message_id=1,
        text="retry",
    )

    assert result is not None
    assert result.message_id == 1
    assert bot._edit_attempts == 2


@pytest.mark.anyio
async def test_get_updates_retries_on_retry_after() -> None:
    bot = FakeBot()
    bot.updates_retry_after = 0.0
    client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)

    with anyio.fail_after(1):
        updates = await client.get_updates(offset=None, timeout_s=0)

    assert updates == []
    assert bot._updates_attempts == 2
