from __future__ import annotations

import os
from urllib.error import URLError

import pytest

import eightmem.services.memory_service as memory_service
import eightmem.channels.telegram_adapter as telegram_adapter
from eightmem.channels.telegram_adapter import (
    TelegramConfigError,
    TelegramOutgoingMessage,
    build_send_message_request,
    get_telegram_webhook_info,
    load_telegram_config,
    normalize_telegram_webhook_url,
    parse_telegram_update,
    process_telegram_update,
    send_telegram_message,
    set_telegram_webhook,
)


def test_load_telegram_config_from_env(monkeypatch):
    monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "123:abc")
    monkeypatch.setenv("TELEGRAM_WEBHOOK_SECRET", "secret-1")

    config = load_telegram_config()

    assert config.bot_token == "123:abc"
    assert config.webhook_secret == "secret-1"
    assert config.api_base_url.endswith("/bot123:abc")


def test_load_telegram_config_requires_token(monkeypatch):
    monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)

    with pytest.raises(TelegramConfigError):
        load_telegram_config()


def test_normalize_telegram_webhook_url_appends_webhook_path():
    assert (
        normalize_telegram_webhook_url("https://example.ngrok-free.app")
        == "https://example.ngrok-free.app/channels/telegram/webhook"
    )
    assert (
        normalize_telegram_webhook_url("https://example.ngrok-free.app/channels/telegram/webhook")
        == "https://example.ngrok-free.app/channels/telegram/webhook"
    )


def test_set_telegram_webhook_sends_secret(monkeypatch):
    calls: list[tuple[str, dict]] = []

    def _fake_post(config, method, payload, timeout_seconds=15):
        calls.append((method, payload))
        return {"ok": True, "result": True}

    monkeypatch.setattr(telegram_adapter, "_post_telegram_api", _fake_post)
    result = set_telegram_webhook(
        telegram_adapter.TelegramConfig(bot_token="123:abc", webhook_secret="secret-1"),
        forward_url="https://public.example",
    )

    assert result["ok"] is True
    assert calls == [
        (
            "setWebhook",
            {
                "url": "https://public.example/channels/telegram/webhook",
                "secret_token": "secret-1",
            },
        )
    ]


def test_get_telegram_webhook_info_uses_api(monkeypatch):
    calls: list[tuple[str, dict]] = []

    def _fake_post(config, method, payload, timeout_seconds=10):
        calls.append((method, payload))
        return {"ok": True, "result": {"url": "https://public.example/channels/telegram/webhook"}}

    monkeypatch.setattr(telegram_adapter, "_post_telegram_api", _fake_post)
    result = get_telegram_webhook_info(telegram_adapter.TelegramConfig(bot_token="123:abc"))

    assert result["ok"] is True
    assert calls == [("getWebhookInfo", {})]


def test_parse_telegram_update_text_message():
    payload = {
        "update_id": 101,
        "message": {
            "message_id": 12,
            "text": "Remember: keep answers concise",
            "chat": {"id": 555},
            "from": {"id": 999},
        },
    }

    incoming = parse_telegram_update(payload)

    assert incoming is not None
    assert incoming.update_id == 101
    assert incoming.chat_id == "555"
    assert incoming.user_id == "999"
    assert incoming.text == "Remember: keep answers concise"


def test_parse_telegram_update_callback_query():
    payload = {
        "update_id": 102,
        "callback_query": {
            "id": "cbq-1",
            "data": "Keep no emojis",
            "from": {"id": 999},
            "message": {
                "message_id": 44,
                "chat": {"id": 555},
            },
        },
    }

    incoming = parse_telegram_update(payload)

    assert incoming is not None
    assert incoming.update_id == 102
    assert incoming.chat_id == "555"
    assert incoming.user_id == "999"
    assert incoming.text == "Keep no emojis"
    assert incoming.callback_query_id == "cbq-1"
    assert incoming.source_message_id == 44


