import hashlib
import hmac
import json
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest
from fastapi import FastAPI
from httpx import ASGITransport

from app.db.session import get_bypass_session
from app.models.whatsapp import (
    TemplateCategory,
    TemplateStatus,
    WhatsAppAccount,
    WhatsAppTemplate,
)
from app.routers.whatsapp import router

REALISTIC_WEBHOOK_PAYLOAD: dict[str, Any] = {
    "object": "whatsapp_business_account",
    "entry": [
        {
            "id": "WABA_ID",
            "changes": [
                {
                    "value": {
                        "messaging_product": "whatsapp",
                        "metadata": {
                            "display_phone_number": "15551234567",
                            "phone_number_id": "PHONE_NUMBER_ID",
                        },
                        "messages": [
                            {
                                "from": "31612345678",
                                "id": "wamid.test123",
                                "timestamp": "1700000000",
                                "text": {"body": "I want to make a reservation"},
                                "type": "text",
                            }
                        ],
                    },
                    "field": "messages",
                }
            ],
        }
    ],
}

STATUS_ONLY_WEBHOOK_PAYLOAD: dict[str, Any] = {
    "object": "whatsapp_business_account",
    "entry": [
        {
            "id": "WABA_ID",
            "changes": [
                {
                    "value": {
                        "messaging_product": "whatsapp",
                        "metadata": {
                            "display_phone_number": "15551234567",
                            "phone_number_id": "PHONE_NUMBER_ID",
                        },
                        "statuses": [
                            {
                                "id": "wamid.test123",
                                "status": "delivered",
                                "timestamp": "1700000001",
                                "recipient_id": "31612345678",
                            }
                        ],
                    },
                    "field": "messages",
                }
            ],
        }
    ],
}

TEMPLATE_STATUS_WEBHOOK_PAYLOAD: dict[str, Any] = {
    "object": "whatsapp_business_account",
    "entry": [
        {
            "id": "WABA_ID",
            "changes": [
                {
                    "value": {
                        "message_template_status_update": {
                            "message_template_id": "meta-template-1",
                            "event": "APPROVED",
                        }
                    },
                    "field": "message_template_status_update",
                }
            ],
        }
    ],
}


def compute_signature(body: bytes, secret: str) -> str:
    digest = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return f"sha256={digest}"


def make_execute_result(value):
    result = MagicMock()
    result.scalar_one_or_none.return_value = value
    return result


def make_scalars_all_result(values):
    """Mock for queries that use .scalars().all() (e.g. select(Restaurant.id))."""
    result = MagicMock()
    scalars = MagicMock()
    scalars.all.return_value = values
    result.scalars.return_value = scalars
    return result


def make_whatsapp_account() -> WhatsAppAccount:
    return WhatsAppAccount(
        id="wa-account-1",
        restaurant_id="restaurant-1",
        waba_id="WABA_ID",
        phone_number_id="PHONE_NUMBER_ID",
        phone_number="15551234567",
        display_name="Host",
        access_token_encrypted=b"token",
    )


@pytest.fixture
def webhook_app(mock_session):
    app = FastAPI()
    app.include_router(router, prefix="/api/v1")

    async def override_get_session():
        return mock_session

    app.dependency_overrides[get_bypass_session] = override_get_session
    yield app
    app.dependency_overrides.clear()


@pytest.fixture
def whatsapp_settings() -> SimpleNamespace:
    return SimpleNamespace(
        META_WEBHOOK_VERIFY_TOKEN="test-verify-token",
        META_APP_SECRET="test-app-secret",
        RESTATE_INGRESS_URL="http://restate.test",
        RESTATE_CLOUD_AUTH_TOKEN="",
        REDIS_URL=None,
    )


class StubClient:
    def __init__(self, response: Any) -> None:
        self._response = response
        self.calls: list[tuple[str, dict[str, Any] | None]] = []

    async def post(self, url: str, json: dict[str, Any] | None = None, **_: Any):
        self.calls.append((url, json))
        return self._response

    async def aclose(self) -> None:
        pass


