"""Unit tests for core DexFatherBot API methods using MockBot.

Tests cover: send methods, edit/delete, reactions, reply_to_message_id
passthrough, handler decorators, dispatch routing, exception
propagation in _dispatch, bound-message convenience methods,
rich components, component events, upload_file, error handler,
middleware chain execution order, and answer_* methods.
"""

import io

import pytest

from dexfather.testing import MockBot, TestClient
from dexfather.types import (
    CallbackQuery,
    Chat,
    ComponentInteraction,
    InlineQuery,
    InlineQueryResultArticle,
    Message,
    MessageReactionUpdated,
    ReactionType,
    Update,
    User,
)


@pytest.fixture
def bot():
    """A fresh MockBot for each test."""
    return MockBot()


# ── Send method tests ─────────────────────────────────────────────


@pytest.mark.asyncio
async def test_send_message(bot):
    """send_message records the correct API call."""
    await bot.send_message(123, "Hello!")
    assert len(bot.sent_messages) == 1
    call = bot.sent_messages[0]
    assert call["method"] == "sendMessage"
    assert call["chat_id"] == 123
    assert call["text"] == "Hello!"


@pytest.mark.asyncio
async def test_send_photo(bot):
    """send_photo records chat_id, photo_url, and caption."""
    await bot.send_photo(42, "https://example.com/pic.jpg", caption="Nice!")
    assert len(bot.sent_messages) == 1
    call = bot.sent_messages[0]
    assert call["method"] == "sendPhoto"
    assert call["chat_id"] == 42
    assert call["photo_url"] == "https://example.com/pic.jpg"
    assert call["caption"] == "Nice!"


@pytest.mark.asyncio
async def test_send_document(bot):
    """send_document records chat_id and document_url."""
    await bot.send_document(10, "https://example.com/file.pdf", caption="Report")
    assert len(bot.sent_messages) == 1
    call = bot.sent_messages[0]
    assert call["method"] == "sendDocument"
    assert call["chat_id"] == 10
    assert call["document_url"] == "https://example.com/file.pdf"
    assert call["caption"] == "Report"


# ── Edit/Delete tests ────────────────────────────────────────────


@pytest.mark.asyncio
async def test_edit_message_text(bot):
    """edit_message_text records the correct parameters."""
    await bot.edit_message_text(1, 42, "Updated text")
    call = bot.sent_messages[0]
    assert call["method"] == "editMessageText"
    assert call["chat_id"] == 1
    assert call["message_id"] == 42
    assert call["text"] == "Updated text"


@pytest.mark.asyncio
async def test_delete_message(bot):
    """delete_message records the correct parameters."""
    await bot.delete_message(1, 99)
    call = bot.sent_messages[0]
    assert call["method"] == "deleteMessage"
    assert call["chat_id"] == 1
    assert call["message_id"] == 99


# ── Reaction tests ───────────────────────────────────────────────


@pytest.mark.asyncio
async def test_set_message_reaction(bot):
    """set_message_reaction records chat_id, message_id, and reaction."""
    await bot.set_message_reaction(1, 42, reaction="thumbsup")
    call = bot.sent_messages[0]
    assert call["method"] == "setMessageReaction"
    assert call["chat_id"] == 1
    assert call["message_id"] == 42
    assert call["reaction"] == "thumbsup"


# ── reply_to_message_id passthrough ──────────────────────────────


@pytest.mark.asyncio
async def test_reply_to_message_id_passthrough(bot):
    """reply_to_message_id is passed through on send_message."""
    await bot.send_message(123, "Reply!", reply_to_message_id=42)
    call = bot.sent_messages[0]
    assert call.get("reply_to_message_id") == 42


@pytest.mark.asyncio
async def test_reply_to_message_id_none_not_recorded(bot):
    """reply_to_message_id=None is not recorded in the call."""
    await bot.send_message(123, "No reply")
    call = bot.sent_messages[0]
    # None values are filtered out by MockBot._request
    assert "reply_to_message_id" not in call


# ── Handler decorator tests ─────────────────────────────────────


@pytest.mark.asyncio
async def test_on_message_decorator(bot):
    """@bot.on_message() registers a handler that fires on text messages."""
    handled = []

    @bot.on_message()
    async def on_text(update, b):
        handled.append(update.message.text)

    update = Update(
        update_id=1,
        message=Message(
            message_id=1,
            chat=Chat(id=1, type="private"),
            from_user=User(id=1),
            text="hello from test",
            date=0,
        ),
    )
    await bot._dispatch(update)
    assert handled == ["hello from test"]