def test_process_telegram_update_routes_to_service(fake_home, monkeypatch):
    monkeypatch.setattr(
        memory_service,
        "generate_text",
        lambda **_: type(
            "_FakeResult",
            (),
            {
                "text": "Here is a concise answer from memory.",
                "text_source": "response",
                "done": True,
                "done_reason": "stop",
                "eval_count": 1,
                "eval_duration": 1,
                "raw": {},
            },
        )(),
    )

    payload = {
        "update_id": 202,
        "message": {
            "message_id": 20,
            "text": "Draft: send a short update",
            "chat": {"id": 777},
            "from": {"id": 321},
        },
    }

    outgoing = process_telegram_update(payload, model="qwen2.5:14b")

    assert outgoing is not None
    assert outgoing.chat_id == "777"
    assert outgoing.user_id == "321"
    assert outgoing.update_id == 202
    assert outgoing.action == "draft_with_memory"
    assert "concise answer" in outgoing.text.lower()


def test_process_telegram_update_adds_compare_photo_path(fake_home):
    process_telegram_update(
        {
            "update_id": 203,
            "message": {
                "message_id": 21,
                "text": "Remember: keep answers concise",
                "chat": {"id": 777},
                "from": {"id": 321},
            },
        }
    )

    outgoing = process_telegram_update(
        {
            "update_id": 204,
            "message": {
                "message_id": 22,
                "text": "/compare Draft: write a founder update in my style",
                "chat": {"id": 777},
                "from": {"id": 321},
            },
        }
    )

    assert outgoing is not None
    assert outgoing.action == "compare_with_memory"
    assert outgoing.photo_path
    assert outgoing.photo_path.endswith(".png")


def test_build_send_message_request():
    os.environ["TELEGRAM_BOT_TOKEN"] = "123:abc"
    config = load_telegram_config()
    url, body, headers = build_send_message_request(
        config,
        outgoing=type("_Msg", (), {"chat_id": "777", "text": "hello"})(),
    )

    assert url == "https://api.telegram.org/bot123:abc/sendMessage"
    assert b'"chat_id": "777"' in body
    assert b'"text": "hello"' in body
    assert headers["Content-Type"] == "application/json"


def test_build_send_message_request_supports_distinct_button_callback_data():
    os.environ["TELEGRAM_BOT_TOKEN"] = "123:abc"
    config = load_telegram_config()
    outgoing = TelegramOutgoingMessage(
        chat_id="777",
        text="Approval needed",
        user_id="321",
        update_id=1,
        action="approval_notification",
        reply_keyboard=[
            [
                {"text": "Approve", "callback_data": "approval:approved:abc"},
                {"text": "Deny", "callback_data": "approval:denied:abc"},
            ]
        ],
    )

    _, body, _ = build_send_message_request(config, outgoing)

    assert b'"text": "Approve"' in body
    assert b'"callback_data": "approval:approved:abc"' in body
    assert b'"text": "Deny"' in body
    assert b'"callback_data": "approval:denied:abc"' in body


def test_build_send_message_request_includes_reply_keyboard():
    os.environ["TELEGRAM_BOT_TOKEN"] = "123:abc"
    config = load_telegram_config()
    url, body, headers = build_send_message_request(
        config,
        outgoing=type(
            "_Msg",
            (),
            {
                "chat_id": "777",
                "text": "Choose one",
                "reply_keyboard": [["Keep bullets", "Keep long-form"], ["Review later"], ["/passport", "/compare"]],
                "remove_keyboard": False,
            },
        )(),
    )

    assert url == "https://api.telegram.org/bot123:abc/sendMessage"
    assert b'"reply_markup"' in body
    assert b'"inline_keyboard"' in body
    assert b'"callback_data": "Keep bullets"' in body
    assert b'"Keep bullets"' in body
    assert b'"/passport"' in body
    assert b'"/compare"' in body
    assert headers["Content-Type"] == "application/json"


def test_process_telegram_update_preserves_callback_query_metadata(fake_home):
    process_telegram_update(
        {
            "update_id": 300,
            "message": {
                "message_id": 1,
                "text": "Remember: use emojis",
                "chat": {"id": 777},
                "from": {"id": 321},
            },
        }
    )
    process_telegram_update(
        {
            "update_id": 3001,
            "message": {
                "message_id": 2,
                "text": "Correct: do not use emojis",
                "chat": {"id": 777},
                "from": {"id": 321},
            },
        }
    )

    outgoing = process_telegram_update(
        {
            "update_id": 301,
            "callback_query": {
                "id": "cbq-2",
                "data": "Keep emojis",
                "from": {"id": 321},
                "message": {
                    "message_id": 99,
                    "chat": {"id": 777},
                },
            },
        }
    )

    assert outgoing is not None
    assert outgoing.action == "resolve_memory_conflict"
    assert outgoing.callback_query_id == "cbq-2"
    assert outgoing.source_message_id == 99


