"""Sending invitations for an event that is still a draft, through ASGI against Postgres.

An unpublished event 404s at `/i/{token}` and `/e/{slug}` on purpose — a half-filled event
must not be walkable. The consequence that used to go unstated is that the invitation link
is *dead* while the event is a draft, and a send is the one action that puts that link
somewhere it cannot be recalled from. Both send routes therefore refuse, and both compose
routes say so before the admin writes anything.

The refusal is the API's, not the browser's (design D1). The composer's `event_published`
flag is a hint that goes stale the moment someone unpublishes the event in another tab.
"""

import uuid
from typing import Any

import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

from app.main import app

from .conftest import requires_db

pytestmark = [
    requires_db,
    pytest.mark.usefixtures("app_engine_per_loop", "super_admin"),
]

MESSAGE = {
    "subject": "You're invited",
    "header": "Dear Guest 0",
    "body": "Please come to the Walima.",
    "footer": "Regards\nTest & Case",
}

PANES = [
    {
        "locale": "en",
        "invitation_type": kind,
        "subject": "You're invited",
        "header": "Dear {guest_name}",
        "body": "Please come.\n\n{invitation_link}",
        "footer": "Regards",
    }
    for kind in ("single", "family")
]


def _client() -> AsyncClient:
    return AsyncClient(transport=ASGITransport(app=app), base_url="http://test")


async def _unpublish(session: AsyncSession, event_id: uuid.UUID) -> None:
    await session.execute(
        text("UPDATE event SET is_published = false WHERE id = :id"), {"id": event_id}
    )
    await session.commit()


async def _guest_ids(session: AsyncSession, event_id: uuid.UUID) -> list[str]:
    rows = await session.execute(
        text("SELECT id FROM guest WHERE event_id = :id"), {"id": event_id}
    )
    return [str(row[0]) for row in rows]


# ------------------------------------------------------------------ one guest


async def test_compose_reports_a_published_event_as_sendable(
    fixture_event: dict[str, Any],
) -> None:
    async with _client() as client:
        response = await client.get(
            f"/api/admin/invitations/{fixture_event['invitation_ids'][0]}/message"
        )

    assert response.status_code == 200, response.text
    assert response.json()["event_published"] is True


async def test_compose_reports_a_draft_event_before_the_admin_writes_anything(
    session: AsyncSession, fixture_event: dict[str, Any]
) -> None:
    await _unpublish(session, fixture_event["event_id"])

    async with _client() as client:
        response = await client.get(
            f"/api/admin/invitations/{fixture_event['invitation_ids'][0]}/message"
        )

    # Composing still works — the panel opens and explains itself rather than erroring.
    assert response.status_code == 200, response.text
    assert response.json()["event_published"] is False


async def test_sending_to_one_guest_is_refused_while_the_event_is_a_draft(
    session: AsyncSession, fixture_event: dict[str, Any]
) -> None:
    """The link in that email would land on a 404 for every guest who clicked it."""
    await _unpublish(session, fixture_event["event_id"])
    invitation_id = fixture_event["invitation_ids"][0]

    async with _client() as client:
        response = await client.post(
            f"/api/admin/invitations/{invitation_id}/send",
            json={**MESSAGE, "confirmed_quiet_hours": True},
        )

    assert response.status_code == 409, response.text
    detail = response.json()["detail"]
    assert detail["code"] == "event_unpublished"
    assert "draft" in detail["message"]

    # Refused *before* the insert: a queued job the worker later picks up would deliver the
    # dead link anyway, which is the whole failure this guard exists to prevent.
    queued = await session.scalar(
        text("SELECT count(*) FROM message_job WHERE invitation_id = :id"),
        {"id": invitation_id},
    )
    assert queued == 0


async def test_publishing_the_event_makes_the_same_send_go_through(
    session: AsyncSession, fixture_event: dict[str, Any]
) -> None:
    """The refusal is about the event's state, not about the message or the guest."""
    await _unpublish(session, fixture_event["event_id"])
    invitation_id = fixture_event["invitation_ids"][0]
    body = {**MESSAGE, "confirmed_quiet_hours": True}

    async with _client() as client:
        refused = await client.post(f"/api/admin/invitations/{invitation_id}/send", json=body)
        await session.execute(
            text("UPDATE event SET is_published = true WHERE id = :id"),
            {"id": fixture_event["event_id"]},
        )
        await session.commit()
        accepted = await client.post(f"/api/admin/invitations/{invitation_id}/send", json=body)

    assert refused.status_code == 409
    assert accepted.status_code == 200, accepted.text


# ------------------------------------------------------------------ a batch


async def test_batched_send_is_refused_while_the_event_is_a_draft(
    session: AsyncSession, fixture_event: dict[str, Any]
) -> None:
    """The same refusal at wave scale, where the cost of being wrong is the whole list."""
    event_id = fixture_event["event_id"]
    await _unpublish(session, event_id)
    guest_ids = await _guest_ids(session, event_id)

    async with _client() as client:
        response = await client.post(
            f"/api/admin/events/{event_id}/invitations/send-batch",
            json={
                "batch_id": str(uuid.uuid4()),
                "guest_ids": guest_ids,
                "panes": PANES,
                "confirmed_quiet_hours": True,
            },
        )

    assert response.status_code == 409, response.text
    assert response.json()["detail"]["code"] == "event_unpublished"

    queued = await session.scalar(
        text(
            "SELECT count(*) FROM message_job j JOIN invitation i ON i.id = j.invitation_id "
            "WHERE i.event_id = :id"
        ),
        {"id": event_id},
    )
    assert queued == 0


async def test_bulk_compose_reports_the_draft_state(
    session: AsyncSession, fixture_event: dict[str, Any]
) -> None:
    event_id = fixture_event["event_id"]
    guest_ids = await _guest_ids(session, event_id)

    async with _client() as client:
        published = await client.post(
            f"/api/admin/events/{event_id}/invitations/compose",
            json={"guest_ids": guest_ids},
        )
        await _unpublish(session, event_id)
        draft = await client.post(
            f"/api/admin/events/{event_id}/invitations/compose",
            json={"guest_ids": guest_ids},
        )

    assert published.json()["event_published"] is True
    assert draft.json()["event_published"] is False