@pytest.mark.asyncio
async def test_on_callback_query_decorator(bot):
    """@bot.on_callback_query() registers a handler that fires on callbacks."""
    handled = []

    @bot.on_callback_query()
    async def on_cb(update, b):
        handled.append(update.callback_query.data)

    from dexfather.types import CallbackQuery

    update = Update(
        update_id=1,
        callback_query=CallbackQuery(
            id="cb1",
            from_user=User(id=1),
            data="test_data",
        ),
    )
    await bot._dispatch(update)
    assert handled == ["test_data"]


@pytest.mark.asyncio
async def test_on_message_reaction_decorator(bot):
    """@bot.on_message_reaction() registers a handler for reaction updates."""
    handled = []

    @bot.on_message_reaction()
    async def on_reaction(update, b):
        handled.append(update.message_reaction.message_id)

    update = Update(
        update_id=1,
        message_reaction=MessageReactionUpdated(
            chat=Chat(id=1, type="private"),
            message_id=42,
            user=User(id=1),
            date=0,
            new_reaction=[ReactionType(emoji="thumbsup")],
        ),
    )
    await bot._dispatch(update)
    assert handled == [42]


# ── Dispatch routing tests ───────────────────────────────────────


@pytest.mark.asyncio
async def test_dispatch_calls_matching_handler(bot):
    """Dispatch routes update to the first matching handler."""
    start_calls = []
    help_calls = []

    @bot.command("start")
    async def on_start(update, b):
        start_calls.append(1)

    @bot.command("help")
    async def on_help(update, b):
        help_calls.append(1)

    update = Update(
        update_id=1,
        message=Message(
            message_id=1,
            chat=Chat(id=1, type="private"),
            text="/help",
            date=0,
        ),
    )
    await bot._dispatch(update)
    assert start_calls == []
    assert help_calls == [1]


@pytest.mark.asyncio
async def test_dispatch_propagates_exceptions(bot):
    """MockBot._dispatch re-raises handler exceptions (not swallowed)."""

    @bot.on_message()
    async def on_msg(update, b):
        raise ValueError("intentional test error")

    update = Update(
        update_id=1,
        message=Message(
            message_id=1,
            chat=Chat(id=1, type="private"),
            from_user=User(id=1),
            text="trigger error",
            date=0,
        ),
    )
    with pytest.raises(ValueError, match="intentional test error"):
        await bot._dispatch(update)


# ── MockBot reset/state tests ───────────────────────────────────


@pytest.mark.asyncio
async def test_multiple_calls_accumulate(bot):
    """Multiple API calls are all recorded in sent_messages."""
    await bot.send_message(1, "First")
    await bot.send_message(2, "Second")
    await bot.send_photo(3, "https://example.com/pic.jpg")
    assert len(bot.sent_messages) == 3
    assert bot.sent_messages[0]["text"] == "First"
    assert bot.sent_messages[1]["text"] == "Second"
    assert bot.sent_messages[2]["method"] == "sendPhoto"


@pytest.mark.asyncio
async def test_bot_reset_clears_calls(bot):
    """MockBot.reset() clears all recorded calls."""
    await bot.send_message(1, "Hello")
    assert len(bot.sent_messages) == 1
    bot.reset()
    assert len(bot.sent_messages) == 0


# ══════════════════════════════════════════════════════════════════
# NEW TESTS — Bound-message convenience methods
# ══════════════════════════════════════════════════════════════════


@pytest.mark.asyncio
async def test_send_message_returns_bound_message_with_reply(bot):
    """send_message returns ApiResponse; bound Message.reply() works via _dispatch."""
    replied = []

    @bot.on_message()
    async def on_msg(update, b):
        # The message should be bound after dispatch
        msg = update.message
        assert msg._bot is not None  # bound
        await msg.reply("reply text")
        replied.append(True)

    update = Update(
        update_id=1,
        message=Message(
            message_id=10,
            chat=Chat(id=5, type="private"),
            from_user=User(id=1),
            text="hello",
            date=0,
        ),
    )
    await bot._dispatch(update)
    assert replied == [True]
    # The reply call should include reply_to_message_id
    reply_call = bot.sent_messages[0]
    assert reply_call["method"] == "sendMessage"
    assert reply_call["chat_id"] == 5
    assert reply_call["text"] == "reply text"
    assert reply_call["reply_to_message_id"] == 10


