"""Cemetery Map settings — boundary & section validation rules.

Covers eight data-integrity rules for the Settings > Cemetery Map tab:

1. Cemetery boundary can't be edited/cleared once a section has its own
   boundary drawn inside it (supersedes the old INDL-55 BR-01
   allow-with-warning behaviour).
2. No default section is ever auto-created for a new tenant.
3. Sections can't overlap or nest inside one another (sharing only an edge
   is allowed).
4. A section must be fully inside the cemetery boundary (pre-existing,
   INDL-55 E-05 — regression-covered here for completeness).
5. A section's boundary can't be redrawn or cleared once it has plots.
6. A section can't be deleted while it has plots.
7. (Frontend concern — the section row/map feature disappear together once
   the DELETE succeeds; nothing further to assert server-side beyond rule 6.)
8. A section can't be created — even a boundary-less, metadata-only one —
   until the cemetery boundary is drawn.
"""
from uuid import uuid4

import pytest
from httpx import AsyncClient
from sqlalchemy import func, select
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.sections.models.section import Section
from src.apps.tenants.models.account import Account
from src.core.security import build_token_payload, create_access_token

pytestmark = pytest.mark.asyncio

# A large cemetery-grounds polygon near Ottawa (lng, lat) — ~780 m x 555 m —
# big enough to contain every small section polygon below.
ACCOUNT_BOUNDARY = {
    "type": "Polygon",
    "coordinates": [[
        [-75.7000, 45.4200],
        [-75.6900, 45.4200],
        [-75.6900, 45.4250],
        [-75.7000, 45.4250],
        [-75.7000, 45.4200],
    ]],
}


def _square(x0, y0, x1, y1) -> dict:
    return {
        "type": "Polygon",
        "coordinates": [[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]],
    }


# Two sections sharing a vertical edge at lng=-75.6975 — must be ALLOWED.
SECTION_A = _square(-75.6980, 45.4210, -75.6975, 45.4215)
SECTION_B_ADJACENT = _square(-75.6975, 45.4210, -75.6970, 45.4215)

# Overlaps SECTION_A's eastern portion — must be REJECTED.
SECTION_B_OVERLAP = _square(-75.6977, 45.4210, -75.6972, 45.4215)

# Fully nested inside SECTION_A, no shared edge — must be REJECTED.
SECTION_B_NESTED = _square(-75.6979, 45.4211, -75.6976, 45.4214)


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

