"""INDL-41 — Cemetery Map: Core View backend coverage.

Covers the map bootstrap endpoint, tenant-configurable status catalog, grave
grouping, plot-delete guard, name search, geometry writes, section remaining
counts, and the records plot_id filter — including the PRD's Security Acceptance
Criteria (SAC-01..SAC-09).
"""
from uuid import uuid4

import pytest
from geoalchemy2 import WKTElement
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.auth.models.user import User
from src.apps.plots.models.plot import Plot
from src.apps.records.models.record import Record
from src.apps.sections.models.section import Section
from src.apps.tenants.models.account import Account
from src.core.security import build_token_payload, create_access_token
from src.core.utils.geometry import validate_polygon_geojson

pytestmark = pytest.mark.asyncio


# Two adjacent polygons (share the x=-75.6970 edge) + one far away.
POLY_A = {"type": "Polygon", "coordinates": [[
    [-75.6972, 45.4215], [-75.6970, 45.4215],
    [-75.6970, 45.4217], [-75.6972, 45.4217], [-75.6972, 45.4215]]]}
POLY_B = {"type": "Polygon", "coordinates": [[
    [-75.6970, 45.4215], [-75.6968, 45.4215],
    [-75.6968, 45.4217], [-75.6970, 45.4217], [-75.6970, 45.4215]]]}
POLY_FAR = {"type": "Polygon", "coordinates": [[
    [-75.5000, 45.3000], [-75.4998, 45.3000],
    [-75.4998, 45.3002], [-75.5000, 45.3002], [-75.5000, 45.3000]]]}
BOWTIE = {"type": "Polygon", "coordinates": [[[0, 0], [1, 1], [0, 1], [1, 0], [0, 0]]]}


# ── helpers ─────────────────────────────────────────────────────────────────

async def _make_account(db: AsyncSession) -> Account:
    uid = uuid4().hex[:8]
    acc = Account(
        organization_name=f"Map Cemetery {uid}",
        subdomain=f"map-{uid}",
        contact_email=f"map-{uid}@test.ca",
        plan="starter",
        status="active",
    )
    db.add(acc)
    await db.flush()
    return acc


async def _make_user(db: AsyncSession, account: Account, role: str = "administrator") -> str:
    user = User(
        tenant_id=account.id,
        email=f"{role}-{uuid4().hex[:6]}@test.ca",
        password_hash="x",
        first_name="T",
        last_name="U",
        role=role,
        status="active",
    )
    db.add(user)
    await db.flush()
    return create_access_token(build_token_payload(user, account))


def _headers(token: str, account: Account) -> dict:
    return {"Authorization": f"Bearer {token}", "X-Tenant-ID": str(account.id)}


async def _make_section(db, account, code="A", name="Section A") -> Section:
    sec = Section(
        tenant_id=account.id, code=code, name=name,
        plot_number_prefix=code, next_plot_seq=1,
    )
    db.add(sec)
    await db.flush()
    return sec


async def _make_plot(db, account, ref, section=None, status="vacant", geojson=None) -> Plot:
    plot = Plot(
        tenant_id=account.id,
        plot_ref=ref,
        section_id=section.id if section else None,
        status=status,
    )
    if geojson is not None:
        poly = validate_polygon_geojson(geojson)
        plot.geometry = WKTElement(poly.wkt, srid=4326, extended=False)
        plot.centroid = WKTElement(
            f"POINT({poly.centroid.x} {poly.centroid.y})", srid=4326, extended=False
        )
        plot.latitude = round(poly.centroid.y, 7)
        plot.longitude = round(poly.centroid.x, 7)
    db.add(plot)
    await db.flush()
    return plot


async def _make_record(db, account, plot, first="John", last="Walsh") -> Record:
    rec = Record(tenant_id=account.id, plot_id=plot.id, first_name=first, last_name=last)
    db.add(rec)
    await db.flush()
    return rec


# ── AC-01/03/17 + SAC-09: map bootstrap ───────────────────────────────────────