@pytest.mark.asyncio
async def test_send_message_returns_bound_message_with_edit(bot):
    """Bound Message.edit() records editMessageText."""
    edited = []

    @bot.on_message()
    async def on_msg(update, b):
        msg = update.message
        await msg.edit("edited text")
        edited.append(True)

    update = Update(
        update_id=1,
        message=Message(
            message_id=7,
            chat=Chat(id=3, type="private"),
            from_user=User(id=1),
            text="original",
            date=0,
        ),
    )
    await bot._dispatch(update)
    assert edited == [True]
    call = bot.sent_messages[0]
    assert call["method"] == "editMessageText"
    assert call["chat_id"] == 3
    assert call["message_id"] == 7
    assert call["text"] == "edited text"


@pytest.mark.asyncio
async def test_send_message_returns_bound_message_with_delete(bot):
    """Bound Message.delete() records deleteMessage."""
    deleted = []

    @bot.on_message()
    async def on_msg(update, b):
        msg = update.message
        await msg.delete()
        deleted.append(True)

    update = Update(
        update_id=1,
        message=Message(
            message_id=20,
            chat=Chat(id=9, type="group"),
            from_user=User(id=1),
            text="bye",
            date=0,
        ),
    )
    await bot._dispatch(update)
    assert deleted == [True]
    call = bot.sent_messages[0]
    assert call["method"] == "deleteMessage"
    assert call["chat_id"] == 9
    assert call["message_id"] == 20


@pytest.mark.asyncio
async def test_unbound_message_reply_raises_runtime_error():
    """Message.reply() raises RuntimeError when not bound to a bot."""
    msg = Message(
        message_id=1,
        chat=Chat(id=1, type="private"),
        text="test",
        date=0,
    )
    with pytest.raises(RuntimeError, match="not bound to a bot"):
        await msg.reply("hi")


# ── send_photo / send_document / send_video return bound Messages ──


@pytest.mark.asyncio
async def test_send_photo_returns_bound_message(bot):
    """send_photo records sendPhoto; response can be used as bound msg via dispatch."""
    replied_photo = []

    @bot.on_message()
    async def on_msg(update, b):
        await update.message.reply_photo("https://example.com/pic.jpg", caption="Photo!")
        replied_photo.append(True)

    update = Update(
        update_id=1,
        message=Message(
            message_id=5,
            chat=Chat(id=2, type="private"),
            from_user=User(id=1),
            text="send me a photo",
            date=0,
        ),
    )
    await bot._dispatch(update)
    assert replied_photo == [True]
    call = bot.sent_messages[0]
    assert call["method"] == "sendPhoto"
    assert call["photo_url"] == "https://example.com/pic.jpg"
    assert call["caption"] == "Photo!"
    assert call["reply_to_message_id"] == 5


@pytest.mark.asyncio
async def test_send_document_records_correctly(bot):
    """send_document records sendDocument with all params."""
    await bot.send_document(7, "https://example.com/doc.pdf", caption="Doc")
    call = bot.sent_messages[0]
    assert call["method"] == "sendDocument"
    assert call["chat_id"] == 7
    assert call["document_url"] == "https://example.com/doc.pdf"
    assert call["caption"] == "Doc"


# ── edit_message_text returns correct params ─────────────────────


@pytest.mark.asyncio
async def test_edit_message_text_returns_api_response(bot):
    """edit_message_text returns an ApiResponse with ok=True."""
    result = await bot.edit_message_text(1, 42, "Updated")
    assert result.ok is True
    call = bot.sent_messages[0]
    assert call["method"] == "editMessageText"
    assert call["text"] == "Updated"


# ── delete_message returns bool ──────────────────────────────────


@pytest.mark.asyncio
async def test_delete_message_returns_api_response(bot):
    """delete_message returns an ApiResponse with ok=True."""
    result = await bot.delete_message(1, 99)
    assert result.ok is True


# ── get_me returns correct mock ──────────────────────────────────


@pytest.mark.asyncio
async def test_get_me_returns_bot_info(bot):
    """MockBot._request('getMe') returns the mock _me dict."""
    result = await bot._request("getMe")
    assert result.ok is True
    assert result.result["id"] == 999
    assert result.result["username"] == "test_bot"
    assert result.result["name"] == "Test Bot"


# ── send_rich_component sends correct API method ─────────────────