async def _make_account(db: AsyncSession) -> Account:
    uid = uuid4().hex[:8]
    acc = Account(
        organization_name=f"Rules {uid}",
        subdomain=f"rules-{uid}",
        contact_email=f"rules-{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 _set_boundary(client, acc, token, geojson=None) -> None:
    resp = await client.patch(
        "/api/v1/accounts/me/map-config",
        json={"boundary": geojson if geojson is not None else ACCOUNT_BOUNDARY},
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200, resp.text


async def _create_section(client, acc, token, code, name, boundary=None):
    payload = {"code": code, "name": name}
    if boundary is not None:
        payload["boundary"] = boundary
    return await client.post("/api/v1/sections", json=payload, headers=_headers(token, acc))


# ── Rule 8: no section before the cemetery boundary exists ────────────────────

async def test_create_section_requires_cemetery_boundary(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    resp = await _create_section(client, acc, token, "A", "Section A")
    assert resp.status_code == 422
    assert "cemetery boundary" in resp.json()["message"].lower()


async def test_create_section_succeeds_once_boundary_drawn(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    resp = await _create_section(client, acc, token, "A", "Section A")
    assert resp.status_code == 201, resp.text


# ── Rule 1: cemetery boundary is locked once a section has a boundary ─────────

async def test_boundary_edit_blocked_when_a_section_has_boundary(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    resp = await _create_section(client, acc, token, "A", "Section A", boundary=SECTION_A)
    assert resp.status_code == 201, resp.text

    edit = await client.patch(
        "/api/v1/accounts/me/map-config",
        json={"boundary": ACCOUNT_BOUNDARY}, headers=_headers(token, acc),
    )
    assert edit.status_code == 409
    assert "delete all sections" in edit.json()["message"].lower()

    clear = await client.patch(
        "/api/v1/accounts/me/map-config",
        json={"boundary": None}, headers=_headers(token, acc),
    )
    assert clear.status_code == 409


async def test_boundary_edit_allowed_when_no_section_has_boundary(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    # A section exists but has no boundary of its own (metadata-only).
    resp = await _create_section(client, acc, token, "A", "Section A")
    assert resp.status_code == 201, resp.text

    edit = await client.patch(
        "/api/v1/accounts/me/map-config",
        json={"boundary": ACCOUNT_BOUNDARY}, headers=_headers(token, acc),
    )
    assert edit.status_code == 200, edit.text


# ── Rule 3: sections can't overlap or nest; edge-touching is fine ─────────────

async def test_create_section_overlapping_another_rejected(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    first = await _create_section(client, acc, token, "A", "Section A", boundary=SECTION_A)
    assert first.status_code == 201, first.text

    resp = await _create_section(client, acc, token, "B", "Section B", boundary=SECTION_B_OVERLAP)
    assert resp.status_code == 422
    assert "overlaps another section" in resp.json()["message"].lower()


async def test_create_section_nested_inside_another_rejected(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    first = await _create_section(client, acc, token, "A", "Section A", boundary=SECTION_A)
    assert first.status_code == 201, first.text

    resp = await _create_section(client, acc, token, "B", "Section B", boundary=SECTION_B_NESTED)
    assert resp.status_code == 422
    assert "overlaps another section" in resp.json()["message"].lower()


async def test_create_section_sharing_edge_allowed(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    first = await _create_section(client, acc, token, "A", "Section A", boundary=SECTION_A)
    assert first.status_code == 201, first.text

    resp = await _create_section(client, acc, token, "B", "Section B", boundary=SECTION_B_ADJACENT)
    assert resp.status_code == 201, resp.text


async def test_update_section_boundary_overlap_rejected(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    a = await _create_section(client, acc, token, "A", "Section A", boundary=SECTION_A)
    assert a.status_code == 201, a.text
    b = await _create_section(client, acc, token, "B", "Section B")  # no boundary yet
    assert b.status_code == 201, b.text
    b_id = b.json()["data"]["id"]

    resp = await client.patch(
        f"/api/v1/sections/{b_id}",
        json={"boundary": SECTION_B_OVERLAP}, headers=_headers(token, acc),
    )
    assert resp.status_code == 422
    assert "overlaps another section" in resp.json()["message"].lower()


# ── Rule 5: a section's boundary can't be redrawn/cleared once it has plots ──

async def test_update_section_boundary_blocked_when_plots_exist(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    sec_resp = await _create_section(client, acc, token, "A", "Section A", boundary=SECTION_A)
    assert sec_resp.status_code == 201, sec_resp.text
    sec_id = sec_resp.json()["data"]["id"]
    db_session.add(Plot(tenant_id=acc.id, plot_ref="A-1", section_id=sec_id, status="vacant"))
    await db_session.flush()

    redraw = await client.patch(
        f"/api/v1/sections/{sec_id}",
        json={"boundary": SECTION_B_ADJACENT}, headers=_headers(token, acc),
    )
    assert redraw.status_code == 409
    assert "plot" in redraw.json()["message"].lower()

    clear = await client.patch(
        f"/api/v1/sections/{sec_id}",
        json={"boundary": None}, headers=_headers(token, acc),
    )
    assert clear.status_code == 409


async def test_update_section_boundary_allowed_without_plots(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    sec_resp = await _create_section(client, acc, token, "A", "Section A", boundary=SECTION_A)
    assert sec_resp.status_code == 201, sec_resp.text
    sec_id = sec_resp.json()["data"]["id"]

    resp = await client.patch(
        f"/api/v1/sections/{sec_id}",
        json={"boundary": None}, headers=_headers(token, acc),
    )
    assert resp.status_code == 200, resp.text


# ── Rule 6: a section can't be deleted while it has plots ─────────────────────

async def test_delete_section_blocked_when_plots_exist(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    sec_resp = await _create_section(client, acc, token, "A", "Section A", boundary=SECTION_A)
    sec_id = sec_resp.json()["data"]["id"]
    db_session.add(Plot(tenant_id=acc.id, plot_ref="A-1", section_id=sec_id, status="vacant"))
    await db_session.flush()

    resp = await client.delete(f"/api/v1/sections/{sec_id}", headers=_headers(token, acc))
    assert resp.status_code == 409
    assert "plot" in resp.json()["message"].lower()

    still = (await db_session.execute(
        select(Section).where(Section.id == sec_id))).scalar_one_or_none()
    assert still is not None


async def test_delete_empty_section_succeeds(client: AsyncClient, db_session):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, "administrator")
    await _set_boundary(client, acc, token)
    sec_resp = await _create_section(client, acc, token, "A", "Section A", boundary=SECTION_A)
    sec_id = sec_resp.json()["data"]["id"]

    resp = await client.delete(f"/api/v1/sections/{sec_id}", headers=_headers(token, acc))
    assert resp.status_code == 204

    gone = (await db_session.execute(
        select(Section).where(Section.id == sec_id))).scalar_one_or_none()
    assert gone is None


# ── Rule 2: no default section is ever auto-created for a new tenant ──────────

async def test_new_tenant_provisioning_creates_no_sections(db_session):
    from src.apps.tenants.services.tenant_service import TenantService

    uid = uuid4().hex[:8]
    service = TenantService(db_session)
    account, _admin = await service.create({
        "organization_name": f"Fresh Cemetery {uid}",
        "subdomain": f"fresh-{uid}",
        "contact_email": f"fresh-{uid}@test.ca",
        "admin_password": "Sup3rSecret!",
        "admin_first_name": "T",
        "admin_last_name": "U",
    })

    count = (await db_session.execute(
        select(func.count(Section.id)).where(Section.tenant_id == account.id))).scalar_one()
    assert count == 0
