import importlib
import os
import sys
from unittest.mock import AsyncMock, MagicMock

import pytest
from restate import TerminalError

base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
restate_path = os.path.join(base_dir, "restate_services")
if restate_path not in sys.path:
    sys.path.insert(0, restate_path)

whatsapp_inbound = importlib.import_module("whatsapp_inbound")


def _mock_ctx(key: str = "wamid.test123"):
    ctx = AsyncMock()
    ctx.key = MagicMock(return_value=key)

    async def run_side_effect(_name, fn):
        return await fn()

    ctx.run = AsyncMock(side_effect=run_side_effect)
    return ctx


def test_extract_message_text_text_message() -> None:
    message = {"type": "text", "text": {"body": "Hello"}}

    assert whatsapp_inbound._extract_message_text(message) == "Hello"


def test_extract_message_text_interactive_button_reply() -> None:
    message = {
        "type": "interactive",
        "interactive": {"button_reply": {"title": "Yes", "id": "btn_yes"}},
    }

    assert whatsapp_inbound._extract_message_text(message) == "Yes (btn_yes)"


def test_extract_message_text_interactive_list_reply() -> None:
    message = {
        "type": "interactive",
        "interactive": {"list_reply": {"title": "Dinner", "id": "list_dinner"}},
    }

    assert whatsapp_inbound._extract_message_text(message) == "Dinner (list_dinner)"


def test_extract_message_text_unsupported_type() -> None:
    with pytest.raises(TerminalError, match="Unsupported or empty WhatsApp message type") as exc:
        whatsapp_inbound._extract_message_text({"type": "location"})

    assert exc.value.status_code == 422


def test_extract_message_text_missing_type() -> None:
    with pytest.raises(TerminalError, match="WhatsApp message type is required") as exc:
        whatsapp_inbound._extract_message_text({})

    assert exc.value.status_code == 422


def test_extract_message_text_image_with_caption() -> None:
    message = {"type": "image", "image": {"caption": "Look at this"}}

    assert whatsapp_inbound._extract_message_text(message) == "Look at this"


def test_extract_message_text_button_message() -> None:
    message = {"type": "button", "button": {"text": "Click me"}}

    assert whatsapp_inbound._extract_message_text(message) == "Click me"


def test_chunk_text_short() -> None:
    assert whatsapp_inbound._chunk_text("hello", limit=10) == ["hello"]


def test_chunk_text_long() -> None:
    assert whatsapp_inbound._chunk_text("abcdefghij", limit=4) == ["abcd", "efgh", "ij"]


def test_chunk_text_exact() -> None:
    assert whatsapp_inbound._chunk_text("1234", limit=4) == ["1234"]


@pytest.mark.asyncio(loop_scope="session")
async def test_process_missing_phone_number_id() -> None:
    ctx = _mock_ctx()

    with pytest.raises(TerminalError, match="phone_number_id is required") as exc:
        await whatsapp_inbound.process(
            ctx,
            {
                "message": {
                    "from": "+31612345678",
                    "type": "text",
                    "text": {"body": "Hi"},
                }
            },
        )

    assert exc.value.status_code == 422
    ctx.run.assert_not_called()


@pytest.mark.asyncio(loop_scope="session")
async def test_process_missing_message() -> None:
    ctx = _mock_ctx()

    with pytest.raises(TerminalError, match="message payload is required") as exc:
        await whatsapp_inbound.process(ctx, {"phone_number_id": "12345"})

    assert exc.value.status_code == 422
    ctx.run.assert_not_called()


@pytest.mark.asyncio(loop_scope="session")
async def test_process_missing_sender() -> None:
    ctx = _mock_ctx()
    data = {
        "phone_number_id": "12345",
        "message": {"type": "text", "text": {"body": "Hi"}},
    }

    with pytest.raises(TerminalError, match="message.from is required") as exc:
        await whatsapp_inbound.process(ctx, data)

    assert exc.value.status_code == 422
    ctx.run.assert_not_called()


# ---------------------------------------------------------------------------
# _send_or_terminal: regression for trace 019e66cb89212ea11744b648b34fa9c6
#
# Permanent Meta errors (e.g. (#131037) display-name-not-approved) were
# raising plain WhatsAppSendError inside ctx.run("send_whatsapp_response", …),
# which Restate treated as transient and retried up to 9× per delivery —
# cascading into webhook ReadTimeouts and Meta re-deliveries.
# ---------------------------------------------------------------------------


@pytest.mark.asyncio(loop_scope="session")
async def test_send_or_terminal_passes_through_on_success() -> None:
    async def _ok() -> str:
        return "wamid.ok"

    result = await whatsapp_inbound._send_or_terminal(_ok())

    assert result == "wamid.ok"


@pytest.mark.asyncio(loop_scope="session")
async def test_send_or_terminal_raises_terminal_on_permanent_send_error() -> None:
    from app.services.whatsapp_sender import WhatsAppSendError

    async def _permanent() -> str:
        raise WhatsAppSendError(
            "(#131037) WhatsApp provided number needs display name approval before "
            "message can be sent.",
            is_transient=False,
            meta_error_code=131037,
        )

    with pytest.raises(TerminalError) as exc:
        await whatsapp_inbound._send_or_terminal(_permanent())

    assert exc.value.status_code == 502
    assert "131037" in str(exc.value)


@pytest.mark.asyncio(loop_scope="session")
async def test_send_or_terminal_propagates_transient_send_error() -> None:
    from app.services.whatsapp_sender import WhatsAppSendError

    async def _transient() -> str:
        raise WhatsAppSendError(
            "WhatsApp request failed: 503 Service Unavailable",
            is_transient=True,
        )

    # Transient errors must NOT be promoted — Restate should retry them.
    with pytest.raises(WhatsAppSendError) as exc:
        await whatsapp_inbound._send_or_terminal(_transient())

    assert exc.value.is_transient is True
    assert not isinstance(exc.value, TerminalError)