@pytest.mark.asyncio
async def test_send_rich_component(bot):
    """send_rich_component records sendRichComponent with correct params."""
    component = {"type": "stats", "items": [{"label": "Price", "value": "$100"}]}
    await bot._request(
        "sendRichComponent",
        chat_id=1,
        component=component,
        text="Stats",
        height="compact",
    )
    call = bot.sent_messages[0]
    assert call["method"] == "sendRichComponent"
    assert call["chat_id"] == 1
    assert call["component"]["type"] == "stats"
    assert call["text"] == "Stats"
    assert call["height"] == "compact"


# ── update_component sends correct API method ────────────────────


@pytest.mark.asyncio
async def test_update_component(bot):
    """update_component records updateComponent with correct params."""
    await bot._request(
        "updateComponent",
        chat_id=1,
        message_id=10,
        component_id="comp_1",
        props={"value": 75},
    )
    call = bot.sent_messages[0]
    assert call["method"] == "updateComponent"
    assert call["chat_id"] == 1
    assert call["message_id"] == 10
    assert call["component_id"] == "comp_1"
    assert call["props"] == {"value": 75}


# ── on_component_event decorator registers TypeHandler ───────────


@pytest.mark.asyncio
async def test_on_component_event(bot):
    """on_component_event registers a TypeHandler for component_interaction."""
    handled = []

    # MockBot mirrors DexFatherBot — check for on_message_reaction pattern
    # Component events use TypeHandler("component_interaction", ...)
    from dexfather.handlers import TypeHandler

    async def on_ci(update, b):
        handled.append(update.component_interaction)

    bot.add_handler(TypeHandler("component_interaction", on_ci))

    update = Update(
        update_id=1,
        component_interaction=ComponentInteraction(
            component_id="btn_1",
            interaction_type="button_click",
        ),
    )
    await bot._dispatch(update)
    assert len(handled) == 1
    assert handled[0].component_id == "btn_1"
    assert handled[0].interaction_type == "button_click"


# ── upload_file with bytes input ─────────────────────────────────


@pytest.mark.asyncio
async def test_upload_file_with_bytes(bot):
    """Upload file with raw bytes records uploadFile."""
    await bot._request("uploadFile", file_data=b"raw bytes content", filename="test.bin")
    call = bot.sent_messages[0]
    assert call["method"] == "uploadFile"
    assert call["file_data"] == b"raw bytes content"
    assert call["filename"] == "test.bin"


# ── upload_file with BytesIO input ───────────────────────────────


@pytest.mark.asyncio
async def test_upload_file_with_bytesio(bot):
    """Upload file with BytesIO records uploadFile with the read data."""
    bio = io.BytesIO(b"bytesio content")
    data = bio.read()
    await bot._request("uploadFile", file_data=data, filename="buffer.txt")
    call = bot.sent_messages[0]
    assert call["method"] == "uploadFile"
    assert call["file_data"] == b"bytesio content"


# ── set_message_reaction sends correct params ────────────────────


@pytest.mark.asyncio
async def test_set_message_reaction_correct_params(bot):
    """set_message_reaction sends chat_id, message_id, and reaction."""
    await bot.set_message_reaction(10, 50, reaction="heart")
    call = bot.sent_messages[0]
    assert call["method"] == "setMessageReaction"
    assert call["chat_id"] == 10
    assert call["message_id"] == 50
    assert call["reaction"] == "heart"


@pytest.mark.asyncio
async def test_set_message_reaction_none_reaction(bot):
    """set_message_reaction with no reaction omits it from the call."""
    await bot.set_message_reaction(10, 50)
    call = bot.sent_messages[0]
    assert call["method"] == "setMessageReaction"
    assert "reaction" not in call  # None is filtered


# ── answer_callback_query sends correct params ───────────────────


@pytest.mark.asyncio
async def test_answer_callback_query(bot):
    """answer_callback_query records callback_query_id and optional text."""
    await bot.answer_callback_query("cb123", text="Done!", show_alert=True)
    call = bot.sent_messages[0]
    assert call["method"] == "answerCallbackQuery"
    assert call["callback_query_id"] == "cb123"
    assert call["text"] == "Done!"
    assert call["show_alert"] is True


# ── answer_inline_query serializes Pydantic models ───────────────


@pytest.mark.asyncio
async def test_answer_inline_query_serializes_pydantic(bot):
    """answer_inline_query accepts and records Pydantic model results."""
    results = [
        InlineQueryResultArticle(
            id="1",
            title="Test Result",
            input_message_content={"message_text": "Hello!"},
            description="A test article",
        ),
    ]
    # Convert to dicts as the real bot would
    serialized = [r.model_dump() for r in results]
    await bot.answer_inline_query("iq_42", serialized, cache_time=60)
    call = bot.sent_messages[0]
    assert call["method"] == "answerInlineQuery"
    assert call["inline_query_id"] == "iq_42"
    assert len(call["results"]) == 1
    assert call["results"][0]["title"] == "Test Result"
    assert call["results"][0]["type"] == "article"
    assert call["cache_time"] == 60


