"""
Tests for the admin notification bell.

Covers:
  - a notification row is created for each of the three public enquiry triggers
    (cemetery contact form, plot availability enquiry, marketing enquiry)
  - the two streams are separate: a tenant never sees platform notifications
    and a site admin never sees a tenant's
  - cross-tenant isolation on list, count, mark-read and mark-all-read
  - unread count and read transitions

Note: no `asyncio_mode = auto` is configured in this repo, so every async test
must carry @pytest.mark.asyncio explicitly or it silently does not run.
"""
from __future__ import annotations

import uuid

import pytest
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.auth.models.user import User
from src.apps.notifications.constants import NotificationScope, NotificationType
from src.apps.notifications.models.notification import Notification
from src.apps.notifications.services.notification_service import NotificationService
from src.apps.tenants.models.account import Account
from src.core.security import build_token_payload, create_access_token, hash_password

TENANT_URL = "/api/v1/notifications"
ADMIN_URL = "/api/site-admin/notifications"


# ── Helpers ───────────────────────────────────────────────────────────────────


async def _create_account(db: AsyncSession, *, org: str = "Test Cemetery") -> Account:
    subdomain = f"cem-{uuid.uuid4().hex[:10]}"
    account = Account(
        organization_name=org,
        subdomain=subdomain,
        contact_email=f"contact@{subdomain}.ca",
        plan="starter",
        # Must be 'active': require_onboarding_complete gates the whole tenant
        # router on account.status, not on onboarding_completed_at.
        status="active",
    )
    db.add(account)
    await db.flush()
    return account


async def _create_user(
    db: AsyncSession,
    account: Account | None,
    *,
    role: str = "administrator",
    email: str | None = None,
) -> User:
    email = email or f"{role}-{uuid.uuid4().hex[:8]}@test.ca"
    user = User(
        tenant_id=account.id if account else None,
        email=email,
        password_hash=hash_password("Password123!"),
        first_name="Test",
        last_name=role.title(),
        role=role,
        status="active",
    )
    db.add(user)
    await db.flush()
    return user


def _headers(user: User, account: Account | None = None) -> dict:
    token = create_access_token(build_token_payload(user, account))
    headers = {"Authorization": f"Bearer {token}"}
    if account is not None:
        headers["X-Tenant-ID"] = str(account.id)
    return headers


async def _make_tenant(db: AsyncSession, *, org: str = "Test Cemetery", role: str = "administrator"):
    account = await _create_account(db, org=org)
    user = await _create_user(db, account, role=role)
    return _headers(user, account), user, account


async def _site_admin_headers(db: AsyncSession) -> dict:
    user = await _create_user(
        db, None, role="site_admin", email=f"sa-{uuid.uuid4().hex[:8]}@indelis.com"
    )
    return _headers(user)


async def _seed_tenant_notification(db: AsyncSession, account: Account, *, title: str = "Tenant enquiry"):
    return await NotificationService(db).create(
        scope=NotificationScope.TENANT,
        tenant_id=account.id,
        type=NotificationType.ENQUIRY_CONTACT_FORM,
        title=title,
        body="Body text.",
        entity_type="contact_inquiry",
        entity_id=uuid.uuid4(),
        link="/portal/sales?tab=inbox",
    )


async def _seed_platform_notification(db: AsyncSession, *, title: str = "Platform enquiry"):
    return await NotificationService(db).create(
        scope=NotificationScope.PLATFORM,
        type=NotificationType.ENQUIRY_MARKETING,
        title=title,
        body="Body text.",
        entity_type="website_enquiry",
        entity_id=uuid.uuid4(),
        link="/admin/enquiries",
    )


# ── Triggers: a notification row is written on public enquiry submission ──────


@pytest.mark.asyncio
async def test_cemetery_contact_form_creates_tenant_notification(
    client: AsyncClient, db_session: AsyncSession, monkeypatch
):
    # POST /inquiries enforces reCAPTCHA whenever a secret key is configured
    # (it is, in this environment). Clearing the key takes the route's own
    # documented "skipped when not configured" path rather than stubbing the
    # verifier out.
    from src.core.config import settings

    monkeypatch.setattr(settings, "RECAPTCHA_SECRET_KEY", "", raising=False)

    _, _, account = await _make_tenant(db_session)

    resp = await client.post(
        "/api/public/inquiries",
        json={
            "topic": "pre_need_plot",
            "sender_name": "Jane Doe",
            "sender_email": "jane@example.com",
            "sender_phone": "(613) 555-0100",
            "message": "I would like information about plots.",
        },
        headers={"X-Tenant-ID": str(account.id)},
    )
    assert resp.status_code == 201, resp.text

    rows = (
        (
            await db_session.execute(
                select(Notification).where(Notification.tenant_id == account.id)
            )
        )
        .scalars()
        .all()
    )
    assert len(rows) == 1
    assert rows[0].recipient_scope == NotificationScope.TENANT.value
    assert rows[0].type == NotificationType.ENQUIRY_CONTACT_FORM.value
    assert rows[0].entity_type == "contact_inquiry"
    assert "Jane Doe" in rows[0].title
    assert rows[0].read_at is None
    assert rows[0].link.startswith("/portal/sales")


