"""INDL-36 Service Scheduling Phase 2 — week calendar + week-PDF API tests.

Runs against the FastAPI app + the indelis_test Postgres DB (see conftest.py).
Redis rate-limiting fails open, and per-test tenant/user IDs are unique UUIDs,
so counters never collide across tests.

Covers (SEC-01..SEC-12 subset + functional ACs):
  Week calendar view      → AC-12 (draft excluded), SEC-03 (no family_contact_name),
                            SEC-06 (bad week_start 422), SEC-07 (bad event_type 422)
  PATCH /{id}/status      → SEC-01 (view_only 403), SEC-02 (cross-tenant 404),
                            SEC-08 (extra fields 422), SEC-09 (FSM 422 / idempotent 200)
  GET /week-pdf           → SEC-04 (401 unauth), SEC-05 (200 + hardened headers),
                            XML-escaping HIGH regression (&, <, >, =CMD prefix)
"""
import uuid
from datetime import date, time

import pytest
import pytest_asyncio
from httpx import AsyncClient

from src.apps.auth.models.user import User
from src.apps.scheduling.models.service_event import ServiceEvent
from src.apps.tenants.models.account import Account
from src.core.security import build_token_payload, create_access_token, hash_password

BASE = "/api/v1/scheduling"

# A concrete week window used across the calendar/PDF tests. The service-side
# window is [week_start, week_start + 7d); the default payload date (2026-08-01)
# falls on day 0, so it is inside this window.
WEEK_START = "2026-08-01"


# ── Fixtures / helpers (mirrors tests/test_scheduling.py) ─────────────────────

def _token(user, account) -> str:
    return create_access_token(build_token_payload(user, account))


async def _make_user(db_session, account, role, email):
    user = User(
        tenant_id=account.id,
        email=email,
        password_hash=hash_password("TestPassword123"),
        first_name=role.capitalize(),
        last_name="User",
        role=role,
        status="active",
    )
    db_session.add(user)
    await db_session.flush()
    return user


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


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


@pytest_asyncio.fixture
async def viewonly_headers(db_session, test_account):
    user = await _make_user(db_session, test_account, "view_only", "vo@testcemetery.com")
    return _headers(user, test_account)


@pytest_asyncio.fixture
async def other_tenant(db_session):
    account = Account(
        organization_name="Other Cemetery",
        subdomain="othercemetery",
        contact_email="admin@othercemetery.com",
        plan="starter",
        status="active",
    )
    db_session.add(account)
    await db_session.flush()
    return account


@pytest_asyncio.fixture
async def foreign_service(db_session, other_tenant):
    ev = ServiceEvent(
        tenant_id=other_tenant.id,
        service_type="interment",
        scheduled_date=date(2026, 8, 1),
        scheduled_time=time(11, 0),
        status="awaiting_family_confirm",
        decedent_name="Foreign Decedent",
    )
    db_session.add(ev)
    await db_session.flush()
    return ev


def _payload(**overrides) -> dict:
    data = {
        "service_type": "interment",
        "decedent_name": "John Doe",
        "date": WEEK_START,
        "start_time": "10:00:00",
        "duration_minutes": 90,
        "plot_location": "Section A, Row 3, Plot 12",
        "section": "Garden of Peace",
        "officiant": "Rev. Smith",
        "family_contact_name": "Mary Doe",
        "family_contact_phone": "(613) 555-0199",
        "family_contact_email": "mary@example.com",
        "expected_attendees": 40,
        "event_note": "Graveside only.",
        "crew_member_ids": [],
        "run_sheet_items": [],
        "equipment_needed": "Lowering device",
        "notes_for_crew": "Bring tent.",
        "notify_family_confirmation": False,
        "notify_family_reminder_24h": False,
        "status": "draft",
    }
    data.update(overrides)
    return data


async def _create_service(client, headers, **overrides) -> str:
    res = await client.post(BASE, headers=headers, json=_payload(**overrides))
    assert res.status_code == 201, res.text
    return res.json()["data"]["id"]


# ── Week calendar view ────────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_week_returns_nondraft_excludes_draft_and_omits_pii(
    client: AsyncClient, staff_headers
):
    # AC-12: draft excluded. SEC-03: family_contact_name omitted from calendar rows.
    visible_id = await _create_service(
        client, staff_headers, status="awaiting_family_confirm",
        decedent_name="Visible Decedent",
    )
    draft_id = await _create_service(
        client, staff_headers, status="draft", decedent_name="Draft Hidden",
    )
    res = await client.get(
        f"{BASE}?view=week&week_start={WEEK_START}", headers=staff_headers
    )
    assert res.status_code == 200, res.text
    data = res.json()["data"]
    items = data["items"]
    ids = {item["id"] for item in items}
    assert visible_id in ids, "non-draft service must appear in week view"
    assert draft_id not in ids, "AC-12: draft service must be excluded from week view"
    for item in items:
        assert "family_contact_name" not in item, "SEC-03: PII must be omitted"
        assert "family_contact_phone" not in item
        assert "family_contact_email" not in item


@pytest.mark.asyncio
@pytest.mark.parametrize("bad_week", ["2026-13-99", "'; DROP TABLE--", "not-a-date"])
async def test_week_start_invalid_is_422(client: AsyncClient, staff_headers, bad_week):
    # SEC-06 — unparseable / injection week_start rejected by FastAPI date parsing.
    res = await client.get(
        f"{BASE}?view=week&week_start={bad_week}", headers=staff_headers
    )
    assert res.status_code == 422, res.text


