from flask import Flask


def _build_request_app():
    return Flask(__name__)


def test_webhook_menu_command_handles_bot_suffix(monkeypatch):
    from routes import openclaw_bot_routes as bot

    calls = []

    def fake_handle_cmd(cmd, arg, chat_id, reply_to):
        calls.append((cmd, arg, chat_id, reply_to))

    monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "BOT_USERNAME", "@KnownBot")

    app = _build_request_app()
    payload = {
        "update_id": 10001,
        "message": {
            "message_id": 55,
            "chat": {"id": -200, "type": "supergroup"},
            "from": {"id": 777},
            "text": "/menu@OtherBot",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert calls == [("menu", "", -200, 55)]


def test_openclaw_answer_wakeup_query_returns_menu():
    from routes import openclaw_bot_routes as bot

    text, kb = bot.openclaw_answer("小龍蝦")

    assert "OpenClaw（小O）" in text
    assert kb == bot.main_menu_keyboard()


def test_openclaw_answer_variants_are_menu_wakeup():
    from routes import openclaw_bot_routes as bot

    assert bot._looks_like_wakeup_prompt("小O_小龍蝦") is True
    assert bot._looks_like_wakeup_prompt("Hello, 小O") is True
    assert bot._looks_like_wakeup_prompt("今天業績") is False
    assert bot._looks_like_wakeup_prompt("你好") is True


def test_webhook_clears_stale_user_context_at_request_start():
    from routes import openclaw_bot_routes as bot

    bot._CURRENT_USER_ID_CTX.set(999)

    app = _build_request_app()
    with app.test_request_context("/bot/telegram/webhook", method="POST", json=None):
        bot.telegram_webhook()

    assert bot._CURRENT_USER_ID_CTX.get() is None


def test_menu_command_returns_full_main_menu_keyboard(monkeypatch):
    from routes import openclaw_bot_routes as bot
    from services.openclaw_bot import menu_keyboards

    captured = {}

    def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown", **_kwargs):
        captured["chat_id"] = chat_id
        captured["text"] = text
        captured["keyboard"] = keyboard
        captured["parse_mode"] = parse_mode

    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
    monkeypatch.setattr(bot, "send_message", fake_send_message)
    monkeypatch.setattr(bot, "BOT_USERNAME", "@KnownBot")
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)

    app = _build_request_app()
    payload = {
        "update_id": 10002,
        "message": {
            "message_id": 56,
            "chat": {"id": -200, "type": "supergroup"},
            "from": {"id": 777},
            "text": "/menu",
        },
    }

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
        bot.telegram_webhook()

    assert captured["chat_id"] == -200
    assert captured["keyboard"] == menu_keyboards.main_menu_keyboard()
    assert captured["parse_mode"] == "Markdown"
    assert "OpenClaw（小O）" in captured["text"]


def test_private_menu_command_is_allowed_when_no_whitelist_and_fallback_enabled(monkeypatch):
    from routes import openclaw_bot_routes as bot

    calls = []

    def fake_handle_cmd(cmd, arg, chat_id, reply_to):
        calls.append((cmd, arg, chat_id, reply_to))

    monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
    monkeypatch.setattr(bot, "BOT_USERNAME", "@KnownBot")
    monkeypatch.setattr(bot, "_ALLOW_PRIVATE_WITHOUT_WHITELIST", True)
    monkeypatch.setattr(bot, "ALLOWED_USERS", set())

    app = _build_request_app()
    payload = {
        "update_id": 10020,
        "message": {
            "message_id": 55,
            "chat": {"id": 777, "type": "private"},
            "from": {"id": 777777},
            "text": "/menu",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert calls == [("menu", "", 777, 55)]


def test_is_authorized_private_mode_switch(monkeypatch):
    from routes import openclaw_bot_routes as bot

    monkeypatch.setattr(bot, "ALLOWED_USERS", set())
    monkeypatch.setattr(bot, "_ALLOW_PRIVATE_WITHOUT_WHITELIST", True)
    assert bot._is_authorized("private", 777, 42) is True

    monkeypatch.setattr(bot, "_ALLOW_PRIVATE_WITHOUT_WHITELIST", False)
    assert bot._is_authorized("private", 777, 42) is False


def test_photo_message_uses_ollama_vision_before_gemini(monkeypatch):
    from routes import openclaw_bot_routes as bot

    sent = []
    handled = []

    class FakeResponse:
        def __init__(self, json_data=None, content=b"fake-image"):
            self._json_data = json_data or {}
            self.content = content

        def json(self):
            return self._json_data

    def fake_get(url, **_kwargs):
        if "getFile" in url:
            return FakeResponse({"result": {"file_path": "photos/product.jpg"}})
        return FakeResponse(content=b"fake-image")

    monkeypatch.setattr(bot.requests, "get", fake_get)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
    monkeypatch.setattr(
        bot,
        "send_message",
        lambda *args, **kwargs: sent.append((args, kwargs)),
    )
    monkeypatch.setattr(
        bot,
        "handle_cmd",
        lambda cmd, arg, chat_id, reply_to: handled.append((cmd, arg, chat_id, reply_to)),
    )
    monkeypatch.setattr(
        bot,
        "_identify_product_name_with_ollama_vision",
        lambda img_b64, request_id: "理膚寶水 B5 修復霜",
    )
    monkeypatch.setattr(
        bot,
        "_identify_product_name_with_gemini_vision",
        lambda img_b64, request_id: (_ for _ in ()).throw(AssertionError("Gemini should not run first")),
    )

    app = _build_request_app()
    payload = {
        "update_id": 10030,
        "message": {
            "message_id": 80,
            "chat": {"id": 777, "type": "private"},
            "from": {"id": 777777},
            "photo": [{"file_id": "small"}, {"file_id": "large"}],
        },
    }

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
        bot.telegram_webhook()

    assert handled == [("competitor", "理膚寶水 B5 修復霜", 777, 80)]
    assert "理膚寶水 B5 修復霜" in sent[0][0][1]


def test_photo_message_falls_back_to_gemini_when_ollama_empty(monkeypatch):
    from routes import openclaw_bot_routes as bot

    handled = []
    calls = []

    class FakeResponse:
        def __init__(self, json_data=None, content=b"fake-image"):
            self._json_data = json_data or {}
            self.content = content

        def json(self):
            return self._json_data

    def fake_get(url, **_kwargs):
        if "getFile" in url:
            return FakeResponse({"result": {"file_path": "photos/product.jpg"}})
        return FakeResponse(content=b"fake-image")

    def fake_ollama(_img_b64, _request_id):
        calls.append("ollama")
        return ""

    def fake_gemini(_img_b64, _request_id):
        calls.append("gemini")
        return "飛利浦 Sonicare"

    monkeypatch.setattr(bot.requests, "get", fake_get)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
    monkeypatch.setattr(bot, "send_message", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        bot,
        "handle_cmd",
        lambda cmd, arg, chat_id, reply_to: handled.append((cmd, arg, chat_id, reply_to)),
    )
    monkeypatch.setattr(bot, "_identify_product_name_with_ollama_vision", fake_ollama)
    monkeypatch.setattr(bot, "_identify_product_name_with_gemini_vision", fake_gemini)

    app = _build_request_app()
    payload = {
        "update_id": 10031,
        "message": {
            "message_id": 81,
            "chat": {"id": 777, "type": "private"},
            "from": {"id": 777777},
            "photo": [{"file_id": "small"}, {"file_id": "large"}],
        },
    }

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
        bot.telegram_webhook()

    assert calls == ["ollama", "gemini"]
    assert handled == [("competitor", "飛利浦 Sonicare", 777, 81)]


def test_obs_heal_audit_uses_current_callback_user(monkeypatch):
    from types import SimpleNamespace
    from routes import openclaw_bot_routes as bot
    import services.ollama_service as ollama_service
    import services.auto_heal_service as auto_heal_module

    captured = {}

    class FakeAutoHeal:
        def handle_exception(self, error_type, context):
            captured["error_type"] = error_type
            captured["context"] = context
            return SimpleNamespace(success=True, action="ALERT_ONLY", message="ok")

    sent = []
    monkeypatch.setattr(bot, "send_message", lambda *args, **kwargs: sent.append((args, kwargs)))
    monkeypatch.setattr(ollama_service, "_is_unhealthy", lambda _host: True)
    monkeypatch.setattr(auto_heal_module, "auto_heal_service", FakeAutoHeal())

    token = bot._CURRENT_USER_ID_CTX.set(777001)
    try:
        bot.handle_cmd("obs_heal", "GCP-A", -200, 55)
    finally:
        bot._CURRENT_USER_ID_CTX.reset(token)

    assert captured["error_type"] == "ollama_unhealthy"
    assert captured["context"]["triggered_by"] == "telegram_user_777001"
    assert sent


def test_webhook_menu_callback_edits_existing_message(monkeypatch):
    from routes import openclaw_bot_routes as bot

    edited = []

    def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
        edited.append((chat_id, message_id, text, keyboard, parse_mode))
        return {"ok": True}

    monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload = {
        "update_id": 10002,
        "callback_query": {
            "id": "cb1",
            "from": {"id": 777},
            "message": {
                "message_id": 66,
                "chat": {"id": -200, "type": "supergroup"},
            },
            "data": "menu:main",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert len(edited) == 1
    chat_id, message_id, text, keyboard, parse_mode = edited[0]
    assert chat_id == -200
    assert message_id == 66
    assert text == "👋 *OpenClaw* — 請選擇功能類別"
    assert isinstance(keyboard, list)
    assert parse_mode == "Markdown"


def test_webhook_event_ignore_callback_edits_and_audits(monkeypatch):
    from routes import openclaw_bot_routes as bot

    edited = []
    sent = []
    audits = []

    def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
        edited.append((chat_id, message_id, text, keyboard, parse_mode))
        return {"ok": True}

    def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown"):
        sent.append((chat_id, text, reply_to, keyboard, parse_mode))
        return {"ok": True}

    def fake_audit(event_id, user_label, ts_label):
        audits.append((event_id, user_label, ts_label))

    monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
    monkeypatch.setattr(bot, "send_message", fake_send_message)
    monkeypatch.setattr(bot, "_write_event_ignore_audit", fake_audit)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload = {
        "update_id": 10103,
        "callback_query": {
            "id": "cb-eig-1",
            "from": {"id": 777, "username": "alice"},
            "message": {
                "message_id": 79,
                "chat": {"id": -200, "type": "supergroup"},
                "text": "<b>待處理事件</b>",
            },
            "data": "momo:eig:event-123",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert audits and audits[0][0:2] == ("event-123", "alice")
    assert len(edited) == 1
    _, _, text, keyboard, parse_mode = edited[0]
    assert "&lt;b&gt;待處理事件&lt;/b&gt;" in text
    assert "已忽略" in text
    assert "alice" in text
    assert keyboard is None
    assert parse_mode == "HTML"
    assert sent == []


def test_webhook_legacy_menu_callback_normalizes_prefix(monkeypatch):
    from routes import openclaw_bot_routes as bot

    edited = []

    def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
        edited.append((chat_id, message_id, text, keyboard, parse_mode))
        return {"ok": True}

    monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload = {
        "update_id": 10005,
        "callback_query": {
            "id": "cb-legacy",
            "from": {"id": 777},
            "message": {"message_id": 123, "chat": {"id": -200, "type": "supergroup"}},
            "data": "menu_main",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert len(edited) == 1
    assert edited[0][0] == -200
    assert edited[0][1] == 123
    assert edited[0][2] == "👋 *OpenClaw* — 請選擇功能類別"


def test_webhook_await_callback_edits_existing_message(monkeypatch):
    from routes import openclaw_bot_routes as bot

    edited = []

    def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
        edited.append((chat_id, message_id, text, keyboard, parse_mode))
        return {"ok": True}

    monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload = {
        "update_id": 10003,
        "callback_query": {
            "id": "cb2",
            "from": {"id": 777},
            "message": {
                "message_id": 77,
                "chat": {"id": -200, "type": "supergroup"},
            },
            "data": "await:date_range_sales",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert len(edited) == 1
    _, _, text, keyboard, _ = edited[0]
    assert "輸入 `/取消` 可退出_" in text
    assert keyboard == [[{"text": "✖ 取消", "callback_data": "menu:main"}]]


def test_webhook_cmd_callback_updates_with_message_edit(monkeypatch):
    from routes import openclaw_bot_routes as bot

    edited = []
    sent = []

    def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
        edited.append((chat_id, message_id, text, keyboard, parse_mode))
        return {"ok": True}

    def fake_handle_cmd(cmd, arg, chat_id, reply_to):
        bot.send_message(chat_id, f"{cmd}:{arg}", reply_to=reply_to)

    def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown"):
        sent.append((chat_id, text, reply_to, keyboard, parse_mode))
        return {"ok": True}

    monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
    monkeypatch.setattr(bot, "send_message", fake_send_message)
    monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload = {
        "update_id": 10004,
        "callback_query": {
            "id": "cb3",
            "from": {"id": 777},
            "message": {"message_id": 88, "chat": {"id": -200, "type": "supergroup"}},
            "data": "cmd:sales:2026/04/30",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert len(edited) == 1
    assert edited[0][0] == -200
    assert edited[0][1] == 88
    assert edited[0][2] == "sales:2026/04/30"
    assert edited[0][4] == "Markdown"
    assert sent == []


def test_webhook_text_command_uses_agent_dispatch_for_sales(monkeypatch):
    from routes import openclaw_bot_routes as bot

    dispatched = []
    sent = []
    queried = []

    def fake_openclaw_answer(question, chat_id=None):
        dispatched.append((question, chat_id))
        return ("NL response", None)

    def fake_query_sales(date):
        queried.append(date)
        return {'found': True, 'date': date}

    monkeypatch.setattr(bot, "openclaw_answer", fake_openclaw_answer)
    monkeypatch.setattr(bot, "_OPENCLAW_AGENT_DISPATCH_ENABLED", True)
    monkeypatch.setattr(bot, "_AGENT_DISPATCH_CMDS", {"sales"})
    monkeypatch.setattr(bot, "send_message", lambda *_args, **_kwargs: sent.append(_args))
    monkeypatch.setattr(bot, "query_sales", fake_query_sales)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
    monkeypatch.setattr(bot, "BOT_USERNAME", "@KnownBot")

    app = _build_request_app()
    payload = {
        "update_id": 10011,
        "message": {
            "message_id": 123,
            "chat": {"id": -200, "type": "supergroup"},
            "from": {"id": 777},
            "text": "/sales 2026/04/30",
        },
    }

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
        bot.telegram_webhook()

    assert dispatched == [("請查 2026/04/30 的業績數字（包含營收、訂單數、毛利率）", -200)]
    assert queried == []
    assert sent
    assert sent[0][1] == "NL response"


def test_webhook_cmd_callback_stays_structured_when_dispatch_enabled(monkeypatch):
    from routes import openclaw_bot_routes as bot

    dispatched = []
    queried_sales = []
    edited = []

    def fake_dispatch(cmd, arg, chat_id, reply_to):
        dispatched.append((cmd, arg, chat_id, reply_to))
        return True

    def fake_query_sales(date):
        queried_sales.append(date)
        return {'found': False, 'date': date}

    def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
        edited.append((chat_id, message_id, text))
        return {"ok": True}

    def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown", **_kwargs):
        return {"ok": True}

    monkeypatch.setattr(bot, "_agent_dispatch_cmd", fake_dispatch)
    monkeypatch.setattr(bot, "_OPENCLAW_AGENT_DISPATCH_ENABLED", True)
    monkeypatch.setattr(bot, "_AGENT_DISPATCH_CMDS", {"sales"})
    monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
    monkeypatch.setattr(bot, "send_message", fake_send_message)
    monkeypatch.setattr(bot, "query_sales", fake_query_sales)
    monkeypatch.setattr(bot, "query_top_products", lambda *_args, **_kwargs: [])
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload = {
        "update_id": 10012,
        "callback_query": {
            "id": "cb-menu-sales",
            "from": {"id": 777},
            "message": {"message_id": 444, "chat": {"id": -200, "type": "supergroup"}},
            "data": "cmd:sales:2026/04/30",
        },
    }

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
        bot.telegram_webhook()

    assert dispatched == []
    assert queried_sales == ["2026/04/30"]
    assert len(edited) == 1
    assert edited[0][0] == -200
    assert edited[0][1] == 444
    assert edited[0][2].startswith("⚠️ *查無資料*")


def test_webhook_duplicate_update_id_is_skipped(monkeypatch):
    from routes import openclaw_bot_routes as bot

    calls = []
    answered = []

    def fake_handle_cmd(cmd, arg, chat_id, reply_to):
        calls.append((cmd, arg, chat_id, reply_to))

    monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": answered.append(_cq_id))
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
    monkeypatch.setattr(bot, "send_message", lambda *_args, **_kwargs: {"ok": True})

    app = _build_request_app()
    payload = {
        "update_id": 20001,
        "callback_query": {
            "id": "dup-cb",
            "from": {"id": 777},
            "message": {"message_id": 99, "chat": {"id": -200, "type": "supergroup"}},
            "data": "menu:main",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert calls == []
    assert answered == ["dup-cb", "dup-cb"]


def test_webhook_cmd_callback_ignores_not_modified(monkeypatch):
    from routes import openclaw_bot_routes as bot

    edited = []
    sent = []

    def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
        edited.append((chat_id, message_id, text, keyboard, parse_mode))
        return {"ok": False, "description": "Bad Request: message is not modified"}

    def fake_handle_cmd(cmd, arg, chat_id, reply_to):
        bot.send_message(chat_id, f"{cmd}:{arg}", reply_to=reply_to)

    def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown", **_kwargs):
        sent.append((chat_id, text, reply_to, keyboard, parse_mode))
        return {"ok": True}

    monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
    monkeypatch.setattr(bot, "send_message", fake_send_message)
    monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload = {
        "update_id": 20002,
        "callback_query": {
            "id": "cb4",
            "from": {"id": 777},
            "message": {"message_id": 101, "chat": {"id": -200, "type": "supergroup"}},
            "data": "cmd:sales:2026/04/30",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert edited
    assert sent == []


def test_webhook_menu_callback_does_not_duplicate_on_message_not_found(monkeypatch):
    from routes import openclaw_bot_routes as bot

    sent = []

    def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
        return {
            "ok": False,
            "error_code": 404,
            "description": "Bad Request: message to edit not found",
        }

    def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown", **_kwargs):
        sent.append((chat_id, text, reply_to, keyboard, parse_mode))
        return {"ok": True}

    monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
    monkeypatch.setattr(bot, "send_message", fake_send_message)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload = {
        "update_id": 10006,
        "callback_query": {
            "id": "cb5",
            "from": {"id": 777},
            "message": {"message_id": 222, "chat": {"id": -200, "type": "supergroup"}},
            "data": "menu:main",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert sent == []


def test_webhook_cmd_callback_does_not_duplicate_on_message_not_found(monkeypatch):
    from routes import openclaw_bot_routes as bot

    sent = []

    def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
        return {
            "ok": False,
            "error_code": 404,
            "description": "Bad Request: MESSAGE_ID_INVALID",
        }

    def fake_handle_cmd(cmd, arg, chat_id, reply_to):
        bot.send_message(chat_id, f"{cmd}:{arg}", reply_to=reply_to)

    def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown", **_kwargs):
        sent.append((chat_id, text, reply_to, keyboard, parse_mode))
        return {"ok": True}

    monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
    monkeypatch.setattr(bot, "send_message", fake_send_message)
    monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload = {
        "update_id": 10007,
        "callback_query": {
            "id": "cb6",
            "from": {"id": 777},
            "message": {"message_id": 333, "chat": {"id": -200, "type": "supergroup"}},
            "data": "cmd:sales:2026/04/30",
        },
    }

    with app.test_request_context(
        "/bot/telegram/webhook", method="POST", json=payload
    ):
        bot.telegram_webhook()

    assert sent == []


def test_webhook_callback_dedup_key_without_update_id(monkeypatch):
    from routes import openclaw_bot_routes as bot

    calls = []

    # 當 callback 沒有 update_id 時，第二次同樣 payload 要直接被 dedupe，
    # 但第一次仍應可正常執行一次。
    from services import telegram_update_guard as guard
    guard._seen_update_ids.clear()
    guard._seen_update_id_set.clear()

    def fake_handle_cmd(cmd, arg, chat_id, reply_to):
        calls.append((cmd, arg, chat_id, reply_to))

    monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload = {
        "callback_query": {
            "id": "cb-no-id",
            "from": {"id": 777},
            "message": {"message_id": 123, "chat": {"id": -200, "type": "supergroup"}},
            "data": "cmd:sales:2026/04/30",
        },
    }

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
        bot.telegram_webhook()

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
        bot.telegram_webhook()

    assert calls == [('sales', '2026/04/30', -200, 123)]


def test_webhook_callback_dedup_key_varies_by_message_id(monkeypatch):
    from routes import openclaw_bot_routes as bot

    calls = []

    from services import telegram_update_guard as guard
    guard._seen_update_ids.clear()
    guard._seen_update_id_set.clear()

    def fake_handle_cmd(cmd, arg, chat_id, reply_to):
        calls.append((cmd, arg, chat_id, reply_to))

    monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload_1 = {
        "callback_query": {
            "id": "cb-no-id",
            "from": {"id": 777},
            "message": {"message_id": 201, "chat": {"id": -200, "type": "supergroup"}},
            "data": "cmd:sales:2026/04/30",
        },
    }
    payload_2 = {
        "callback_query": {
            "id": "cb-no-id",
            "from": {"id": 777},
            "message": {"message_id": 202, "chat": {"id": -200, "type": "supergroup"}},
            "data": "cmd:sales:2026/04/30",
        },
    }

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload_1):
        bot.telegram_webhook()

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload_2):
        bot.telegram_webhook()

    assert calls == [
        ('sales', '2026/04/30', -200, 201),
        ('sales', '2026/04/30', -200, 202),
    ]


def test_webhook_callback_dedup_with_same_callback_query_id_different_update_id(monkeypatch):
    from routes import openclaw_bot_routes as bot

    calls = []

    from services import telegram_update_guard as guard
    guard._seen_update_ids.clear()
    guard._seen_update_id_set.clear()

    def fake_handle_cmd(cmd, arg, chat_id, reply_to):
        calls.append((cmd, arg, chat_id, reply_to))

    monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
    monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
    monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
    monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)

    app = _build_request_app()
    payload_1 = {
        "update_id": 30001,
        "callback_query": {
            "id": "cb-repeat",
            "from": {"id": 777},
            "message": {"message_id": 301, "chat": {"id": -200, "type": "supergroup"}},
            "data": "cmd:sales:2026/04/30",
        },
    }
    payload_2 = {
        "update_id": 30002,
        "callback_query": {
            "id": "cb-repeat",
            "from": {"id": 777},
            "message": {"message_id": 301, "chat": {"id": -200, "type": "supergroup"}},
            "data": "cmd:sales:2026/04/30",
        },
    }

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload_1):
        bot.telegram_webhook()

    with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload_2):
        bot.telegram_webhook()

    assert calls == [('sales', '2026/04/30', -200, 301)]