# ── Error handler (bot.catch) gets called on exception ───────────


@pytest.mark.asyncio
async def test_error_handler_via_dispatch():
    """DexFatherBot._dispatch uses _error_handler on exceptions.

    Since MockBot doesn't have the _error_handler path, we test
    the pattern by directly simulating what DexFatherBot._dispatch does.
    """
    from dexfather.testing import MockBot

    bot = MockBot()
    errors_caught = []

    # Monkey-patch _dispatch to simulate error handler behavior
    original_dispatch = bot._dispatch

    async def dispatch_with_error_handler(update):
        try:
            await original_dispatch(update)
        except Exception as e:
            errors_caught.append((type(e).__name__, str(e)))

    @bot.on_message()
    async def on_msg(update, b):
        raise RuntimeError("handler broke")

    update = Update(
        update_id=1,
        message=Message(
            message_id=1,
            chat=Chat(id=1, type="private"),
            from_user=User(id=1),
            text="crash me",
            date=0,
        ),
    )
    await dispatch_with_error_handler(update)
    assert len(errors_caught) == 1
    assert errors_caught[0][0] == "RuntimeError"
    assert "handler broke" in errors_caught[0][1]


# ── Middleware chain execution order ─────────────────────────────


@pytest.mark.asyncio
async def test_middleware_chain_execution_order():
    """Middlewares execute in registration order (first = outermost).

    Since MockBot doesn't process middlewares in _dispatch, we test
    middleware chain ordering via the DexFatherBot's middleware pattern
    using a simplified chain builder.
    """
    from dexfather.middleware import BaseMiddleware

    order = []

    class FirstMiddleware(BaseMiddleware):
        async def __call__(self, handler, update, bot):
            order.append("first_pre")
            result = await handler(update, bot)
            order.append("first_post")
            return result

    class SecondMiddleware(BaseMiddleware):
        async def __call__(self, handler, update, bot):
            order.append("second_pre")
            result = await handler(update, bot)
            order.append("second_post")
            return result

    # Build chain exactly as DexFatherBot._dispatch does
    async def inner_dispatch(update, bot):
        order.append("handler")

    chain = inner_dispatch
    middlewares = [FirstMiddleware(), SecondMiddleware()]
    for mw in reversed(middlewares):
        prev = chain
        chain = lambda u, b, _mw=mw, _prev=prev: _mw(_prev, u, b)

    # Create a minimal update
    update = Update(
        update_id=1,
        message=Message(
            message_id=1,
            chat=Chat(id=1, type="private"),
            text="test",
            date=0,
        ),
    )
    await chain(update, None)

    # First middleware wraps second, which wraps handler
    assert order == ["first_pre", "second_pre", "handler", "second_post", "first_post"]


# ── TestClient integration tests ─────────────────────────────────


@pytest.mark.asyncio
async def test_test_client_send_command(bot):
    """TestClient.send_command dispatches to command handlers."""
    received = []

    @bot.command("ping")
    async def on_ping(update, b):
        received.append("pong")

    client = TestClient(bot)
    await client.send_command("/ping")
    assert received == ["pong"]


@pytest.mark.asyncio
async def test_test_client_send_callback_query(bot):
    """TestClient.send_callback_query dispatches to callback handlers."""
    received = []

    @bot.on_callback_query()
    async def on_cb(update, b):
        received.append(update.callback_query.data)

    client = TestClient(bot)
    await client.send_callback_query("action:confirm")
    assert received == ["action:confirm"]


@pytest.mark.asyncio
async def test_test_client_send_inline_query(bot):
    """TestClient.send_inline_query dispatches to inline handlers."""
    received = []

    @bot.on_inline_query()
    async def on_iq(update, b):
        received.append(update.inline_query.query)

    client = TestClient(bot)
    await client.send_inline_query("search term")
    assert received == ["search term"]


@pytest.mark.asyncio
async def test_test_client_send_poll_answer(bot):
    """TestClient.send_poll_answer dispatches to poll answer handlers."""
    received = []

    @bot.on_poll_answer()
    async def on_pa(update, b):
        received.append(update.poll_answer.option_ids)

    client = TestClient(bot)
    await client.send_poll_answer("poll1", [0, 2])
    assert received == [[0, 2]]
