"""Integration tests for INDL-44 — Memorial Tribute Moderation.

Covers the enhanced endpoints:
  GET   /api/v1/memorials/tributes            (deceased_name join, pending default)
  PATCH /api/v1/memorials/tributes/{id}/status (approve / reject / edit & approve)
  GET   /api/v1/memorials?is_published=...    (published-count stat card filter)

Functional ACs (AC-01, AC-03, AC-04, AC-06, AC-10, AC-11) and the security
acceptance criteria (SEC-01..SEC-10) are both exercised here.
"""
import datetime as dt

import pytest
import pytest_asyncio
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.memorials.models.memorial import Memorial
from src.apps.memorials.models.tribute import Tribute
from src.apps.records.models.record import Record
from src.apps.site_admin.models.audit_log import AuditLog
from src.apps.tenants.models.account import Account
from src.core.security import build_token_payload, create_access_token, hash_password


# ── helpers ──────────────────────────────────────────────────────────────────
async def _make_account(db, *, subdomain, name="Test Cemetery"):
    account = Account(
        organization_name=name,
        subdomain=subdomain,
        contact_email=f"admin@{subdomain}.com",
        plan="starter",
        status="active",
    )
    db.add(account)
    await db.flush()
    return account


async def _make_user(db, account, *, role, email=None):
    user = User(
        tenant_id=account.id,
        email=email or f"{role}@{account.subdomain}.com",
        password_hash=hash_password("TestPassword123"),
        first_name=role.capitalize(),
        last_name="User",
        role=role,
        status="active",
    )
    db.add(user)
    await db.flush()
    return user


def _headers(user, account):
    token = create_access_token(build_token_payload(user, account))
    return {"Authorization": f"Bearer {token}"}


async def _make_memorial(
    db, account, *, slug, first="Patricia", last="O'Brien",
    display_name=None, published=True,
):
    rec = Record(
        tenant_id=account.id,
        first_name=first,
        last_name=last,
        date_of_birth=dt.date(1942, 9, 14),
        date_of_death=dt.date(2024, 3, 8),
    )
    db.add(rec)
    await db.flush()
    mem = Memorial(
        tenant_id=account.id,
        record_id=rec.id,
        slug=slug,
        display_name=display_name,
        is_published=published,
    )
    db.add(mem)
    await db.flush()
    return mem, rec


async def _make_tribute(db, account, memorial, *, status="pending",
                        name="Eileen Hart", email="eileen@example.com",
                        message="She was a wonderful mother and grandmother."):
    trib = Tribute(
        tenant_id=account.id,
        memorial_id=memorial.id,
        submitter_name=name,
        submitter_email=email,
        message=message,
        status=status,
    )
    db.add(trib)
    await db.flush()
    return trib


@pytest_asyncio.fixture
async def account(db_session: AsyncSession):
    return await _make_account(db_session, subdomain="tenanta")


@pytest_asyncio.fixture
async def staff_headers(db_session, account):
    user = await _make_user(db_session, account, role="staff")
    return _headers(user, account)


# ── AC-10 / AC-01: list + deceased_name + published count ────────────────────
@pytest.mark.asyncio
async def test_list_tributes_deceased_name_from_display_name(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="patricia", display_name="Patricia O'Brien-Hart")
    await _make_tribute(db_session, account, mem)

    resp = await client.get("/api/v1/memorials/tributes", headers=staff_headers)
    assert resp.status_code == 200
    body = resp.json()
    assert body["total"] == 1
    assert body["data"][0]["deceased_name"] == "Patricia O'Brien-Hart"


@pytest.mark.asyncio
async def test_list_tributes_deceased_name_falls_back_to_record(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="david", first="David", last="Tran", display_name=None)
    await _make_tribute(db_session, account, mem)

    resp = await client.get("/api/v1/memorials/tributes", headers=staff_headers)
    assert resp.status_code == 200
    assert resp.json()["data"][0]["deceased_name"] == "David Tran"