async def test_map_returns_structure_and_no_store(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    sec = await _make_section(db_session, acc)
    await _make_plot(db_session, acc, "A-1", sec, geojson=POLY_A)
    await _make_plot(db_session, acc, "A-2", sec)  # point/no-geometry plot

    r = await client.get("/api/v1/plots/map", headers=_headers(token, acc))
    assert r.status_code == 200
    assert "no-store" in (r.headers.get("Cache-Control") or "")  # SAC-09
    data = r.json()["data"]
    assert data["map_mode"] in ("geo", "image")
    # 4 default statuses seeded lazily
    assert len(data["statuses"]) == 4
    assert {s["name"] for s in data["statuses"]} == {
        "Available", "Reserved", "Interred", "On hold"}
    assert data["plots"]["type"] == "FeatureCollection"
    assert len(data["plots"]["features"]) == 2
    # a drawn plot serialises its polygon; the other still renders (point/None)
    geoms = [f["geometry"]["type"] for f in data["plots"]["features"] if f["geometry"]]
    assert "Polygon" in geoms


async def test_map_feature_uses_label_manual(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    sec = await _make_section(db_session, acc)
    p = await _make_plot(db_session, acc, "A-1", sec)
    p.label_manual = "Rose Garden 1"
    await db_session.flush()

    r = await client.get("/api/v1/plots/map", headers=_headers(token, acc))
    feat = r.json()["data"]["plots"]["features"][0]
    assert feat["properties"]["label"] == "Rose Garden 1"  # AC-25


async def test_map_view_only_allowed(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="view_only")
    r = await client.get("/api/v1/plots/map", headers=_headers(token, acc))
    assert r.status_code == 200


async def test_map_tenant_isolation(client: AsyncClient, db_session):
    """SAC-01: tenant from the token/state, never overridable by X-Tenant-ID."""
    acc_a = await _make_account(db_session)
    acc_b = await _make_account(db_session)
    token_a = await _make_user(db_session, acc_a)
    sec_a = await _make_section(db_session, acc_a)
    sec_b = await _make_section(db_session, acc_b, code="B", name="Sec B")
    await _make_plot(db_session, acc_a, "A-1", sec_a)
    await _make_plot(db_session, acc_b, "B-1", sec_b)

    # A's token but spoof the header to B's id — must still only see A's plots.
    headers = {"Authorization": f"Bearer {token_a}", "X-Tenant-ID": str(acc_b.id)}
    r = await client.get("/api/v1/plots/map", headers=headers)
    refs = {f["properties"]["plot_ref"] for f in r.json()["data"]["plots"]["features"]}
    assert refs == {"A-1"}


# ── AC-06/17 + SAC-05: status catalog ─────────────────────────────────────────

async def test_status_list_seeds_defaults(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    r = await client.get("/api/v1/plot-statuses", headers=_headers(token, acc))
    assert r.status_code == 200
    names = [s["name"] for s in r.json()["data"]]
    assert names == ["Available", "Reserved", "Interred", "On hold"]  # sort_order


async def test_status_create_and_duplicate(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    body = {"name": "Maintenance", "color_hex": "#111827"}
    r = await client.post("/api/v1/plot-statuses", json=body, headers=_headers(token, acc))
    assert r.status_code == 201
    assert r.json()["data"]["is_default"] is False
    dup = await client.post("/api/v1/plot-statuses", json=body, headers=_headers(token, acc))
    assert dup.status_code == 409


async def test_status_create_forbidden_for_manager(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="manager")
    r = await client.post(
        "/api/v1/plot-statuses",
        json={"name": "X", "color_hex": "#000000"},
        headers=_headers(token, acc),
    )
    assert r.status_code == 403  # administrator-only


async def test_status_delete_blocked_when_referenced(client: AsyncClient, db_session):
    """SAC-05: a default status used by a plot cannot be deleted."""
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    sec = await _make_section(db_session, acc)
    await _make_plot(db_session, acc, "A-1", sec, status="vacant")
    # seed + fetch statuses
    listed = await client.get("/api/v1/plot-statuses", headers=_headers(token, acc))
    available = next(s for s in listed.json()["data"] if s["status_key"] == "vacant")
    r = await client.delete(
        f"/api/v1/plot-statuses/{available['id']}", headers=_headers(token, acc)
    )
    assert r.status_code == 409


async def test_status_delete_custom_ok(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    created = await client.post(
        "/api/v1/plot-statuses",
        json={"name": "Blocked", "color_hex": "#ef4444"},
        headers=_headers(token, acc),
    )
    sid = created.json()["data"]["id"]
    r = await client.delete(f"/api/v1/plot-statuses/{sid}", headers=_headers(token, acc))
    assert r.status_code == 204


# ── AC-26 + SAC-04: plot delete guard ─────────────────────────────────────────

async def test_delete_blocked_with_linked_record(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    sec = await _make_section(db_session, acc)
    plot = await _make_plot(db_session, acc, "A-1", sec, status="occupied")
    await _make_record(db_session, acc, plot)
    r = await client.delete(f"/api/v1/plots/{plot.id}", headers=_headers(token, acc))
    assert r.status_code == 409


async def test_delete_vacant_ok(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    sec = await _make_section(db_session, acc)
    plot = await _make_plot(db_session, acc, "A-1", sec, status="vacant")
    r = await client.delete(f"/api/v1/plots/{plot.id}", headers=_headers(token, acc))
    assert r.status_code == 204


async def test_delete_forbidden_for_staff(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="staff")
    sec = await _make_section(db_session, acc)
    plot = await _make_plot(db_session, acc, "A-1", sec, status="vacant")
    r = await client.delete(f"/api/v1/plots/{plot.id}", headers=_headers(token, acc))
    assert r.status_code == 403


# ── AC-04: search by decedent/family name ─────────────────────────────────────

async def test_search_by_record_name(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    sec = await _make_section(db_session, acc)
    p1 = await _make_plot(db_session, acc, "A-1", sec, status="occupied")
    await _make_plot(db_session, acc, "A-2", sec, status="vacant")
    await _make_record(db_session, acc, p1, first="Mary", last="Walsh")

    r = await client.get("/api/v1/plots?search=Walsh", headers=_headers(token, acc))
    assert r.status_code == 200
    refs = [p["plot_ref"] for p in r.json()["data"]]
    assert refs == ["A-1"]


# ── AC-19 + SAC-02/03: geometry writes ────────────────────────────────────────

async def test_patch_geometry_mirrors_latlng(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="manager")
    sec = await _make_section(db_session, acc)
    plot = await _make_plot(db_session, acc, "A-1", sec)
    r = await client.patch(
        f"/api/v1/plots/{plot.id}",
        json={"geometry": POLY_A},
        headers=_headers(token, acc),
    )
    assert r.status_code == 200
    data = r.json()["data"]
    assert data["geometry"]["type"] == "Polygon"
    assert data["latitude"] is not None and data["longitude"] is not None


async def test_patch_geometry_self_intersecting_422(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="manager")
    sec = await _make_section(db_session, acc)
    plot = await _make_plot(db_session, acc, "A-1", sec)
    r = await client.patch(
        f"/api/v1/plots/{plot.id}",
        json={"geometry": BOWTIE},
        headers=_headers(token, acc),
    )
    assert r.status_code == 422  # SAC-03


async def test_patch_geometry_forbidden_for_staff(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="staff")
    sec = await _make_section(db_session, acc)
    plot = await _make_plot(db_session, acc, "A-1", sec)
    r = await client.patch(
        f"/api/v1/plots/{plot.id}",
        json={"geometry": POLY_A},
        headers=_headers(token, acc),
    )
    assert r.status_code == 403  # SAC-02


# ── AC-20 + SAC-06: grave grouping ────────────────────────────────────────────

async def test_group_adjacent_plots(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="manager")
    sec = await _make_section(db_session, acc)
    a = await _make_plot(db_session, acc, "A-1", sec, geojson=POLY_A)
    b = await _make_plot(db_session, acc, "A-2", sec, geojson=POLY_B)
    r = await client.post(
        "/api/v1/plot-groups",
        json={"plot_ids": [str(a.id), str(b.id)], "group_type": "couple"},
        headers=_headers(token, acc),
    )
    assert r.status_code == 201
    assert len(r.json()["data"]["plot_ids"]) == 2


async def test_group_non_adjacent_rejected(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="manager")
    sec = await _make_section(db_session, acc)
    a = await _make_plot(db_session, acc, "A-1", sec, geojson=POLY_A)
    far = await _make_plot(db_session, acc, "A-9", sec, geojson=POLY_FAR)
    r = await client.post(
        "/api/v1/plot-groups",
        json={"plot_ids": [str(a.id), str(far.id)], "group_type": "custom"},
        headers=_headers(token, acc),
    )
    assert r.status_code == 422


async def test_group_too_many_plots_422(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="manager")
    ids = [str(uuid4()) for _ in range(51)]
    r = await client.post(
        "/api/v1/plot-groups",
        json={"plot_ids": ids, "group_type": "custom"},
        headers=_headers(token, acc),
    )
    assert r.status_code == 422  # SAC-06


async def test_group_delete(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="manager")
    sec = await _make_section(db_session, acc)
    a = await _make_plot(db_session, acc, "A-1", sec, geojson=POLY_A)
    b = await _make_plot(db_session, acc, "A-2", sec, geojson=POLY_B)
    created = await client.post(
        "/api/v1/plot-groups",
        json={"plot_ids": [str(a.id), str(b.id)], "group_type": "couple"},
        headers=_headers(token, acc),
    )
    gid = created.json()["data"]["id"]
    r = await client.delete(f"/api/v1/plot-groups/{gid}", headers=_headers(token, acc))
    assert r.status_code == 204


async def test_group_cross_tenant_plot_404(client: AsyncClient, db_session):
    acc_a = await _make_account(db_session)
    acc_b = await _make_account(db_session)
    token_a = await _make_user(db_session, acc_a, role="manager")
    sec_a = await _make_section(db_session, acc_a)
    a = await _make_plot(db_session, acc_a, "A-1", sec_a, geojson=POLY_A)
    sec_b = await _make_section(db_session, acc_b, code="B", name="B")
    b = await _make_plot(db_session, acc_b, "B-1", sec_b, geojson=POLY_B)
    r = await client.post(
        "/api/v1/plot-groups",
        json={"plot_ids": [str(a.id), str(b.id)], "group_type": "couple"},
        headers=_headers(token_a, acc_a),
    )
    assert r.status_code == 404


# ── AC-29: section remaining counts ───────────────────────────────────────────

async def test_sections_available_total_counts(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    sec = await _make_section(db_session, acc)
    await _make_plot(db_session, acc, "A-1", sec, status="vacant")
    await _make_plot(db_session, acc, "A-2", sec, status="vacant")
    await _make_plot(db_session, acc, "A-3", sec, status="occupied")

    r = await client.get("/api/v1/sections", headers=_headers(token, acc))
    row = r.json()["data"][0]
    assert row["total_count"] == 3
    assert row["available_count"] == 2


# ── AC-14: records plot_id filter (Occupants tab) ─────────────────────────────

async def test_records_plot_id_filter(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc)
    sec = await _make_section(db_session, acc)
    p1 = await _make_plot(db_session, acc, "A-1", sec, status="occupied")
    p2 = await _make_plot(db_session, acc, "A-2", sec, status="occupied")
    await _make_record(db_session, acc, p1, first="Anne", last="Smith")
    await _make_record(db_session, acc, p2, first="Bob", last="Jones")

    r = await client.get(
        f"/api/v1/records?plot_id={p1.id}", headers=_headers(token, acc)
    )
    assert r.status_code == 200
    data = r.json()["data"]
    assert len(data) == 1
    assert data[0]["last_name"] == "Smith"
