"""The customer-onboarding path, end to end, in one run (task 11.1).

Create an event, upload a real card with its artwork, publish it, add a single guest and a
family guest, open both invitation links, accept one and decline the other — the whole path
a customer is onboarded through, with no engineer involved after the card file exists.

Run it against the dev stack:

    docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.dev \
      exec api python scripts/walkthrough.py

It refuses to run against production, cleans up after itself, and prints each step as it
goes so a failure names the step it failed on rather than a stack trace in the middle.
"""

import asyncio
import struct
import sys
import uuid
import zlib
from datetime import UTC, datetime, timedelta
from typing import Any

from httpx import ASGITransport, AsyncClient
from sqlalchemy import text

from app.config import get_settings
from app.db import get_session_factory
from app.main import app
from app.models.enums import AdminRole
from app.services.auth import CurrentAdmin, get_current_admin

# A real card: two companion files, CSS-keyframe motion with a reduced-motion branch, and a
# leftover <script> and onclick of the kind a careless export actually carries.
CARD = b"""<div class="card">
  <style>
    .card {
      background: #fdf6ec url(paper.svg) center/cover;
      border: 2px solid #d9b26a; border-radius: 1rem;
      padding: 2rem 1.5rem; text-align: center; color: #5a4632;
      animation: settle 900ms ease-out both;
    }
    .card h1 { font-size: clamp(1.5rem, 8cqw, 2.25rem); margin: 0 0 .5rem; }
    .card img { width: 96px; margin: 1rem auto 0; display: block; }
    @keyframes settle { from { opacity: 0; transform: translateY(12px) } }
    @media (prefers-reduced-motion: reduce) { .card { animation: none } }
  </style>
  <script>console.log('left over from the prototype')</script>
  <h1 onclick="track()">Nazifa &amp; Abdullah</h1>
  <p>Thursday, 24 September 2026</p>
  <p>Rose Garden Convention, Dhaka</p>
  <img src="flourish.svg" alt="">
</div>
"""

PAPER = (
    b'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">'
    b'<rect width="16" height="16" fill="#fdf6ec"/></svg>'
)
FLOURISH = (
    b'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 24">'
    b'<path d="M4 12 Q 24 2 48 12 T 92 12" stroke="#d9b26a" fill="none" stroke-width="2"/></svg>'
)


def _share_image(width: int = 1200, height: int = 630) -> bytes:
    """A 1200x630 PNG header, which is all the preview validator reads (design D6).

    Built rather than committed as a binary: the walkthrough should stay a file anyone can
    read end to end, and a real photograph in the repo would be a fixture nobody can review.
    """

    def chunk(kind: bytes, payload: bytes) -> bytes:
        return (
            struct.pack(">I", len(payload))
            + kind
            + payload
            + struct.pack(">I", zlib.crc32(kind + payload) & 0xFFFFFFFF)
        )

    ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
    signature = bytes([0x89]) + b"PNG" + bytes([0x0D, 0x0A, 0x1A, 0x0A])
    return signature + chunk(b"IHDR", ihdr) + chunk(b"IEND", b"")


SHARE_IMAGE = _share_image()


def step(number: str, message: str) -> None:
    print(f"  {number:>4}  {message}", flush=True)


def check(condition: bool, message: str) -> None:
    if not condition:
        sys.exit(f"\nFAILED: {message}")


