"""Tests for keyboard markup builders.

Covers InlineKeyboardMarkup, InlineKeyboardBuilder, ReplyKeyboardMarkup,
ReplyKeyboardRemove, ForceReply, serialization, and chaining.
"""

import pytest

from dexfather.keyboards import (
    ForceReply,
    InlineKeyboardBuilder,
    InlineKeyboardButton,
    InlineKeyboardMarkup,
    ReplyKeyboardMarkup,
    ReplyKeyboardRemove,
)


# ── InlineKeyboardButton ────────────────────────────────────────


class TestInlineKeyboardButton:
    def test_basic_button(self):
        btn = InlineKeyboardButton(text="Click", callback_data="click_val")
        assert btn.text == "Click"
        assert btn.callback_data == "click_val"

    def test_button_with_url(self):
        btn = InlineKeyboardButton(text="Visit", url="https://example.com")
        assert btn.url == "https://example.com"
        assert btn.callback_data is None

    def test_button_with_style_and_shape(self):
        btn = InlineKeyboardButton(
            text="OK",
            callback_data="ok",
            style="primary",
            shape="pill",
            size="lg",
        )
        assert btn.style == "primary"
        assert btn.shape == "pill"
        assert btn.size == "lg"

    def test_button_with_copy_text(self):
        btn = InlineKeyboardButton(
            text="Copy",
            callback_data="copy",
            copy_text={"text": "abc123"},
        )
        assert btn.copy_text == {"text": "abc123"}


# ── InlineKeyboardMarkup ────────────────────────────────────────


class TestInlineKeyboardMarkup:
    def test_from_column(self):
        """from_column places each button on its own row."""
        btns = [
            InlineKeyboardButton(text="A", callback_data="a"),
            InlineKeyboardButton(text="B", callback_data="b"),
            InlineKeyboardButton(text="C", callback_data="c"),
        ]
        markup = InlineKeyboardMarkup.from_column(btns)
        assert len(markup.inline_keyboard) == 3
        assert len(markup.inline_keyboard[0]) == 1
        assert markup.inline_keyboard[0][0].text == "A"

    def test_from_row(self):
        """from_row places all buttons in a single row."""
        btns = [
            InlineKeyboardButton(text="A", callback_data="a"),
            InlineKeyboardButton(text="B", callback_data="b"),
        ]
        markup = InlineKeyboardMarkup.from_row(btns)
        assert len(markup.inline_keyboard) == 1
        assert len(markup.inline_keyboard[0]) == 2

    def test_add_row(self):
        """add_row appends a new row and returns self for chaining."""
        btn_a = InlineKeyboardButton(text="A", callback_data="a")
        btn_b = InlineKeyboardButton(text="B", callback_data="b")
        markup = InlineKeyboardMarkup(inline_keyboard=[[btn_a]])
        result = markup.add_row(btn_b)
        assert result is markup  # chaining
        assert len(markup.inline_keyboard) == 2
        assert markup.inline_keyboard[1][0].text == "B"

    def test_to_dict_serialization(self):
        """to_dict serializes callback_data as callbackData (alias)."""
        btn = InlineKeyboardButton(text="OK", callback_data="ok_val")
        markup = InlineKeyboardMarkup.from_row([btn])
        d = markup.to_dict()
        assert "inline_keyboard" in d
        row = d["inline_keyboard"][0]
        btn_dict = row[0]
        assert btn_dict["text"] == "OK"
        # serialization_alias: callback_data -> callbackData
        assert btn_dict["callbackData"] == "ok_val"
        # callback_data should NOT appear (alias replaces it)
        assert "callback_data" not in btn_dict

    def test_to_dict_excludes_none(self):
        """to_dict omits None fields."""
        btn = InlineKeyboardButton(text="Simple", callback_data="s")
        markup = InlineKeyboardMarkup.from_row([btn])
        d = markup.to_dict()
        btn_dict = d["inline_keyboard"][0][0]
        assert "url" not in btn_dict
        assert "style" not in btn_dict
        assert "shape" not in btn_dict
        assert "size" not in btn_dict
        assert "copy_text" not in btn_dict