@pytest.mark.asyncio
async def test_list_tributes_defaults_to_pending_only(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="mixed")
    await _make_tribute(db_session, account, mem, status="pending", name="Pending One")
    await _make_tribute(db_session, account, mem, status="approved", name="Approved One")
    await _make_tribute(db_session, account, mem, status="rejected", name="Rejected One")

    resp = await client.get("/api/v1/memorials/tributes", headers=staff_headers)
    assert resp.status_code == 200
    body = resp.json()
    assert body["total"] == 1
    assert body["data"][0]["status"] == "pending"


@pytest.mark.asyncio
async def test_published_count_filter(client: AsyncClient, db_session, account, staff_headers):
    await _make_memorial(db_session, account, slug="pub1", published=True)
    await _make_memorial(db_session, account, slug="pub2", published=True)
    await _make_memorial(db_session, account, slug="draft1", published=False)

    resp = await client.get("/api/v1/memorials?is_published=true&limit=1", headers=staff_headers)
    assert resp.status_code == 200
    assert resp.json()["total"] == 2

    resp2 = await client.get("/api/v1/memorials?is_published=false&limit=1", headers=staff_headers)
    assert resp2.json()["total"] == 1


# ── AC-03 / AC-04 / AC-06 / AC-11: moderation actions ────────────────────────
@pytest.mark.asyncio
async def test_approve_tribute(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="approve")
    trib = await _make_tribute(db_session, account, mem)

    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=staff_headers, json={"status": "approved"},
    )
    assert resp.status_code == 200
    assert resp.json()["data"]["status"] == "approved"

    await db_session.refresh(trib)
    assert trib.status == "approved"
    assert trib.moderated_at is not None


@pytest.mark.asyncio
async def test_reject_tribute(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="reject")
    trib = await _make_tribute(db_session, account, mem)

    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=staff_headers, json={"status": "rejected"},
    )
    assert resp.status_code == 200
    assert resp.json()["data"]["status"] == "rejected"


@pytest.mark.asyncio
async def test_edit_and_approve_updates_message(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="edit")
    trib = await _make_tribute(db_session, account, mem, message="Original message.")

    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=staff_headers,
        json={"status": "approved", "message": "Edited by staff. She was cherished."},
    )
    assert resp.status_code == 200
    data = resp.json()["data"]
    assert data["status"] == "approved"
    assert data["message"] == "Edited by staff. She was cherished."

    await db_session.refresh(trib)
    assert trib.message == "Edited by staff. She was cherished."


@pytest.mark.asyncio
async def test_edit_and_approve_writes_audit_log(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="audit")
    trib = await _make_tribute(db_session, account, mem, message="Original.")

    await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=staff_headers,
        json={"status": "approved", "message": "New edited content here."},
    )

    rows = (await db_session.execute(
        select(AuditLog).where(AuditLog.entity_type == "tribute", AuditLog.entity_id == trib.id)
    )).scalars().all()
    assert len(rows) == 1
    log = rows[0]
    assert log.action == "status_approved_edited"
    assert log.old_value["status"] == "pending"
    assert log.new_value["status"] == "approved"
    assert "message_length" in log.new_value  # length recorded, not full PII text


# ── SEC-01: cross-tenant IDOR returns 404 ────────────────────────────────────
@pytest.mark.asyncio
async def test_cross_tenant_patch_returns_404(client: AsyncClient, db_session, account, staff_headers):
    # Tribute belongs to a *different* tenant.
    other = await _make_account(db_session, subdomain="tenantb")
    mem, _ = await _make_memorial(db_session, other, slug="others")
    victim = await _make_tribute(db_session, other, mem)

    resp = await client.patch(
        f"/api/v1/memorials/tributes/{victim.id}/status",
        headers=staff_headers, json={"status": "rejected"},
    )
    assert resp.status_code == 404
    # And the victim tribute is untouched.
    await db_session.refresh(victim)
    assert victim.status == "pending"


# ── SEC-02: message sanitisation / validation ────────────────────────────────
@pytest.mark.asyncio
async def test_edit_message_html_stripped(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="xss")
    trib = await _make_tribute(db_session, account, mem)

    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=staff_headers,
        json={"status": "approved", "message": "Nice tribute.<script>alert(1)</script>"},
    )
    assert resp.status_code == 200
    stored = resp.json()["data"]["message"]
    assert "<script" not in stored.lower()
    assert stored == "Nice tribute."


