from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, call, patch

import httpx
import pytest
from fastapi import FastAPI, HTTPException
from httpx import ASGITransport

from app.auth.tenant import get_current_restaurant, get_tenant_session
from app.models.whatsapp import WhatsAppAccount
from app.routers.whatsapp_management import router


def _mock_settings(**overrides):
    defaults = {
        "WHATSAPP_TOKEN_ENCRYPTION_KEY": "test-key",
        "META_GRAPH_API_VERSION": "v21.0",
    }
    defaults.update(overrides)
    return SimpleNamespace(**defaults)


def _mock_restaurant(restaurant_id: str = "rest-123"):
    restaurant = MagicMock()
    restaurant.id = restaurant_id
    restaurant.name = "Test Restaurant"
    return restaurant


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


def _scalars_result(values):
    result = MagicMock()
    scalars = MagicMock()
    scalars.all.return_value = values
    result.scalars.return_value = scalars
    return result


def _build_app(mock_session, restaurant=None) -> FastAPI:
    app = FastAPI()
    app.include_router(router, prefix="/api/v1")
    current_restaurant = restaurant or _mock_restaurant()

    async def override_get_tenant_session():
        return mock_session

    async def override_get_current_restaurant():
        return current_restaurant

    app.dependency_overrides[get_tenant_session] = override_get_tenant_session
    app.dependency_overrides[get_current_restaurant] = override_get_current_restaurant
    return app


@pytest.mark.asyncio(loop_scope="session")
async def test_callback_success_new_account(mock_session):
    app = _build_app(mock_session)
    transport = ASGITransport(app=app)

    async def mock_refresh(obj):
        if not getattr(obj, "id", None):
            obj.id = "new-account-id"

    mock_session.refresh = AsyncMock(side_effect=mock_refresh)
    mock_session.execute.side_effect = [
        _scalar_result(None),
        _scalar_result(None),
        _scalars_result([]),
        _scalars_result([MagicMock(), MagicMock()]),
    ]

    with (
        patch(
            "app.routers.whatsapp_management.get_settings",
            return_value=_mock_settings(),
        ),
        patch(
            "app.routers.whatsapp_management._exchange_token_and_fetch_metadata",
            new_callable=AsyncMock,
            return_value=("exchanged-token", "waba-123", "phone-123", None, True, None),
        ),
        patch(
            "app.routers.whatsapp_management.encrypt_token",
            return_value=b"encrypted",
        ) as mock_encrypt,
        patch(
            "app.routers.whatsapp_management._provision_default_templates",
            new_callable=AsyncMock,
            return_value=[],
        ) as mock_provision,
        patch(
            "app.routers.whatsapp_management.commit_write",
            new_callable=AsyncMock,
        ) as mock_commit_write,
    ):
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.post(
                "/api/v1/whatsapp/callback",
                json={
                    "code": "auth-code",
                    "waba_id": "waba-123",
                    "phone_number_id": "phone-123",
                },
            )

    app.dependency_overrides.clear()

    assert response.status_code == 200
    assert response.json()["connected"] is True
    assert response.json()["template_count"] == 2
    assert response.json()["message"] == "WhatsApp account connected successfully"
    added_account = mock_session.add.call_args.args[0]
    assert isinstance(added_account, WhatsAppAccount)
    assert response.json()["account_id"] == added_account.id
    assert added_account.restaurant_id == "rest-123"
    assert added_account.waba_id == "waba-123"
    assert added_account.phone_number_id == "phone-123"
    assert added_account.access_token_encrypted == b"encrypted"
    assert added_account.is_active is True
    assert isinstance(added_account.connected_at, datetime)
    mock_encrypt.assert_called_once_with("exchanged-token")
    mock_provision.assert_awaited_once_with(mock_session, "rest-123", added_account.id)
    mock_commit_write.assert_awaited_once_with(mock_session)