@pytest.mark.asyncio
async def test_availability_enquiry_creates_tenant_notification(
    client: AsyncClient, db_session: AsyncSession
):
    _, _, account = await _make_tenant(db_session)

    resp = await client.post(
        "/api/public/availability-enquiry",
        json={
            "sender_name": "John Smith",
            "sender_email": "john@example.com",
            "sender_phone": "(613) 555-0101",
            "message": "Interested in section B.",
            "plot_number": "B-12",
            "section_name": "Birch",
        },
        headers={"X-Tenant-ID": str(account.id)},
    )
    assert resp.status_code == 201, resp.text

    row = (
        await db_session.execute(
            select(Notification).where(Notification.tenant_id == account.id)
        )
    ).scalar_one()
    assert row.type == NotificationType.ENQUIRY_PLOT_AVAILABILITY.value
    # The plot number is surfaced in the bell title so an admin can triage
    # without opening the enquiry.
    assert "B-12" in row.title


@pytest.mark.asyncio
async def test_marketing_book_a_call_creates_platform_notification(
    client: AsyncClient, db_session: AsyncSession
):
    resp = await client.post(
        "/api/public/book-a-call",
        json={
            "name": "Marketing Lead",
            "email": "lead@example.com",
            "organization": "Somewhere Cemetery",
            "message": "Please call me.",
            "day": "Monday",
            "time": "10:00",
        },
    )
    assert resp.status_code == 201, resp.text

    rows = (
        (
            await db_session.execute(
                select(Notification).where(
                    Notification.recipient_scope == NotificationScope.PLATFORM.value,
                    Notification.entity_type == "website_enquiry",
                )
            )
        )
        .scalars()
        .all()
    )
    assert len(rows) >= 1
    row = rows[-1]
    # Platform rows must never carry a tenant_id — the DB check constraint
    # enforces this too.
    assert row.tenant_id is None
    assert row.type == NotificationType.ENQUIRY_MARKETING.value
    assert row.link.startswith("/admin/enquiries")


# ── Stream separation ─────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_tenant_does_not_see_platform_notifications(
    client: AsyncClient, db_session: AsyncSession
):
    headers, _, account = await _make_tenant(db_session)
    await _seed_tenant_notification(db_session, account, title="Mine")
    await _seed_platform_notification(db_session, title="Not mine")

    resp = await client.get(TENANT_URL, headers=headers)
    assert resp.status_code == 200, resp.text
    titles = {item["title"] for item in resp.json()["data"]}
    assert titles == {"Mine"}


@pytest.mark.asyncio
async def test_site_admin_does_not_see_tenant_notifications(
    client: AsyncClient, db_session: AsyncSession
):
    sa_headers = await _site_admin_headers(db_session)
    _, _, account = await _make_tenant(db_session)
    await _seed_tenant_notification(db_session, account, title="Tenant only")
    await _seed_platform_notification(db_session, title="Platform only")

    resp = await client.get(ADMIN_URL, headers=sa_headers)
    assert resp.status_code == 200, resp.text
    titles = {item["title"] for item in resp.json()["data"]}
    assert "Tenant only" not in titles
    assert "Platform only" in titles


@pytest.mark.asyncio
async def test_tenant_cannot_see_another_tenants_notifications(
    client: AsyncClient, db_session: AsyncSession
):
    headers_a, _, account_a = await _make_tenant(db_session, org="Cemetery A")
    _, _, account_b = await _make_tenant(db_session, org="Cemetery B")
    await _seed_tenant_notification(db_session, account_a, title="Belongs to A")
    await _seed_tenant_notification(db_session, account_b, title="Belongs to B")

    resp = await client.get(TENANT_URL, headers=headers_a)
    assert resp.status_code == 200, resp.text
    titles = {item["title"] for item in resp.json()["data"]}
    assert titles == {"Belongs to A"}


# ── Unread count ──────────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_unread_count_counts_only_own_scope(
    client: AsyncClient, db_session: AsyncSession
):
    headers, _, account = await _make_tenant(db_session)
    _, _, other = await _make_tenant(db_session, org="Other Cemetery")
    await _seed_tenant_notification(db_session, account)
    await _seed_tenant_notification(db_session, account)
    await _seed_tenant_notification(db_session, other)
    await _seed_platform_notification(db_session)

    resp = await client.get(f"{TENANT_URL}/unread-count", headers=headers)
    assert resp.status_code == 200, resp.text
    assert resp.json()["data"]["count"] == 2