@pytest.mark.asyncio(loop_scope="session")
async def test_full_inbound_text_message_dispatched(
    webhook_app, mock_session, whatsapp_settings
) -> None:
    account = make_whatsapp_account()
    mock_session.execute = AsyncMock(
        side_effect=[
            make_execute_result(account),  # select(WhatsAppAccount)
        ]
    )

    restate_response = MagicMock()
    restate_response.raise_for_status = MagicMock()

    body = json.dumps(REALISTIC_WEBHOOK_PAYLOAD).encode("utf-8")
    signature = compute_signature(body, whatsapp_settings.META_APP_SECRET)
    expected_message = REALISTIC_WEBHOOK_PAYLOAD["entry"][0]["changes"][0]["value"]["messages"][0]
    expected_payload = {
        "restaurant_id": "restaurant-1",
        "whatsapp_account_id": "wa-account-1",
        "phone_number_id": "PHONE_NUMBER_ID",
        "metadata": {
            "display_phone_number": "15551234567",
            "phone_number_id": "PHONE_NUMBER_ID",
        },
        "message": expected_message,
        "sender": {},
    }
    transport = ASGITransport(app=webhook_app)

    with patch("app.routers.whatsapp.get_settings", return_value=whatsapp_settings):
        post_mock = AsyncMock(return_value=restate_response)
        http_client = httpx.AsyncClient()
        http_client.post = post_mock  # type: ignore[attr-defined]
        webhook_app.state.http_client = http_client
        try:
            async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
                await client.post(
                    "/api/v1/webhooks/whatsapp",
                    content=body,
                    headers={"X-Hub-Signature-256": signature},
                )
        finally:
            await http_client.aclose()
            webhook_app.state.http_client = None

    post_mock.assert_awaited_once_with(
        "http://restate.test/WhatsAppInboundHandler/wamid.test123/process/send",
        json=expected_payload,
        headers={},
    )
    restate_response.raise_for_status.assert_called_once_with()


@pytest.mark.asyncio(loop_scope="session")
async def test_full_inbound_no_messages_field(webhook_app, mock_session, whatsapp_settings) -> None:
    account = make_whatsapp_account()
    mock_session.execute = AsyncMock(
        side_effect=[
            make_execute_result(account),  # select(WhatsAppAccount)
        ]
    )

    restate_response = MagicMock()
    restate_response.raise_for_status = MagicMock()
    body = json.dumps(STATUS_ONLY_WEBHOOK_PAYLOAD).encode("utf-8")
    signature = compute_signature(body, whatsapp_settings.META_APP_SECRET)
    transport = ASGITransport(app=webhook_app)

    with patch("app.routers.whatsapp.get_settings", return_value=whatsapp_settings):
        post_mock = AsyncMock(return_value=restate_response)
        http_client = httpx.AsyncClient()
        http_client.post = post_mock  # type: ignore[attr-defined]
        webhook_app.state.http_client = http_client
        try:
            async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
                await client.post(
                    "/api/v1/webhooks/whatsapp",
                    content=body,
                    headers={"X-Hub-Signature-256": signature},
                )
        finally:
            await http_client.aclose()
            webhook_app.state.http_client = None

    post_mock.assert_not_awaited()


@pytest.mark.asyncio(loop_scope="session")
async def test_full_inbound_template_status_update(
    webhook_app, mock_session, whatsapp_settings
) -> None:
    template = WhatsAppTemplate(
        id="template-1",
        restaurant_id="restaurant-1",
        whatsapp_account_id="wa-account-1",
        meta_template_id="meta-template-1",
        name="reservation_confirmation",
        category=TemplateCategory.UTILITY.value,
        language="en_US",
        status=TemplateStatus.PENDING.value,
    )
    mock_session.execute = AsyncMock(
        side_effect=[
            make_execute_result(template),  # select(WhatsAppTemplate)
        ]
    )

    body = json.dumps(TEMPLATE_STATUS_WEBHOOK_PAYLOAD).encode("utf-8")
    signature = compute_signature(body, whatsapp_settings.META_APP_SECRET)
    transport = ASGITransport(app=webhook_app)
    publish_event = AsyncMock()

    with (
        patch("app.routers.whatsapp.get_settings", return_value=whatsapp_settings),
        patch("app.routers.whatsapp._publish_event", publish_event),
    ):
        async with httpx.AsyncClient(
            transport=transport,
            base_url="http://test",
        ) as client:
            await client.post(
                "/api/v1/webhooks/whatsapp",
                content=body,
                headers={"X-Hub-Signature-256": signature},
            )

    assert template.status == TemplateStatus.APPROVED.value
    assert template.rejection_reason is None
    mock_session.add.assert_called_once_with(template)
    mock_session.commit.assert_awaited_once()
    publish_event.assert_awaited_once()