@pytest.mark.asyncio(loop_scope="session")
async def test_callback_success_existing_account_update(mock_session):
    app = _build_app(mock_session)
    transport = ASGITransport(app=app)
    existing_account = WhatsAppAccount(
        id="existing-account-id",
        restaurant_id="rest-123",
        waba_id="old-waba",
        phone_number_id="phone-123",
        phone_number="+31612345678",
        display_name="Old Display Name",
        access_token_encrypted=b"old-token",
        connected_at=None,
        is_active=False,
    )
    other_active_account = WhatsAppAccount(
        id="other-active-id",
        restaurant_id="rest-123",
        waba_id="another-waba",
        phone_number_id="phone-999",
        phone_number="+31600000000",
        display_name="Another Account",
        access_token_encrypted=b"token",
        connected_at=None,
        is_active=True,
    )
    mock_session.execute.side_effect = [
        _scalar_result(existing_account),
        _scalars_result([existing_account, other_active_account]),
        _scalars_result([MagicMock()]),
    ]

    with (
        patch(
            "app.routers.whatsapp_management.get_settings",
            return_value=_mock_settings(),
        ),
        patch(
            "app.routers.whatsapp_management._exchange_token_and_fetch_metadata",
            new_callable=AsyncMock,
            return_value=("exchanged-token", "new-waba", "phone-123", None, True, None),
        ),
        patch(
            "app.routers.whatsapp_management.encrypt_token",
            return_value=b"encrypted",
        ) as mock_encrypt,
        patch(
            "app.routers.whatsapp_management._provision_default_templates",
            new_callable=AsyncMock,
            return_value=[],
        ) as mock_provision,
        patch(
            "app.routers.whatsapp_management.commit_write",
            new_callable=AsyncMock,
        ) as mock_commit_write,
    ):
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.post(
                "/api/v1/whatsapp/callback",
                json={
                    "code": "updated-auth-code",
                    "waba_id": "new-waba",
                    "phone_number_id": "phone-123",
                },
            )

    app.dependency_overrides.clear()

    assert response.status_code == 200
    assert response.json() == {
        "connected": True,
        "account_id": "existing-account-id",
        "template_count": 1,
        "message": "WhatsApp account connected successfully",
        "webhooks_subscribed": True,
    }
    assert existing_account.waba_id == "new-waba"
    assert existing_account.phone_number_id == "phone-123"
    assert existing_account.access_token_encrypted == b"encrypted"
    assert existing_account.is_active is True
    assert isinstance(existing_account.connected_at, datetime)
    assert other_active_account.is_active is False
    assert mock_session.add.call_args_list == [call(other_active_account), call(existing_account)]
    mock_encrypt.assert_called_once_with("exchanged-token")
    mock_provision.assert_awaited_once_with(mock_session, "rest-123", "existing-account-id")
    mock_commit_write.assert_awaited_once_with(mock_session)


@pytest.mark.asyncio(loop_scope="session")
async def test_callback_empty_code(mock_session):
    app = _build_app(mock_session)
    transport = ASGITransport(app=app)

    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.post(
            "/api/v1/whatsapp/callback",
            json={
                "code": "   ",
                "waba_id": "waba-123",
                "phone_number_id": "phone-123",
            },
        )

    app.dependency_overrides.clear()

    assert response.status_code == 400
    assert response.json() == {"detail": "OAuth code is required"}
    mock_session.execute.assert_not_awaited()


@pytest.mark.asyncio(loop_scope="session")
async def test_callback_missing_waba_id(mock_session):
    app = _build_app(mock_session)
    transport = ASGITransport(app=app)

    with (
        patch(
            "app.routers.whatsapp_management.get_settings",
            return_value=_mock_settings(),
        ),
        patch(
            "app.routers.whatsapp_management._exchange_token_and_fetch_metadata",
            new_callable=AsyncMock,
            side_effect=HTTPException(
                status_code=502,
                detail="Could not determine WhatsApp Business Account or phone number.",
            ),
        ),
    ):
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.post(
                "/api/v1/whatsapp/callback",
                json={
                    "code": "auth-code",
                    "waba_id": None,
                    "phone_number_id": "phone-123",
                },
            )

    app.dependency_overrides.clear()

    assert response.status_code == 502
    mock_session.execute.assert_not_awaited()


@pytest.mark.asyncio(loop_scope="session")
async def test_callback_missing_phone_number_id(mock_session):
    app = _build_app(mock_session)
    transport = ASGITransport(app=app)

    with (
        patch(
            "app.routers.whatsapp_management.get_settings",
            return_value=_mock_settings(),
        ),
        patch(
            "app.routers.whatsapp_management._exchange_token_and_fetch_metadata",
            new_callable=AsyncMock,
            side_effect=HTTPException(
                status_code=502,
                detail="Could not determine WhatsApp Business Account or phone number.",
            ),
        ),
    ):
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.post(
                "/api/v1/whatsapp/callback",
                json={
                    "code": "auth-code",
                    "waba_id": "waba-123",
                    "phone_number_id": None,
                },
            )

    app.dependency_overrides.clear()

    assert response.status_code == 502
    mock_session.execute.assert_not_awaited()