# ── InlineKeyboardBuilder ──────────────────────────────────────


class TestInlineKeyboardBuilder:
    def test_button_and_row(self):
        """button + row creates proper structure."""
        markup = (
            InlineKeyboardBuilder()
            .button(text="Yes", callback_data="yes")
            .button(text="No", callback_data="no")
            .row()
            .button(text="Cancel", callback_data="cancel")
            .as_markup()
        )
        assert len(markup.inline_keyboard) == 2
        assert len(markup.inline_keyboard[0]) == 2
        assert len(markup.inline_keyboard[1]) == 1
        assert markup.inline_keyboard[0][0].text == "Yes"
        assert markup.inline_keyboard[0][1].text == "No"
        assert markup.inline_keyboard[1][0].text == "Cancel"

    def test_row_noop_on_empty(self):
        """row() with no pending buttons is a no-op."""
        builder = InlineKeyboardBuilder()
        builder.row()  # should not crash or add empty row
        markup = builder.as_markup()
        assert len(markup.inline_keyboard) == 0

    def test_as_markup_flushes_pending(self):
        """as_markup flushes pending buttons into a final row."""
        markup = (
            InlineKeyboardBuilder()
            .button(text="A", callback_data="a")
            .button(text="B", callback_data="b")
            .as_markup()
        )
        assert len(markup.inline_keyboard) == 1
        assert len(markup.inline_keyboard[0]) == 2

    def test_adjust_repeat(self):
        """adjust with repeat=True cycles the sizes."""
        builder = InlineKeyboardBuilder()
        for i in range(6):
            builder.button(text=str(i), callback_data=str(i))
        builder.adjust(2, repeat=True)
        markup = builder.as_markup()
        assert len(markup.inline_keyboard) == 3
        for row in markup.inline_keyboard:
            assert len(row) == 2

    def test_adjust_last_size_reused(self):
        """adjust without repeat reuses the last size for remaining buttons."""
        builder = InlineKeyboardBuilder()
        for i in range(5):
            builder.button(text=str(i), callback_data=str(i))
        builder.adjust(3)  # first row = 3, remaining rows = 3 each
        markup = builder.as_markup()
        assert len(markup.inline_keyboard) == 2
        assert len(markup.inline_keyboard[0]) == 3
        assert len(markup.inline_keyboard[1]) == 2

    def test_adjust_multiple_sizes(self):
        """adjust with multiple sizes uses each in sequence, last reused."""
        builder = InlineKeyboardBuilder()
        for i in range(7):
            builder.button(text=str(i), callback_data=str(i))
        builder.adjust(1, 2, 3)  # row1=1, row2=2, then last size=3 repeats
        markup = builder.as_markup()
        # 7 buttons: [1] [2] [3] [1] = 4 rows
        assert len(markup.inline_keyboard) == 4
        assert len(markup.inline_keyboard[0]) == 1
        assert len(markup.inline_keyboard[1]) == 2
        assert len(markup.inline_keyboard[2]) == 3
        assert len(markup.inline_keyboard[3]) == 1  # remaining 1 button

    def test_adjust_empty(self):
        """adjust with no buttons is a no-op."""
        builder = InlineKeyboardBuilder()
        builder.adjust(2)
        markup = builder.as_markup()
        assert len(markup.inline_keyboard) == 0

    def test_adjust_no_sizes(self):
        """adjust with no sizes leaves buttons in pending buffer."""
        builder = InlineKeyboardBuilder()
        builder.button(text="A", callback_data="a")
        builder.adjust()  # no sizes
        markup = builder.as_markup()
        # buttons should be flushed by as_markup
        assert len(markup.inline_keyboard) == 1

    def test_button_with_style(self):
        """Buttons preserve style attribute."""
        markup = (
            InlineKeyboardBuilder()
            .button(text="Buy", callback_data="buy", style="success")
            .as_markup()
        )
        assert markup.inline_keyboard[0][0].style == "success"

    def test_copy(self):
        """copy returns an independent deep copy."""
        original = InlineKeyboardBuilder()
        original.button(text="A", callback_data="a")
        clone = original.copy()

        # Modify clone
        clone.button(text="B", callback_data="b")

        original_markup = original.as_markup()
        clone_markup = clone.as_markup()

        assert len(original_markup.inline_keyboard[0]) == 1
        assert len(clone_markup.inline_keyboard[0]) == 2

    def test_chaining_returns_self(self):
        """All builder methods return self for chaining."""
        builder = InlineKeyboardBuilder()
        assert builder.button(text="X", callback_data="x") is builder
        assert builder.row() is builder


