"""
Integration tests for INDL-46 public memorial page endpoints:
  GET  /api/public/memorials/{slug}
  POST /api/public/memorials/{slug}/candle
  GET  /api/public/memorials/{slug}/tributes
  POST /api/public/memorials/{slug}/tributes   (multipart)

Covers the full-payload assembly, quick facts, timeline, photos, age
computation, candle increment + per-IP rate limit, approved-only tribute
listing + pagination, multipart tribute submission (status=pending), field
validation, publish gate, cross-tenant isolation, and data minimisation.
"""
import datetime as dt

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

TENANT_HDR = "X-Tenant-ID"


@pytest_asyncio.fixture(autouse=True)
async def _flush_rate_limit_keys():
    """Clear per-IP rate-limit keys before each test so candle / tribute tests
    are deterministic across reruns. Best-effort — no-op if Redis is down."""
    try:
        import redis.asyncio as aioredis
        from src.core.config import settings

        r = aioredis.from_url(settings.REDIS_URL, decode_responses=True)
        async for key in r.scan_iter(match="rate:candle:*"):
            await r.delete(key)
        async for key in r.scan_iter(match="rate:tribute-submit:*"):
            await r.delete(key)
        await r.aclose()
    except Exception:
        pass
    yield


# ── fixtures ────────────────────────────────────────────────────────────────
async def _make_section(db, tenant_id, *, code="B", name="Section B"):
    from src.apps.sections.models.section import Section

    section = Section(tenant_id=tenant_id, code=code, name=name)
    db.add(section)
    await db.flush()
    return section


async def _make_plot(db, tenant_id, *, plot_ref, lat=None, lng=None, section=None):
    from src.apps.plots.models.plot import Plot
    from src.apps.plots.models.plot_type import PlotType

    pt = PlotType(tenant_id=tenant_id, name="Single Plot", default_price=4500)
    db.add(pt)
    await db.flush()
    plot = Plot(
        tenant_id=tenant_id,
        plot_ref=plot_ref,
        plot_type_id=pt.id,
        status="occupied",
        latitude=lat,
        longitude=lng,
        section_id=section.id if section else None,
    )
    db.add(plot)
    await db.flush()
    return plot


async def _make_full_memorial(
    db,
    tenant_id,
    *,
    slug,
    plot=None,
    published=True,
    first="Patricia",
    last="O'Brien",
    maiden="Donnelly",
    dob=dt.date(1942, 9, 14),
    dod=dt.date(2024, 3, 8),
    bio="A life of quiet generosity.\n\nShe taught for 38 years.",
    ai=True,
):
    from src.apps.memorials.models.memorial import Memorial
    from src.apps.records.models.record import Record

    rec = Record(
        tenant_id=tenant_id,
        plot_id=plot.id if plot else None,
        first_name=first,
        last_name=last,
        maiden_name=maiden,
        date_of_birth=dob,
        date_of_death=dod,
        birthplace="Halifax, Nova Scotia",
        city_of_residence="Kingston, Ontario",
        occupation="Schoolteacher",
    )
    db.add(rec)
    await db.flush()
    mem = Memorial(
        tenant_id=tenant_id,
        record_id=rec.id,
        slug=slug,
        is_published=published,
        biography_text=bio,
        biography_ai_generated=ai,
        candle_count=347,
        service_date=dt.date(2024, 3, 14),
        service_location="St. Mary's Church",
    )
    db.add(mem)
    await db.flush()
    return mem, rec


async def _add_timeline(db, tenant_id, memorial_id):
    from src.apps.memorials.models.timeline_event import MemorialTimelineEvent

    db.add(MemorialTimelineEvent(
        tenant_id=tenant_id, memorial_id=memorial_id,
        event_date=dt.datetime(1942, 1, 1), title="Born in Halifax",
        description="The eldest of four.", sort_order=1,
    ))
    db.add(MemorialTimelineEvent(
        tenant_id=tenant_id, memorial_id=memorial_id,
        event_date=dt.datetime(1964, 1, 1), title="Moves to Kingston",
        description="Began teaching.", sort_order=2,
    ))
    await db.flush()


async def _add_photo(db, tenant_id, memorial_id, *, key="memorials/p1.jpg", caption="Harbour, 1948"):
    from src.apps.memorials.models.memorial_photo import MemorialPhoto

    db.add(MemorialPhoto(
        tenant_id=tenant_id, memorial_id=memorial_id, s3_key=key, caption=caption, sort_order=1
    ))
    await db.flush()


async def _add_tribute(db, tenant_id, memorial_id, *, name, status="approved", rel="Friend", msg="A kind soul."):
    from src.apps.memorials.models.tribute import Tribute

    t = Tribute(
        tenant_id=tenant_id, memorial_id=memorial_id, submitter_name=name,
        submitter_email="x@example.com", relationship_type=rel, message=msg,
        status=status, submitted_at=dt.datetime.now(dt.timezone.utc),
    )
    db.add(t)
    await db.flush()
    return t