@pytest.mark.asyncio
async def test_unread_count_excludes_read(client: AsyncClient, db_session: AsyncSession):
    headers, _, account = await _make_tenant(db_session)
    first = await _seed_tenant_notification(db_session, account)
    await _seed_tenant_notification(db_session, account)

    resp = await client.patch(f"{TENANT_URL}/{first.id}/read", headers=headers)
    assert resp.status_code == 200, resp.text
    assert resp.json()["data"]["read_at"] is not None

    resp = await client.get(f"{TENANT_URL}/unread-count", headers=headers)
    assert resp.json()["data"]["count"] == 1


@pytest.mark.asyncio
async def test_unread_only_filter(client: AsyncClient, db_session: AsyncSession):
    headers, _, account = await _make_tenant(db_session)
    read_one = await _seed_tenant_notification(db_session, account, title="Read one")
    await _seed_tenant_notification(db_session, account, title="Unread one")
    await client.patch(f"{TENANT_URL}/{read_one.id}/read", headers=headers)

    resp = await client.get(TENANT_URL, params={"unread_only": True}, headers=headers)
    assert resp.status_code == 200, resp.text
    titles = {item["title"] for item in resp.json()["data"]}
    assert titles == {"Unread one"}


# ── Mark read isolation ───────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_tenant_cannot_mark_another_tenants_notification_read(
    client: AsyncClient, db_session: AsyncSession
):
    headers_a, _, _ = await _make_tenant(db_session, org="Cemetery A")
    _, _, account_b = await _make_tenant(db_session, org="Cemetery B")
    theirs = await _seed_tenant_notification(db_session, account_b)

    resp = await client.patch(f"{TENANT_URL}/{theirs.id}/read", headers=headers_a)
    assert resp.status_code == 404, resp.text

    await db_session.refresh(theirs)
    assert theirs.read_at is None


@pytest.mark.asyncio
async def test_tenant_cannot_mark_platform_notification_read(
    client: AsyncClient, db_session: AsyncSession
):
    headers, _, _ = await _make_tenant(db_session)
    platform = await _seed_platform_notification(db_session)

    resp = await client.patch(f"{TENANT_URL}/{platform.id}/read", headers=headers)
    assert resp.status_code == 404, resp.text

    await db_session.refresh(platform)
    assert platform.read_at is None


@pytest.mark.asyncio
async def test_site_admin_cannot_mark_tenant_notification_read(
    client: AsyncClient, db_session: AsyncSession
):
    sa_headers = await _site_admin_headers(db_session)
    _, _, account = await _make_tenant(db_session)
    tenant_row = await _seed_tenant_notification(db_session, account)

    resp = await client.patch(f"{ADMIN_URL}/{tenant_row.id}/read", headers=sa_headers)
    assert resp.status_code == 404, resp.text

    await db_session.refresh(tenant_row)
    assert tenant_row.read_at is None


@pytest.mark.asyncio
async def test_mark_all_read_does_not_touch_other_scopes(
    client: AsyncClient, db_session: AsyncSession
):
    headers, _, account = await _make_tenant(db_session)
    _, _, other = await _make_tenant(db_session, org="Other Cemetery")
    await _seed_tenant_notification(db_session, account)
    await _seed_tenant_notification(db_session, account)
    other_row = await _seed_tenant_notification(db_session, other)
    platform_row = await _seed_platform_notification(db_session)

    resp = await client.post(f"{TENANT_URL}/mark-all-read", headers=headers)
    assert resp.status_code == 200, resp.text
    assert resp.json()["data"]["updated"] == 2

    await db_session.refresh(other_row)
    await db_session.refresh(platform_row)
    assert other_row.read_at is None
    assert platform_row.read_at is None


# ── Service-level guards ──────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_tenant_scope_requires_tenant_id(db_session: AsyncSession):
    """A scope/tenant_id mismatch is a programming error, not a silent write."""
    service = NotificationService(db_session)
    with pytest.raises(ValueError):
        await service.create(
            scope=NotificationScope.TENANT,
            tenant_id=None,
            type=NotificationType.ENQUIRY_CONTACT_FORM,
            title="No tenant",
        )


@pytest.mark.asyncio
async def test_platform_scope_rejects_tenant_id(db_session: AsyncSession):
    account = await _create_account(db_session)
    service = NotificationService(db_session)
    with pytest.raises(ValueError):
        await service.create(
            scope=NotificationScope.PLATFORM,
            tenant_id=account.id,
            type=NotificationType.ENQUIRY_MARKETING,
            title="Tenant on a platform row",
        )


@pytest.mark.asyncio
async def test_ws_payload_shape(db_session: AsyncSession):
    """The frame must match the admin client's AdminWSEvent 'notification'
    variant exactly — a renamed key silently stops updating the bell."""
    account = await _create_account(db_session)
    notification = await _seed_tenant_notification(db_session, account, title="Ping")
    payload = NotificationService.build_ws_payload(notification)

    assert set(payload) == {
        "type",
        "id",
        "notificationType",
        "title",
        "body",
        "link",
        "createdAt",
    }
    assert payload["type"] == "notification"
    assert payload["id"] == str(notification.id)
    assert payload["notificationType"] == notification.type