@pytest.mark.asyncio
async def test_blank_edit_message_rejected(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="blank")
    trib = await _make_tribute(db_session, account, mem)
    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=staff_headers, json={"status": "approved", "message": "   "},
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_oversized_edit_message_rejected(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="big")
    trib = await _make_tribute(db_session, account, mem)
    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=staff_headers, json={"status": "approved", "message": "a" * 5001},
    )
    assert resp.status_code == 422


# ── SEC-03: status enum ──────────────────────────────────────────────────────
@pytest.mark.parametrize("bad_status", ["pending", "deleted", "published", 5])
@pytest.mark.asyncio
async def test_invalid_status_rejected(client: AsyncClient, db_session, account, staff_headers, bad_status):
    mem, _ = await _make_memorial(db_session, account, slug=f"st{bad_status}")
    trib = await _make_tribute(db_session, account, mem)
    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=staff_headers, json={"status": bad_status},
    )
    assert resp.status_code == 422


# ── SEC-04: mass-assignment blocked (extra=forbid) ───────────────────────────
@pytest.mark.asyncio
async def test_extra_fields_rejected(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="extra")
    trib = await _make_tribute(db_session, account, mem)
    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=staff_headers,
        json={"status": "approved", "tenant_id": "00000000-0000-0000-0000-000000000001"},
    )
    assert resp.status_code == 422


# ── SEC-06: authentication & role matrix ─────────────────────────────────────
@pytest.mark.asyncio
async def test_patch_requires_auth(client: AsyncClient, db_session, account):
    mem, _ = await _make_memorial(db_session, account, slug="noauth")
    trib = await _make_tribute(db_session, account, mem)
    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status", json={"status": "approved"}
    )
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_view_only_forbidden_on_patch(client: AsyncClient, db_session, account):
    viewer = await _make_user(db_session, account, role="view_only")
    mem, _ = await _make_memorial(db_session, account, slug="viewonly")
    trib = await _make_tribute(db_session, account, mem)
    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=_headers(viewer, account), json={"status": "approved"},
    )
    assert resp.status_code == 403


@pytest.mark.parametrize("role", ["staff", "manager", "administrator"])
@pytest.mark.asyncio
async def test_staff_plus_can_moderate(client: AsyncClient, db_session, account, role):
    user = await _make_user(db_session, account, role=role, email=f"{role}-mod@a.com")
    mem, _ = await _make_memorial(db_session, account, slug=f"role-{role}")
    trib = await _make_tribute(db_session, account, mem)
    resp = await client.patch(
        f"/api/v1/memorials/tributes/{trib.id}/status",
        headers=_headers(user, account), json={"status": "approved"},
    )
    assert resp.status_code == 200


# ── SEC-07 / SEC-10: list endpoints require auth ─────────────────────────────
@pytest.mark.asyncio
async def test_list_tributes_requires_auth(client: AsyncClient):
    resp = await client.get("/api/v1/memorials/tributes")
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_list_memorials_requires_auth(client: AsyncClient):
    resp = await client.get("/api/v1/memorials?is_published=true")
    assert resp.status_code == 401


# ── SEC-08: limit cap ────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_list_tributes_limit_capped(client: AsyncClient, staff_headers):
    resp = await client.get("/api/v1/memorials/tributes?limit=5000", headers=staff_headers)
    assert resp.status_code == 422  # Query(le=100) rejects oversized page size


# ── SEC-09: deceased_name sanitised ──────────────────────────────────────────
@pytest.mark.asyncio
async def test_deceased_name_sanitised(client: AsyncClient, db_session, account, staff_headers):
    mem, _ = await _make_memorial(db_session, account, slug="dnsan", display_name="<b>Injected</b> Name")
    await _make_tribute(db_session, account, mem)
    resp = await client.get("/api/v1/memorials/tributes", headers=staff_headers)
    name = resp.json()["data"][0]["deceased_name"]
    assert "<" not in name and ">" not in name
    assert name == "Injected Name"