@pytest_asyncio.fixture
async def memorial(db_session: AsyncSession, test_account):
    section = await _make_section(db_session, test_account.id)
    plot = await _make_plot(
        db_session, test_account.id, plot_ref="B-204",
        lat=45.4221458, lng=-75.6966031, section=section,
    )
    mem, _ = await _make_full_memorial(db_session, test_account.id, slug="patricia-obrien", plot=plot)
    await _add_timeline(db_session, test_account.id, mem.id)
    await _add_photo(db_session, test_account.id, mem.id)
    return mem


# ── GET full payload (AC-02/09/10/11/12/22/24) ──────────────────────────────
@pytest.mark.asyncio
async def test_get_memorial_full_payload(client: AsyncClient, test_account, memorial):
    r = await client.get(
        "/api/public/memorials/patricia-obrien",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 200
    d = r.json()["data"]
    assert d["display_name"] == "Patricia O'Brien"
    assert d["maiden_name"] == "Donnelly"
    assert d["date_of_birth"] == "1942-09-14"
    assert d["age_at_death"] == 81  # AC-02 computed age
    assert d["candle_count"] == 347  # AC-09 from DB
    assert d["biography_ai_generated"] is True  # AC-12
    assert d["plot_ref"] == "B-204"
    assert d["section_code"] == "B"
    # Quick facts (AC-10)
    assert d["quick_facts"]["birthplace"] == "Halifax, Nova Scotia"
    assert d["quick_facts"]["occupation"] == "Schoolteacher"
    assert d["quick_facts"]["service_location"] == "St. Mary's Church"
    # Timeline (AC-22) chronological
    assert [t["year"] for t in d["timeline"]] == [1942, 1964]
    assert d["timeline"][0]["heading"] == "Born in Halifax"
    # Photos (AC-24)
    assert d["photo_count"] == 1
    assert d["photos"][0]["caption"] == "Harbour, 1948"
    assert d["photos"][0]["url"].startswith("/api/public/images/")
    # Plot coordinates (AC-14)
    assert d["plot_coordinates"]["has_gps"] is True
    # Cemetery profile (AC-41)
    assert d["cemetery"]["name"] == "Test Cemetery"


@pytest.mark.asyncio
async def test_get_memorial_age_computation(client: AsyncClient, db_session, test_account):
    # died the day before 50th birthday → age 49
    await _make_full_memorial(
        db_session, test_account.id, slug="age-check", plot=None,
        dob=dt.date(1950, 6, 15), dod=dt.date(2000, 6, 14),
    )
    r = await client.get("/api/public/memorials/age-check", headers={TENANT_HDR: str(test_account.id)})
    assert r.json()["data"]["age_at_death"] == 49


@pytest.mark.asyncio
async def test_get_memorial_no_plot_has_null_coordinates(client: AsyncClient, db_session, test_account):
    await _make_full_memorial(db_session, test_account.id, slug="no-plot", plot=None)
    r = await client.get("/api/public/memorials/no-plot", headers={TENANT_HDR: str(test_account.id)})
    assert r.status_code == 200
    assert r.json()["data"]["plot_coordinates"] is None  # AC-17 fallback


# ── Publish gate + isolation (AC-42) ────────────────────────────────────────
@pytest.mark.asyncio
async def test_get_memorial_unpublished_404(client: AsyncClient, db_session, test_account):
    await _make_full_memorial(db_session, test_account.id, slug="hidden", plot=None, published=False)
    r = await client.get("/api/public/memorials/hidden", headers={TENANT_HDR: str(test_account.id)})
    assert r.status_code == 404


@pytest.mark.asyncio
async def test_get_memorial_unknown_404(client: AsyncClient, test_account):
    r = await client.get("/api/public/memorials/does-not-exist", headers={TENANT_HDR: str(test_account.id)})
    assert r.status_code == 404


@pytest.mark.asyncio
async def test_get_memorial_cross_tenant_404(client: AsyncClient, db_session, test_account, memorial):
    from src.apps.tenants.models.account import Account

    other = Account(
        organization_name="Rival", subdomain="rival",
        contact_email="a@rival.com", plan="starter", status="active",
    )
    db_session.add(other)
    await db_session.flush()
    r = await client.get("/api/public/memorials/patricia-obrien", headers={TENANT_HDR: str(other.id)})
    assert r.status_code == 404


@pytest.mark.asyncio
async def test_get_memorial_no_internal_fields(client: AsyncClient, test_account, memorial):
    r = await client.get("/api/public/memorials/patricia-obrien", headers={TENANT_HDR: str(test_account.id)})
    d = r.json()["data"]
    assert "tenant_id" not in d
    assert "record_id" not in d
    assert "visibility_config" not in d


# ── Candle (AC-05/08/09) ────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_candle_increments_and_persists(client: AsyncClient, test_account, memorial):
    r = await client.post(
        "/api/public/memorials/patricia-obrien/candle",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 200
    assert r.json()["data"]["candle_count"] == 348
    # Persisted — reflected on GET
    g = await client.get("/api/public/memorials/patricia-obrien", headers={TENANT_HDR: str(test_account.id)})
    assert g.json()["data"]["candle_count"] == 348


@pytest.mark.asyncio
async def test_candle_rate_limited_per_ip(client: AsyncClient, test_account, memorial):
    h = {TENANT_HDR: str(test_account.id)}
    first = await client.post("/api/public/memorials/patricia-obrien/candle", headers=h)
    second = await client.post("/api/public/memorials/patricia-obrien/candle", headers=h)
    assert first.status_code == 200
    # 1 per IP per hour — second call blocked (requires Redis; skip assert if down)
    if second.status_code != 200:
        assert second.status_code == 429


@pytest.mark.asyncio
async def test_candle_unpublished_404(client: AsyncClient, db_session, test_account):
    await _make_full_memorial(db_session, test_account.id, slug="dark", plot=None, published=False)
    r = await client.post("/api/public/memorials/dark/candle", headers={TENANT_HDR: str(test_account.id)})
    assert r.status_code == 404


@pytest.mark.asyncio
async def test_candle_and_tributes_soft_deleted_record_404(client: AsyncClient, db_session, test_account):
    # Published memorial whose linked record is soft-deleted must 404 on every
    # public endpoint, consistent with the memorial page + directions gate.
    _, rec = await _make_full_memorial(db_session, test_account.id, slug="gone", plot=None)
    rec.deleted_at = dt.datetime.now(dt.timezone.utc)
    await db_session.flush()
    h = {TENANT_HDR: str(test_account.id)}
    assert (await client.get("/api/public/memorials/gone", headers=h)).status_code == 404
    assert (await client.post("/api/public/memorials/gone/candle", headers=h)).status_code == 404
    assert (await client.get("/api/public/memorials/gone/tributes", headers=h)).status_code == 404


# ── Approved tributes list (AC-27/30) ───────────────────────────────────────
@pytest.mark.asyncio
async def test_tributes_list_approved_only_paginated(client: AsyncClient, db_session, test_account, memorial):
    for i in range(8):
        await _add_tribute(db_session, test_account.id, memorial.id, name=f"Approved {i}", status="approved")
    await _add_tribute(db_session, test_account.id, memorial.id, name="Pending Pat", status="pending")

    r = await client.get(
        "/api/public/memorials/patricia-obrien/tributes?page=1&page_size=6",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 200
    body = r.json()
    assert body["total"] == 8  # pending excluded
    assert len(body["data"]) == 6
    # Page 2
    r2 = await client.get(
        "/api/public/memorials/patricia-obrien/tributes?page=2&page_size=6",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert len(r2.json()["data"]) == 2


@pytest.mark.asyncio
async def test_tributes_list_hides_email(client: AsyncClient, db_session, test_account, memorial):
    await _add_tribute(db_session, test_account.id, memorial.id, name="Sarah K", status="approved")
    r = await client.get(
        "/api/public/memorials/patricia-obrien/tributes",
        headers={TENANT_HDR: str(test_account.id)},
    )
    item = r.json()["data"][0]
    assert "submitter_email" not in item  # not shown publicly
    assert item["submitter_name"] == "Sarah K"


# ── Tribute submission (AC-31..39) ──────────────────────────────────────────
@pytest.mark.asyncio
async def test_submit_tribute_creates_pending(client: AsyncClient, db_session, test_account, memorial):
    r = await client.post(
        "/api/public/memorials/patricia-obrien/tributes",
        headers={TENANT_HDR: str(test_account.id)},
        data={
            "submitter_name": "Father James",
            "submitter_email": "james@example.com",
            "relationship": "Parish priest",
            "message": "Patricia was in the third pew every Sunday.",
        },
    )
    assert r.status_code == 201
    assert "tribute_id" in r.json()["data"]

    # It must NOT appear in the public approved list (still pending → INDL-44 queue)
    lst = await client.get(
        "/api/public/memorials/patricia-obrien/tributes",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert lst.json()["total"] == 0


@pytest.mark.asyncio
async def test_submit_tribute_requires_fields(client: AsyncClient, test_account, memorial):
    r = await client.post(
        "/api/public/memorials/patricia-obrien/tributes",
        headers={TENANT_HDR: str(test_account.id)},
        data={"submitter_name": "A", "submitter_email": "bad-email", "message": ""},
    )
    assert r.status_code == 422


@pytest.mark.asyncio
async def test_submit_tribute_message_too_long(client: AsyncClient, test_account, memorial):
    r = await client.post(
        "/api/public/memorials/patricia-obrien/tributes",
        headers={TENANT_HDR: str(test_account.id)},
        data={
            "submitter_name": "Valid Name",
            "submitter_email": "v@example.com",
            "message": "x" * 501,
        },
    )
    assert r.status_code == 422


@pytest.mark.asyncio
async def test_submit_tribute_unpublished_404(client: AsyncClient, db_session, test_account):
    await _make_full_memorial(db_session, test_account.id, slug="closed", plot=None, published=False)
    r = await client.post(
        "/api/public/memorials/closed/tributes",
        headers={TENANT_HDR: str(test_account.id)},
        data={"submitter_name": "Valid Name", "submitter_email": "v@example.com", "message": "hi there"},
    )
    assert r.status_code == 404