@pytest.mark.asyncio(loop_scope="session")
async def test_callback_missing_encryption_key(mock_session):
    app = _build_app(mock_session)
    transport = ASGITransport(app=app)

    with patch(
        "app.routers.whatsapp_management.get_settings",
        return_value=_mock_settings(WHATSAPP_TOKEN_ENCRYPTION_KEY=""),
    ):
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.post(
                "/api/v1/whatsapp/callback",
                json={
                    "code": "auth-code",
                    "waba_id": "waba-123",
                    "phone_number_id": "phone-123",
                },
            )

    app.dependency_overrides.clear()

    assert response.status_code == 503
    assert response.json() == {"detail": "WhatsApp token encryption is not configured"}
    mock_session.execute.assert_not_awaited()


@pytest.mark.asyncio(loop_scope="session")
async def test_callback_phone_number_conflict(mock_session):
    app = _build_app(mock_session)
    transport = ASGITransport(app=app)
    conflicting_account = WhatsAppAccount(
        id="conflict-account-id",
        restaurant_id="other-restaurant",
        waba_id="other-waba",
        phone_number_id="phone-123",
        phone_number="+31612345678",
        display_name="Other Restaurant",
        access_token_encrypted=b"encrypted-token",
        connected_at=datetime.now(UTC).replace(tzinfo=None),
    )
    mock_session.execute.side_effect = [_scalar_result(conflicting_account)]

    with (
        patch(
            "app.routers.whatsapp_management.get_settings",
            return_value=_mock_settings(),
        ),
        patch(
            "app.routers.whatsapp_management._exchange_token_and_fetch_metadata",
            new_callable=AsyncMock,
            return_value=("exchanged-token", "waba-123", "phone-123", None, True, None),
        ),
        patch(
            "app.routers.whatsapp_management.encrypt_token",
            return_value=b"encrypted",
        ) as mock_encrypt,
        patch(
            "app.routers.whatsapp_management._provision_default_templates",
            new_callable=AsyncMock,
            return_value=[],
        ) as mock_provision,
        patch(
            "app.routers.whatsapp_management.commit_write",
            new_callable=AsyncMock,
        ) as mock_commit_write,
    ):
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.post(
                "/api/v1/whatsapp/callback",
                json={
                    "code": "auth-code",
                    "waba_id": "waba-123",
                    "phone_number_id": "phone-123",
                },
            )

    app.dependency_overrides.clear()

    assert response.status_code == 409
    assert response.json() == {
        "detail": ("This WhatsApp phone number is already connected to another restaurant")
    }
    mock_encrypt.assert_called_once_with("exchanged-token")
    mock_provision.assert_not_awaited()
    mock_commit_write.assert_not_awaited()


def _mock_http_client(metadata_payload: dict | None, metadata_success: bool = True) -> MagicMock:
    """Build an ``httpx.AsyncClient`` stand-in usable as an async context manager."""
    metadata_resp = MagicMock()
    metadata_resp.is_success = metadata_success
    metadata_resp.json.return_value = metadata_payload or {}
    metadata_resp.text = "" if metadata_success else "metadata fetch failed"
    metadata_resp.status_code = 200 if metadata_success else 400

    client = MagicMock()
    client.__aenter__ = AsyncMock(return_value=client)
    client.__aexit__ = AsyncMock(return_value=None)
    client.get = AsyncMock(return_value=metadata_resp)
    client.post = AsyncMock()
    return client


@pytest.mark.asyncio(loop_scope="session")
async def test_connect_direct_success_persists_and_reports_subscription(mock_session):
    app = _build_app(mock_session)
    transport = ASGITransport(app=app)

    async def mock_refresh(obj):
        if not getattr(obj, "id", None):
            obj.id = "new-account-id"

    mock_session.refresh = AsyncMock(side_effect=mock_refresh)
    mock_session.execute.side_effect = [
        _scalar_result(None),
        _scalar_result(None),
        _scalars_result([]),
        _scalars_result([]),
        _scalars_result([]),
    ]
    fake_client = _mock_http_client({"display_phone_number": "+31612345678"})

    with (
        patch(
            "app.routers.whatsapp_management.get_settings",
            return_value=_mock_settings(),
        ),
        patch(
            "app.routers.whatsapp_management._meta_http_client",
            return_value=fake_client,
        ),
        patch(
            "app.routers.whatsapp_management._try_subscribe_to_waba",
            new_callable=AsyncMock,
            return_value=(True, None),
        ) as mock_subscribe,
        patch(
            "app.routers.whatsapp_management.encrypt_token",
            return_value=b"encrypted",
        ) as mock_encrypt,
        patch(
            "app.routers.whatsapp_management._provision_default_templates",
            new_callable=AsyncMock,
            return_value=[],
        ),
        patch(
            "app.routers.whatsapp_management.commit_write",
            new_callable=AsyncMock,
        ) as mock_commit_write,
    ):
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.post(
                "/api/v1/whatsapp/connect-direct",
                json={
                    "access_token": "user-supplied-token",
                    "phone_number_id": "phone-123",
                    "waba_id": "waba-123",
                },
            )

    app.dependency_overrides.clear()

    assert response.status_code == 200
    body = response.json()
    assert body["connected"] is True
    assert body["webhooks_subscribed"] is True
    assert "webhook_subscription_error" not in body

    # Subscribe call uses the *user-supplied* bearer token.
    mock_subscribe.assert_awaited_once()
    call = mock_subscribe.await_args
    assert call is not None
    assert call.args[2] == "waba-123"
    assert call.args[3] == "user-supplied-token"
    assert call.kwargs["restaurant_id"] == "rest-123"

    mock_encrypt.assert_called_once_with("user-supplied-token")
    mock_commit_write.assert_awaited_once_with(mock_session)