def test_send_telegram_message_answers_callback_before_send(monkeypatch):
    os.environ["TELEGRAM_BOT_TOKEN"] = "123:abc"
    config = load_telegram_config()
    calls: list[tuple[str, dict[str, object]]] = []

    def _fake_post(config_obj, method, payload, timeout_seconds=15):
        calls.append((method, payload))
        if method == "answerCallbackQuery":
            return {"ok": True}
        return {"ok": True, "result": {"chat_id": payload["chat_id"], "text": payload["text"]}}

    monkeypatch.setattr(telegram_adapter, "_post_telegram_api", _fake_post)

    result = send_telegram_message(
        config,
        outgoing=type(
            "_Msg",
            (),
            {
                "chat_id": "777",
                "text": "Resolved.",
                "reply_keyboard": [["/passport", "/compare"]],
                "remove_keyboard": False,
                "callback_query_id": "cbq-9",
            },
        )(),
    )

    assert calls[0][0] == "answerCallbackQuery"
    assert calls[0][1]["callback_query_id"] == "cbq-9"
    assert result["ok"] is True


def test_send_telegram_message_sends_compare_photo_after_text(monkeypatch, tmp_path):
    os.environ["TELEGRAM_BOT_TOKEN"] = "123:abc"
    config = load_telegram_config()
    image = tmp_path / "compare.png"
    image.write_bytes(b"png")
    calls: list[tuple[str, object]] = []

    def _fake_post(config_obj, method, payload, timeout_seconds=15):
        calls.append((method, payload))
        return {"ok": True, "result": {"chat_id": payload["chat_id"], "text": payload.get("text", "")}}

    def _fake_photo(config_obj, *, chat_id, photo_path, caption, timeout_seconds=15):
        calls.append(("sendPhoto", {"chat_id": chat_id, "photo_path": photo_path, "caption": caption}))
        return {"ok": True, "result": {"photo": True}}

    monkeypatch.setattr(telegram_adapter, "_post_telegram_api", _fake_post)
    monkeypatch.setattr(telegram_adapter, "_post_telegram_photo_via_curl", _fake_photo)

    result = send_telegram_message(
        config,
        outgoing=type(
            "_Msg",
            (),
            {
                "chat_id": "777",
                "text": "8mem Compare",
                "reply_keyboard": [["/passport", "/compare"]],
                "remove_keyboard": False,
                "callback_query_id": None,
                "photo_path": str(image),
            },
        )(),
    )

    assert calls[0][0] == "sendMessage"
    assert calls[1][0] == "sendPhoto"
    assert calls[1][1]["photo_path"] == str(image)
    assert result["ok"] is True
    assert result["photo"]["ok"] is True


def test_post_telegram_api_falls_back_to_curl_on_urlerror(monkeypatch):
    os.environ["TELEGRAM_BOT_TOKEN"] = "123:abc"
    config = load_telegram_config()

    def _raise_urlerror(req, timeout=15):
        raise URLError("dns failure")

    monkeypatch.setattr(telegram_adapter, "urlopen", _raise_urlerror)
    monkeypatch.setattr(
        telegram_adapter.subprocess,
        "run",
        lambda *args, **kwargs: type(
            "_Proc",
            (),
            {
                "returncode": 0,
                "stdout": '{"ok": true, "result": {"id": 1}}',
                "stderr": "",
            },
        )(),
    )

    result = telegram_adapter._post_telegram_api(config, "getMe", {})

    assert result["ok"] is True
    assert result["result"]["id"] == 1


def test_build_send_message_request_can_remove_reply_keyboard():
    os.environ["TELEGRAM_BOT_TOKEN"] = "123:abc"
    config = load_telegram_config()
    url, body, headers = build_send_message_request(
        config,
        outgoing=type(
            "_Msg",
            (),
            {
                "chat_id": "777",
                "text": "Resolved.",
                "reply_keyboard": None,
                "remove_keyboard": True,
            },
        )(),
    )

    assert url == "https://api.telegram.org/bot123:abc/sendMessage"
    assert b'"remove_keyboard": true' in body
    assert headers["Content-Type"] == "application/json"
