"""
INDL-39 — Records Management Enhancements.

Tests for the expanded ``AuditService.get_record_audit`` (documents + memorials)
and the role/tenant-isolation controls on ``GET /api/v1/records/{id}/audit``.

Covers PRD acceptance criteria AC-10, AC-12 and security criteria
SEC-01, SEC-03, SEC-04, SEC-11 (SAC-01..SAC-04, SAC-10).
"""
import uuid

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

from src.apps.auth.models.user import User
from src.apps.documents.models.document import Document
from src.apps.memorials.models.memorial import Memorial
from src.apps.records.models.record import Record
from src.apps.site_admin.models.audit_log import AuditLog
from src.apps.site_admin.services.audit_service import AuditService, MAX_RELATED_IDS
from src.apps.tenants.models.account import Account
from src.core.security import build_token_payload, create_access_token, hash_password

pytestmark = pytest.mark.asyncio


# --------------------------------------------------------------------------- #
# Helpers
# --------------------------------------------------------------------------- #
async def _make_account(db: AsyncSession, subdomain: str) -> Account:
    account = Account(
        organization_name=f"Cemetery {subdomain}",
        subdomain=subdomain,
        contact_email=f"admin@{subdomain}.com",
        plan="starter",
        status="active",
    )
    db.add(account)
    await db.flush()
    return account


async def _make_user(db: AsyncSession, account: Account, role: str) -> User:
    user = User(
        tenant_id=account.id,
        email=f"{role}-{uuid.uuid4().hex[:8]}@{account.subdomain}.com",
        password_hash=hash_password("TestPassword123"),
        first_name=role.capitalize(),
        last_name="User",
        role=role,
        status="active",
    )
    db.add(user)
    await db.flush()
    return user


async def _make_record(db: AsyncSession, account: Account) -> Record:
    record = Record(
        tenant_id=account.id,
        first_name="Patricia",
        last_name="O'Brien",
        is_veteran=True,
        status="active",
    )
    db.add(record)
    await db.flush()
    return record


async def _audit(
    db: AsyncSession,
    account: Account,
    user: User,
    entity_type: str,
    entity_id,
    action: str,
) -> AuditLog:
    entry = AuditLog(
        tenant_id=account.id,
        user_id=user.id,
        entity_type=entity_type,
        entity_id=entity_id,
        action=action,
    )
    db.add(entry)
    await db.flush()
    return entry


def _token(user: User, account: Account) -> dict:
    payload = build_token_payload(user, account)
    return {"Authorization": f"Bearer {create_access_token(payload)}"}


# --------------------------------------------------------------------------- #
# Service-level: expanded entity scope (AC-10, AC-12)
# --------------------------------------------------------------------------- #
async def test_get_record_audit_includes_document_and_memorial(db_session: AsyncSession):
    account = await _make_account(db_session, "greenhills")
    admin = await _make_user(db_session, account, "administrator")
    record = await _make_record(db_session, account)

    doc = Document(
        tenant_id=account.id,
        entity_type="record",
        entity_id=record.id,
        s3_key="s3/key/death-cert.pdf",
        filename="death-cert.pdf",
        uploaded_by=admin.id,
    )
    db_session.add(doc)
    await db_session.flush()

    memorial = Memorial(
        tenant_id=account.id,
        record_id=record.id,
        slug="patricia-obrien",
        is_published=True,
    )
    db_session.add(memorial)
    await db_session.flush()

    # One audit entry per entity type that should surface in the record audit.
    await _audit(db_session, account, admin, "Record", record.id, "create")
    await _audit(db_session, account, admin, "document", doc.id, "document_upload")
    await _audit(db_session, account, admin, "memorial", memorial.id, "created")

    logs, total = await AuditService.get_record_audit(
        db_session, account.id, record.id
    )

    entity_types = {entry.log.entity_type for entry in logs}
    assert total == 3
    assert "Record" in entity_types
    assert "document" in entity_types
    assert "memorial" in entity_types


async def test_get_record_audit_orders_by_created_at_desc(db_session: AsyncSession):
    account = await _make_account(db_session, "orderedcem")
    admin = await _make_user(db_session, account, "administrator")
    record = await _make_record(db_session, account)

    await _audit(db_session, account, admin, "Record", record.id, "create")
    await _audit(db_session, account, admin, "Record", record.id, "update")

    logs, _ = await AuditService.get_record_audit(db_session, account.id, record.id)
    timestamps = [entry.log.created_at for entry in logs]
    assert timestamps == sorted(timestamps, reverse=True)