async def main() -> None:
    settings = get_settings()
    if settings.is_prod:
        sys.exit("Refusing to run against production: this creates and deletes real data.")

    tag = uuid.uuid4().hex[:6]
    admin_id = uuid.uuid4()
    email = f"walkthrough-{tag}@example.com"

    async with get_session_factory()() as session:
        await session.execute(
            text(
                "INSERT INTO admin_user (id, email, name, role, status, auth_method, "
                "                        must_change_password, failed_login_count) "
                "VALUES (:id, :email, 'Walkthrough', 'super_admin', 'active', 'google', "
                "        false, 0)"
            ),
            {"id": admin_id, "email": email},
        )
        await session.commit()

    app.dependency_overrides[get_current_admin] = lambda: CurrentAdmin(
        id=admin_id, email=email, name="Walkthrough", role=AdminRole.SUPER_ADMIN
    )

    event_id: str | None = None
    try:
        async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
            print("\nCustomer onboarding walkthrough\n")

            # 1 — create the event
            created = await client.post(
                "/api/admin/events",
                json={
                    "type": "walima",
                    "title_en": f"Walima Walkthrough {tag}",
                    "title_bn": "ওয়ালিমা",
                    "starts_at": (datetime.now(UTC) + timedelta(days=40)).isoformat(),
                    "venue_name": "Rose Garden Convention",
                    "venue_address": "Dhaka, Bangladesh",
                    # Two host names and a local-format number: this is the only check that
                    # the "And" rendering and E.164 normalisation both survive a real round
                    # trip through the API rather than only through a unit test.
                    "host_name_1": "Abdul Karim",
                    "host_name_2": "Rahima Karim",
                    "host_phone": "01711223344",
                    "is_published": True,
                },
            )
            check(created.status_code == 201, f"create event: {created.text}")
            event: dict[str, Any] = created.json()
            event_id = event["id"]
            check(
                event["host_phone"] == "+8801711223344",
                f"host phone normalised to E.164, got {event['host_phone']}",
            )
            step("1", f"event created, reachable at /e/{event['slug']}")

            # 2 — upload the card with its artwork
            upload = await client.post(
                f"/api/admin/events/{event_id}/card-designs",
                files=[
                    ("document", ("card.html", CARD, "text/html")),
                    ("assets", ("paper.svg", PAPER, "image/svg+xml")),
                    ("assets", ("flourish.svg", FLOURISH, "image/svg+xml")),
                    ("og_image", ("share.png", SHARE_IMAGE, "image/png")),
                ],
            )
            check(upload.status_code == 201, f"upload card: {upload.text}")
            design = upload.json()
            document = design["config"]["document"]
            check("<script" not in document, "the leftover script survived the sanitiser")
            check("onclick" not in document, "the onclick handler survived the sanitiser")
            check("paper.svg" not in document, "a relative reference was not rewritten")
            check("prefers-reduced-motion" in document, "the reduced-motion branch was lost")
            # The preview image is not a file the card references, so it must not have
            # landed in the rewrite table alongside the companions (design D2).
            check(design["preview_image"] is not None, "the preview image was not stored")
            check(
                "share.png" not in design["assets"],
                "the preview image leaked into the card's asset map",
            )
            step("2", f"card uploaded as draft v{design['version']}, {design['total_bytes']} bytes")

            # 3 — publish it
            published = await client.post(f"/api/admin/card-designs/{design['id']}/publish")
            check(published.status_code == 200, f"publish: {published.text}")
            step("3", "card published — this is the step guests feel")

            # 3b — the link now previews, identically on both routes and for every guest
            public = await client.get(f"/api/events/{event['slug']}")
            check(public.status_code == 200, f"open event: {public.text}")
            preview = public.json()["preview"]
            check(preview["image"] is not None, "the published preview image is not on the page")
            check(
                preview["canonical_url"].endswith(f"/e/{event['slug']}"),
                "the preview does not canonicalise to the public page",
            )
            step("3b", "links preview with the card's share image")

            # 4 — add one single guest and one family guest
            guests = {}
            for name, invitation_type, seats in (
                ("Ayesha Siddika", "single", 1),
                ("Rahim Uddin", "family", 4),
            ):
                response = await client.post(
                    f"/api/admin/events/{event_id}/guests",
                    json={
                        "full_name": name,
                        "email": f"{name.split()[0].lower()}-{tag}@example.com",
                        "invitation_type": invitation_type,
                        "max_guests": seats,
                    },
                )
                check(response.status_code == 201, f"add {name}: {response.text}")
                guests[invitation_type] = response.json()
            step("4", "two guests added, each with their own invitation and token")

            # 5 — open both links, as a guest would
            for invitation_type, guest in guests.items():
                token = guest["invitations"][0]["token"]
                page = await client.get(f"/api/invitations/by-token/{token}")
                check(page.status_code == 200, f"open {invitation_type} link: {page.text}")
                body = page.json()
                check(body["card"] is not None, "the published card is not on the invitation")
                check(
                    body["greeting"]["invitation_type"] == invitation_type,
                    f"{invitation_type} guest got the wrong greeting",
                )
                check(
                    guest["full_name"] not in (body["card"]["document"] or ""),
                    "a guest's name appears inside the card, which is fixed for the event",
                )
                # A crawler fetching this link learns nothing a visitor to the public page
                # could not read, which is what makes previewing a tokenized URL safe (D3).
                check(
                    body["preview"] == public.json()["preview"],
                    "the tokenized preview differs from the public one",
                )
                check(
                    guest["full_name"] not in str(body["preview"]),
                    "a guest's name reached the link preview",
                )
            step("5", "both links open, each with the card and its own greeting")

            # 6 — one accepts, one declines
            accept_token = guests["family"]["invitations"][0]["token"]
            accepted = await client.post(
                f"/api/rsvp/{accept_token}/accept",
                json={
                    "name": "Rahim Uddin",
                    "email": f"rahim-{tag}@example.com",
                    "party_size": 4,
                },
            )
            check(accepted.status_code == 200, f"accept: {accepted.text}")

            decline_token = guests["single"]["invitations"][0]["token"]
            declined = await client.post(f"/api/rsvp/{decline_token}/decline", json={})
            check(declined.status_code == 200, f"decline: {declined.text}")
            step("6", "one RSVP accepted for 4, one declined")

            # 7 — the numbers the host actually watches
            impact = await client.get(f"/api/admin/events/{event_id}/delete-impact")
            check(impact.status_code == 200, "delete impact")
            check(impact.json()["guests"] == 2, "the event does not hold both guests")
            check(impact.json()["responses"] == 2, "both responses are not recorded")
            step("7", "the event reports 2 guests and 2 responses")

            print("\n  All steps passed.\n")

    finally:
        async with get_session_factory()() as session:
            if event_id:
                await session.execute(text("DELETE FROM event WHERE id = :id"), {"id": event_id})
            await session.execute(
                text("DELETE FROM audit_log WHERE admin_user_id = :id"), {"id": admin_id}
            )
            await session.execute(text("DELETE FROM admin_user WHERE id = :id"), {"id": admin_id})
            await session.commit()
        app.dependency_overrides.pop(get_current_admin, None)


if __name__ == "__main__":
    asyncio.run(main())