@pytest.mark.asyncio
@pytest.mark.parametrize("bad_type", ["banana", "burial", "'; DROP", "memorial"])
async def test_week_event_type_outside_allowlist_is_422(
    client: AsyncClient, staff_headers, bad_type
):
    # SEC-07 — event_type must be within the Literal allowlist.
    res = await client.get(
        f"{BASE}?view=week&week_start={WEEK_START}&event_type={bad_type}",
        headers=staff_headers,
    )
    assert res.status_code == 422, res.text


# ── PATCH /{id}/status ────────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_patch_status_as_view_only_is_403(
    client: AsyncClient, staff_headers, viewonly_headers
):
    # SEC-01 — view_only lacks the STAFF floor for status changes.
    sid = await _create_service(
        client, staff_headers, status="awaiting_family_confirm"
    )
    res = await client.patch(
        f"{BASE}/{sid}/status", headers=viewonly_headers, json={"status": "confirmed"}
    )
    assert res.status_code == 403, res.text


@pytest.mark.asyncio
async def test_patch_status_cross_tenant_is_404(
    client: AsyncClient, staff_headers, foreign_service
):
    # SEC-02 — a service owned by another tenant is invisible → 404 (not 403).
    res = await client.patch(
        f"{BASE}/{foreign_service.id}/status", headers=staff_headers,
        json={"status": "confirmed"},
    )
    assert res.status_code == 404, res.text


@pytest.mark.asyncio
async def test_patch_status_extra_body_fields_is_422(
    client: AsyncClient, staff_headers
):
    # SEC-08 — StatusUpdateRequest has extra='forbid'.
    sid = await _create_service(
        client, staff_headers, status="awaiting_family_confirm"
    )
    res = await client.patch(
        f"{BASE}/{sid}/status", headers=staff_headers,
        json={"status": "confirmed", "tenant_id": str(uuid.uuid4()), "foo": "bar"},
    )
    assert res.status_code == 422, res.text


@pytest.mark.asyncio
async def test_patch_status_draft_to_confirmed_is_422(
    client: AsyncClient, staff_headers
):
    # SEC-09 — a draft cannot jump straight to confirmed.
    sid = await _create_service(client, staff_headers, status="draft")
    res = await client.patch(
        f"{BASE}/{sid}/status", headers=staff_headers, json={"status": "confirmed"}
    )
    assert res.status_code == 422, res.text


@pytest.mark.asyncio
async def test_patch_status_already_confirmed_is_idempotent_200(
    client: AsyncClient, staff_headers
):
    # SEC-09 — awaiting→confirmed 200, and a second confirm is idempotent 200.
    sid = await _create_service(
        client, staff_headers, status="awaiting_family_confirm"
    )
    first = await client.patch(
        f"{BASE}/{sid}/status", headers=staff_headers, json={"status": "confirmed"}
    )
    assert first.status_code == 200, first.text
    assert first.json()["data"]["status"] == "confirmed"
    second = await client.patch(
        f"{BASE}/{sid}/status", headers=staff_headers, json={"status": "confirmed"}
    )
    assert second.status_code == 200, second.text
    assert second.json()["data"]["status"] == "confirmed"


# ── GET /week-pdf ─────────────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_week_pdf_authenticated_staff_200_with_hardened_headers(
    client: AsyncClient, staff_headers
):
    # SEC-05 — staff gets a PDF with security headers.
    await _create_service(
        client, staff_headers, status="awaiting_family_confirm",
        decedent_name="Pdf Decedent",
    )
    res = await client.get(
        f"{BASE}/week-pdf?week_start={WEEK_START}", headers=staff_headers
    )
    assert res.status_code == 200, res.text
    assert res.headers["content-type"] == "application/pdf"
    assert res.headers["content-disposition"].startswith("attachment")
    # The endpoint sets "no-store"; the security middleware augments it to
    # "no-cache, no-store, must-revalidate". Assert the no-store directive holds.
    assert "no-store" in res.headers["cache-control"]
    assert res.headers["x-content-type-options"] == "nosniff"
    assert res.content[:4] == b"%PDF"


@pytest.mark.asyncio
async def test_week_pdf_unauthenticated_is_401(client: AsyncClient, test_account):
    # SEC-04 — valid tenant header but no bearer token → auth failure 401.
    th = {"X-Tenant-ID": str(test_account.id)}
    res = await client.get(f"{BASE}/week-pdf?week_start={WEEK_START}", headers=th)
    assert res.status_code == 401, res.text


@pytest.mark.asyncio
async def test_week_pdf_escapes_xml_special_chars_and_formula_prefix_200(
    client: AsyncClient, staff_headers
):
    # Regression for the XML-escaping HIGH fix: names containing &, <, >, and a
    # spreadsheet-formula prefix (=CMD) must not break PDF generation.
    await _create_service(
        client, staff_headers, status="awaiting_family_confirm",
        decedent_name="Doe & <b>Smith</b> >x",
        family_contact_name="=CMD|' /C calc'!A0",
        section="A & B <section>",
        plot_location="Row <1> & <2>",
        notes_for_crew="tent & chairs <urgent>",
    )
    res = await client.get(
        f"{BASE}/week-pdf?week_start={WEEK_START}", headers=staff_headers
    )
    assert res.status_code == 200, res.text
    assert res.content[:4] == b"%PDF"
