"""
Integration tests for INDL-43 public directions endpoints:
  GET /api/public/memorial/{slug}/directions
  GET /api/public/memorial/{slug}/directions.pdf

Covers happy path, no-GPS fallback, publish gate, cross-tenant isolation,
soft-deleted record, data minimisation, slug validation, method-not-allowed,
PDF headers/filename sanitisation, and the entrance-coords walking-time path.
"""
import pytest
import pytest_asyncio
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

TENANT_HDR = "X-Tenant-ID"


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_memorial(db, tenant_id, *, slug, plot, published=True, first="Jane", last="Doe"):
    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)
    db.add(rec)
    await db.flush()
    mem = Memorial(tenant_id=tenant_id, record_id=rec.id, slug=slug, is_published=published)
    db.add(mem)
    await db.flush()
    return mem, rec


@pytest_asyncio.fixture
async def gps_section(db_session: AsyncSession, test_account):
    from src.apps.sections.models.section import Section

    section = Section(tenant_id=test_account.id, code="B", name="Section B")
    db_session.add(section)
    await db_session.flush()
    return section


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


# ── AC-01/02/03: happy path ─────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_directions_gps_happy_path(client: AsyncClient, test_account, gps_memorial):
    r = await client.get(
        "/api/public/memorial/patricia-obrien/directions",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 200
    data = r.json()["data"]
    assert data["section_code"] == "B"
    assert data["section_name"] == "Section B"
    assert data["plot_ref"] == "B-204"
    assert data["has_gps"] is True
    assert data["latitude"] == pytest.approx(45.4221458)
    assert data["longitude"] == pytest.approx(-75.6966031)
    # No entrance coords configured → walking time/distance hidden (AC-03).
    assert data["walking_minutes"] is None
    assert data["distance_m"] is None


# ── AC-03/SAC-10: entrance coords → walking time/distance appear ────────────
@pytest.mark.asyncio
async def test_directions_with_entrance_coords(client: AsyncClient, db_session, test_account, gps_memorial):
    test_account.config_json = {"entrance_latitude": 45.4218, "entrance_longitude": -75.6975}
    await db_session.flush()
    r = await client.get(
        "/api/public/memorial/patricia-obrien/directions",
        headers={TENANT_HDR: str(test_account.id)},
    )
    data = r.json()["data"]
    assert data["walking_minutes"] is not None and data["walking_minutes"] >= 1
    assert data["distance_m"] is not None and data["distance_m"] > 0


# ── AC-08: no-GPS fallback ──────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_directions_no_gps(client: AsyncClient, db_session, test_account, gps_section):
    plot = await _make_plot(db_session, test_account.id, plot_ref="B-11", section=gps_section)
    await _make_memorial(db_session, test_account.id, slug="james-nolan", plot=plot)
    r = await client.get(
        "/api/public/memorial/james-nolan/directions",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 200
    data = r.json()["data"]
    assert data["has_gps"] is False
    assert data["latitude"] is None and data["longitude"] is None
    assert data["walking_minutes"] is None


# ── T-05: memorial with no linked plot → 404 (card absent) ──────────────────
@pytest.mark.asyncio
async def test_directions_no_plot_returns_404(client: AsyncClient, db_session, test_account):
    await _make_memorial(db_session, test_account.id, slug="no-plot-here", plot=None)
    r = await client.get(
        "/api/public/memorial/no-plot-here/directions",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 404


# ── AC-11/SEC-02: publish gate ──────────────────────────────────────────────
@pytest.mark.asyncio
async def test_directions_unpublished_returns_404(client: AsyncClient, db_session, test_account, gps_section):
    plot = await _make_plot(db_session, test_account.id, plot_ref="B-99", lat=45.42, lng=-75.69, section=gps_section)
    await _make_memorial(db_session, test_account.id, slug="hidden-grace", plot=plot, published=False)
    r = await client.get(
        "/api/public/memorial/hidden-grace/directions",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 404


# ── SAC-02/API2: soft-deleted record → 404 ──────────────────────────────────
@pytest.mark.asyncio
async def test_directions_soft_deleted_record_returns_404(client: AsyncClient, db_session, test_account, gps_section):
    import datetime as dt

    plot = await _make_plot(db_session, test_account.id, plot_ref="B-77", lat=45.42, lng=-75.69, section=gps_section)
    _, rec = await _make_memorial(db_session, test_account.id, slug="deleted-dan", plot=plot)
    rec.deleted_at = dt.datetime.now(dt.timezone.utc)
    await db_session.flush()
    r = await client.get(
        "/api/public/memorial/deleted-dan/directions",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 404


# ── SAC-01/API1: cross-tenant isolation ─────────────────────────────────────
@pytest.mark.asyncio
async def test_directions_cross_tenant_returns_404(client: AsyncClient, db_session, test_account, gps_memorial):
    from src.apps.tenants.models.account import Account

    other = Account(
        organization_name="Rival Cemetery",
        subdomain="rivalcemetery",
        contact_email="admin@rival.com",
        plan="starter",
        status="active",
    )
    db_session.add(other)
    await db_session.flush()
    # Same slug requested on the other tenant's context → must not leak.
    r = await client.get(
        "/api/public/memorial/patricia-obrien/directions",
        headers={TENANT_HDR: str(other.id)},
    )
    assert r.status_code == 404


# ── SAC-03/API3: data minimisation ──────────────────────────────────────────
@pytest.mark.asyncio
async def test_directions_response_has_no_internal_fields(client: AsyncClient, test_account, gps_memorial):
    r = await client.get(
        "/api/public/memorial/patricia-obrien/directions",
        headers={TENANT_HDR: str(test_account.id)},
    )
    data = r.json()["data"]
    forbidden = {"internal_notes", "price", "reserved_by", "reserved_by_name", "cause_of_death", "contract_id", "tenant_id", "deleted_at", "id"}
    assert forbidden.isdisjoint(data.keys())


# ── SEC-05/SAC-07: slug validation ──────────────────────────────────────────
@pytest.mark.asyncio
@pytest.mark.parametrize("bad_slug", ["BAD_slug", "has space", "..%2fadmin", "a", "UPPER", "sql'inject"])
async def test_directions_invalid_slug_rejected(client: AsyncClient, test_account, bad_slug):
    r = await client.get(
        f"/api/public/memorial/{bad_slug}/directions",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code in (404, 422)  # 422 from route validation; 404 if path doesn't match


# ── SAC-12: method not allowed ──────────────────────────────────────────────
@pytest.mark.asyncio
async def test_directions_post_not_allowed(client: AsyncClient, test_account, gps_memorial):
    r = await client.post(
        "/api/public/memorial/patricia-obrien/directions",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 405


# ── AC-06/07/SAC-05/SAC-08: PDF endpoint ────────────────────────────────────
@pytest.mark.asyncio
async def test_directions_pdf_happy_path(client: AsyncClient, test_account, gps_memorial):
    r = await client.get(
        "/api/public/memorial/patricia-obrien/directions.pdf",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 200
    assert r.headers["content-type"] == "application/pdf"
    assert 'attachment; filename="directions-B-204.pdf"' in r.headers["content-disposition"]
    assert r.headers.get("x-content-type-options") == "nosniff"
    assert r.content[:5] == b"%PDF-"


@pytest.mark.asyncio
async def test_directions_pdf_unpublished_returns_404(client: AsyncClient, db_session, test_account, gps_section):
    plot = await _make_plot(db_session, test_account.id, plot_ref="B-88", lat=45.42, lng=-75.69, section=gps_section)
    await _make_memorial(db_session, test_account.id, slug="secret-sam", plot=plot, published=False)
    r = await client.get(
        "/api/public/memorial/secret-sam/directions.pdf",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 404


# ── SAC-06: CRLF in plot_ref cannot inject a header ─────────────────────────
@pytest.mark.asyncio
async def test_directions_pdf_filename_sanitised(client: AsyncClient, db_session, test_account, gps_section):
    plot = await _make_plot(
        db_session, test_account.id, plot_ref="B-204\r\nSet-Cookie: x=1", lat=45.42, lng=-75.69, section=gps_section
    )
    await _make_memorial(db_session, test_account.id, slug="crlf-carol", plot=plot)
    r = await client.get(
        "/api/public/memorial/crlf-carol/directions.pdf",
        headers={TENANT_HDR: str(test_account.id)},
    )
    assert r.status_code == 200
    cd = r.headers["content-disposition"]
    # The CRLF (header-split vector) and the ": " that would start an injected
    # header must be stripped — only [A-Za-z0-9._-] survive in the filename.
    assert "\r" not in cd and "\n" not in cd
    assert ": " not in cd.split("filename=")[1]
    assert cd.startswith("attachment; filename=")