@pytest.mark.asyncio(loop_scope="session")
async def test_connect_direct_subscribe_failure_persists_with_warning(mock_session):
    """Subscribe failure must not block outbound credentials; just surface it."""
    app = _build_app(mock_session)
    transport = ASGITransport(app=app)

    async def mock_refresh(obj):
        if not getattr(obj, "id", None):
            obj.id = "new-account-id"

    mock_session.refresh = AsyncMock(side_effect=mock_refresh)
    mock_session.execute.side_effect = [
        _scalar_result(None),
        _scalar_result(None),
        _scalars_result([]),
        _scalars_result([]),
        _scalars_result([]),
    ]
    fake_client = _mock_http_client({"display_phone_number": "+31612345678"})

    with (
        patch(
            "app.routers.whatsapp_management.get_settings",
            return_value=_mock_settings(),
        ),
        patch(
            "app.routers.whatsapp_management._meta_http_client",
            return_value=fake_client,
        ),
        patch(
            "app.routers.whatsapp_management._try_subscribe_to_waba",
            new_callable=AsyncMock,
            return_value=(False, "(#200) Permissions error"),
        ),
        patch(
            "app.routers.whatsapp_management.encrypt_token",
            return_value=b"encrypted",
        ) as mock_encrypt,
        patch(
            "app.routers.whatsapp_management._provision_default_templates",
            new_callable=AsyncMock,
            return_value=[],
        ),
        patch(
            "app.routers.whatsapp_management.commit_write",
            new_callable=AsyncMock,
        ) as mock_commit_write,
    ):
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            response = await client.post(
                "/api/v1/whatsapp/connect-direct",
                json={
                    "access_token": "user-supplied-token",
                    "phone_number_id": "phone-123",
                    "waba_id": "waba-123",
                },
            )

    app.dependency_overrides.clear()

    assert response.status_code == 200
    body = response.json()
    assert body["connected"] is True
    assert body["webhooks_subscribed"] is False
    assert body["webhook_subscription_error"] == "(#200) Permissions error"
    assert "(#200) Permissions error" in body["message"]
    assert "Manage messages" in body["message"]

    # Credentials must still be saved so outbound messaging works.
    mock_encrypt.assert_called_once_with("user-supplied-token")
    mock_commit_write.assert_awaited_once_with(mock_session)
    assert mock_session.add.called


@pytest.mark.asyncio(loop_scope="session")
async def test_try_subscribe_to_waba_uses_user_token_on_success():
    from app.routers.whatsapp_management import _try_subscribe_to_waba

    sub_resp = MagicMock()
    sub_resp.is_success = True
    client = MagicMock()
    client.post = AsyncMock(return_value=sub_resp)

    ok, error = await _try_subscribe_to_waba(
        client,
        "https://graph.facebook.com/v21.0",
        "waba-123",
        "user-token-abc",
    )

    assert ok is True
    assert error is None
    client.post.assert_awaited_once_with(
        "https://graph.facebook.com/v21.0/waba-123/subscribed_apps",
        headers={"Authorization": "Bearer user-token-abc"},
    )


@pytest.mark.asyncio(loop_scope="session")
async def test_try_subscribe_to_waba_returns_error_without_raising():
    from app.routers.whatsapp_management import _try_subscribe_to_waba

    sub_resp = MagicMock()
    sub_resp.is_success = False
    sub_resp.status_code = 403
    sub_resp.text = '{"error":{"message":"(#200) Permissions error"}}'
    sub_resp.json.return_value = {"error": {"message": "(#200) Permissions error"}}
    client = MagicMock()
    client.post = AsyncMock(return_value=sub_resp)

    ok, error = await _try_subscribe_to_waba(
        client,
        "https://graph.facebook.com/v21.0",
        "waba-123",
        "user-token-abc",
        restaurant_id="rest-123",
    )

    assert ok is False
    assert error is not None
    assert "(#200) Permissions error" in error