# --------------------------------------------------------------------------- #
# Cross-tenant isolation on the document sub-query (SEC-04 / AS-02)
# --------------------------------------------------------------------------- #
async def test_cross_tenant_document_audit_not_leaked(db_session: AsyncSession):
    """A document in tenant B that shares the tenant-A record's entity_id must
    never contribute its audit entries to tenant A's audit response."""
    tenant_a = await _make_account(db_session, "tenanta")
    tenant_b = await _make_account(db_session, "tenantb")
    admin_a = await _make_user(db_session, tenant_a, "administrator")
    admin_b = await _make_user(db_session, tenant_b, "administrator")

    record_a = await _make_record(db_session, tenant_a)

    # Malicious: tenant-B document whose entity_id points at tenant A's record.
    rogue_doc = Document(
        tenant_id=tenant_b.id,
        entity_type="record",
        entity_id=record_a.id,
        s3_key="s3/key/rogue.pdf",
        filename="rogue.pdf",
        uploaded_by=admin_b.id,
    )
    db_session.add(rogue_doc)
    await db_session.flush()

    # Audit entry written under tenant B for that rogue document.
    await _audit(db_session, tenant_b, admin_b, "document", rogue_doc.id, "document_upload")
    # A legitimate tenant-A record entry.
    await _audit(db_session, tenant_a, admin_a, "Record", record_a.id, "create")

    logs, total = await AuditService.get_record_audit(
        db_session, tenant_a.id, record_a.id
    )
    ids = {entry.log.entity_id for entry in logs}
    assert rogue_doc.id not in ids
    assert total == 1  # only the legitimate tenant-A Record entry


# --------------------------------------------------------------------------- #
# Bounded related_ids (SEC-11 / API4)
# --------------------------------------------------------------------------- #
async def test_related_ids_bounded(db_session: AsyncSession):
    """Even with many documents, the collection stays bounded and the query
    returns without error."""
    account = await _make_account(db_session, "bulkcem")
    admin = await _make_user(db_session, account, "administrator")
    record = await _make_record(db_session, account)

    # Create more documents than the cap to exercise the bound.
    for i in range(MAX_RELATED_IDS + 20):
        db_session.add(
            Document(
                tenant_id=account.id,
                entity_type="record",
                entity_id=record.id,
                s3_key=f"s3/key/doc-{i}.pdf",
                filename=f"doc-{i}.pdf",
                uploaded_by=admin.id,
            )
        )
    await db_session.flush()

    logs, total = await AuditService.get_record_audit(db_session, account.id, record.id)
    # No audit rows written for those docs, so total is 0 — the point is it does
    # not raise and completes quickly with a bounded IN-clause.
    assert total == 0
    assert isinstance(logs, list)


# --------------------------------------------------------------------------- #
# Endpoint role guards (SEC-01 / SAC-03 / AC-13)
# --------------------------------------------------------------------------- #
@pytest_asyncio.fixture
async def record_with_admin(db_session: AsyncSession):
    account = await _make_account(db_session, "roleguard")
    admin = await _make_user(db_session, account, "administrator")
    record = await _make_record(db_session, account)
    return account, admin, record


@pytest.mark.parametrize("role", ["staff", "manager", "view_only"])
async def test_audit_endpoint_forbidden_below_administrator(
    client, db_session: AsyncSession, record_with_admin, role
):
    account, _admin, record = record_with_admin
    user = await _make_user(db_session, account, role)
    resp = await client.get(
        f"/api/v1/records/{record.id}/audit", headers=_token(user, account)
    )
    assert resp.status_code == 403


async def test_audit_endpoint_allowed_for_administrator(
    client, record_with_admin
):
    account, admin, record = record_with_admin
    resp = await client.get(
        f"/api/v1/records/{record.id}/audit", headers=_token(admin, account)
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["success"] is True


async def test_audit_endpoint_page_size_capped(client, record_with_admin):
    """SAC-10 — page_size beyond the server cap (200) is rejected (422)."""
    account, admin, record = record_with_admin
    resp = await client.get(
        f"/api/v1/records/{record.id}/audit?page_size=10000",
        headers=_token(admin, account),
    )
    assert resp.status_code == 422


# --------------------------------------------------------------------------- #
# Delete role guard (SEC-02 / API5)
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("role", ["staff", "manager", "view_only"])
async def test_delete_record_forbidden_below_administrator(
    client, db_session: AsyncSession, record_with_admin, role
):
    account, _admin, record = record_with_admin
    user = await _make_user(db_session, account, role)
    resp = await client.delete(
        f"/api/v1/records/{record.id}", headers=_token(user, account)
    )
    assert resp.status_code == 403