# ── ReplyKeyboardMarkup ─────────────────────────────────────────


class TestReplyKeyboardMarkup:
    def test_from_column(self):
        markup = ReplyKeyboardMarkup.from_column(["A", "B", "C"])
        assert len(markup.keyboard) == 3
        assert markup.keyboard[0][0].text == "A"
        assert markup.keyboard[2][0].text == "C"

    def test_from_row(self):
        markup = ReplyKeyboardMarkup.from_row(["A", "B"])
        assert len(markup.keyboard) == 1
        assert len(markup.keyboard[0]) == 2

    def test_add_row(self):
        markup = ReplyKeyboardMarkup.from_row(["A"])
        result = markup.add_row("B", "C")
        assert result is markup
        assert len(markup.keyboard) == 2
        assert len(markup.keyboard[1]) == 2

    def test_defaults(self):
        markup = ReplyKeyboardMarkup.from_row(["A"])
        assert markup.resize_keyboard is True
        assert markup.one_time_keyboard is False
        assert markup.input_field_placeholder is None

    def test_custom_options(self):
        markup = ReplyKeyboardMarkup.from_row(
            ["A"],
            resize_keyboard=False,
            one_time_keyboard=True,
            input_field_placeholder="Type here...",
        )
        assert markup.resize_keyboard is False
        assert markup.one_time_keyboard is True
        assert markup.input_field_placeholder == "Type here..."

    def test_to_dict(self):
        markup = ReplyKeyboardMarkup.from_row(["A"], input_field_placeholder="Type...")
        d = markup.to_dict()
        assert "keyboard" in d
        assert d["resize_keyboard"] is True
        assert d["one_time_keyboard"] is False
        assert d["input_field_placeholder"] == "Type..."


# ── ReplyKeyboardRemove ─────────────────────────────────────────


class TestReplyKeyboardRemove:
    def test_basic(self):
        remove = ReplyKeyboardRemove()
        assert remove.remove_keyboard is True

    def test_to_dict(self):
        d = ReplyKeyboardRemove().to_dict()
        assert d == {"remove_keyboard": True}


# ── ForceReply ──────────────────────────────────────────────────


class TestForceReply:
    def test_basic(self):
        fr = ForceReply()
        assert fr.force_reply is True
        assert fr.input_field_placeholder is None
        assert fr.selective is None

    def test_with_options(self):
        fr = ForceReply(
            input_field_placeholder="Enter name...",
            selective=True,
        )
        assert fr.input_field_placeholder == "Enter name..."
        assert fr.selective is True

    def test_to_dict_basic(self):
        d = ForceReply().to_dict()
        assert d == {"force_reply": True}
        assert "input_field_placeholder" not in d
        assert "selective" not in d

    def test_to_dict_with_options(self):
        d = ForceReply(input_field_placeholder="Reply...", selective=True).to_dict()
        assert d["force_reply"] is True
        assert d["input_field_placeholder"] == "Reply..."
        assert d["selective"] is True
