"""
Tests for INDL-05 — Site Admin Payments Module.
Covers T-01 through T-21:
  KPIs, list, filters (plan/source/search/pagination),
  CSV export, POST record, auth guards, input validation,
  invoice PDF generation + download.
"""
import pytest
from datetime import date, datetime, timezone
from decimal import Decimal
from uuid import uuid4

from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.site_admin.models.platform_payment import PlatformPayment
from src.apps.tenants.models.account import Account
from src.apps.tenants.models.subscription import Subscription
from src.apps.auth.models.user import User
from src.core.security import hash_password


# ── Helpers ───────────────────────────────────────────────────────────────────


async def _site_admin_token(db: AsyncSession) -> str:
    from src.core.security import create_access_token, build_token_payload

    user = User(
        tenant_id=None,
        email=f"sa.payments.{id(db)}@indelis.com",
        password_hash=hash_password("Test1234!"),
        first_name="Site",
        last_name="Admin",
        role="site_admin",
        status="active",
    )
    db.add(user)
    await db.flush()
    return create_access_token(build_token_payload(user))


async def _admin_token(db: AsyncSession, account: Account) -> str:
    from src.core.security import create_access_token, build_token_payload

    user = User(
        tenant_id=account.id,
        email=f"admin.{id(db)}@test.ca",
        password_hash=hash_password("Test1234!"),
        first_name="Admin",
        last_name="User",
        role="administrator",
        status="active",
    )
    db.add(user)
    await db.flush()
    return create_access_token(build_token_payload(user))


async def _make_account(db: AsyncSession, *, suffix: str = "", plan: str = "starter") -> Account:
    suffix = suffix or str(id(db))[-6:]
    account = Account(
        organization_name=f"Riverside Memorial {suffix}",
        subdomain=f"riverside-{suffix}",
        contact_email=f"admin@riverside-{suffix}.ca",
        plan=plan,
        status="active",
    )
    db.add(account)
    await db.flush()
    return account


async def _make_payment(
    db: AsyncSession,
    account: Account,
    *,
    plan: str = "starter",
    amount: float = 149.00,
    source: str = "portal",
    status: str = "paid",
    invoice_no: str | None = None,
    payment_date: date | None = None,
) -> PlatformPayment:
    payment = PlatformPayment(
        account_id=account.id,
        plan=plan,
        amount_cad=Decimal(str(amount)),
        method="Visa •••• 4242",
        invoice_no=invoice_no or f"INV-{id(db)}-{plan[:2].upper()}",
        source=source,
        status=status,
        payment_date=payment_date or date.today(),
    )
    db.add(payment)
    await db.flush()
    return payment


# ── T-01: KPIs — basic structure ──────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t01_kpis_returns_all_fields(client: AsyncClient, db_session: AsyncSession):
    """T-01: GET /api/site-admin/payments/kpis returns 200 with all KPI keys."""
    token = await _site_admin_token(db_session)
    resp = await client.get(
        "/api/site-admin/payments/kpis",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["success"] is True
    kpis = body["data"]
    assert "total_received" in kpis
    assert "this_month" in kpis
    assert "this_month_label" in kpis
    assert "mrr" in kpis
    assert "payment_count" in kpis


# ── T-02: list — no filters ───────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t02_list_paginated_no_filters(client: AsyncClient, db_session: AsyncSession):
    """T-02: GET /api/site-admin/payments (no filters) returns pagination envelope."""
    token = await _site_admin_token(db_session)
    resp = await client.get(
        "/api/site-admin/payments",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert "total" in body
    assert "page" in body
    assert "page_size" in body
    assert "pages" in body
    assert isinstance(body["data"], list)


# ── T-03: filter by plan ──────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t03_filter_by_plan_professional(client: AsyncClient, db_session: AsyncSession):
    """T-03: ?plan=professional returns only Professional rows."""
    token = await _site_admin_token(db_session)
    acc = await _make_account(db_session, plan="professional")
    await _make_payment(db_session, acc, plan="professional", amount=349.00)

    resp = await client.get(
        "/api/site-admin/payments?plan=professional",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    body = resp.json()
    rows = body["data"]
    # All returned rows must be Professional
    for row in rows:
        assert row["plan"] == "Professional"


# ── T-04: filter by source ────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t04_filter_by_source_admin(client: AsyncClient, db_session: AsyncSession):
    """T-04: ?source=admin returns only admin-source rows."""
    token = await _site_admin_token(db_session)
    acc = await _make_account(db_session)
    await _make_payment(db_session, acc, source="admin")

    resp = await client.get(
        "/api/site-admin/payments?source=admin",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    for row in resp.json()["data"]:
        assert row["source"] == "admin"


# ── T-05: full-text search ────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t05_search_by_org_name(client: AsyncClient, db_session: AsyncSession):
    """T-05: ?q=zzzunique returns rows whose org name matches."""
    token = await _site_admin_token(db_session)
    acc = await _make_account(db_session, suffix="zzzunique")
    await _make_payment(db_session, acc)

    resp = await client.get(
        "/api/site-admin/payments?q=zzzunique",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    rows = resp.json()["data"]
    assert len(rows) >= 1
    assert all("zzzunique" in row["organization"].lower() or "zzzunique" in row["email"].lower() for row in rows)


# ── T-06: pagination ──────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t06_pagination_page2(client: AsyncClient, db_session: AsyncSession):
    """T-06: ?page=2&page_size=5 returns page=2 and page_size=5 in response."""
    token = await _site_admin_token(db_session)
    # Create 6 payments so page 2 is reachable
    for i in range(6):
        acc = await _make_account(db_session, suffix=f"pg{i}")
        await _make_payment(db_session, acc)

    resp = await client.get(
        "/api/site-admin/payments?page=2&page_size=5",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["page"] == 2
    assert body["page_size"] == 5


# ── T-07: CSV export ──────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t07_csv_export_content_type(client: AsyncClient, db_session: AsyncSession):
    """T-07: GET /export-csv returns text/csv with correct header row."""
    token = await _site_admin_token(db_session)
    acc = await _make_account(db_session)
    await _make_payment(db_session, acc)

    resp = await client.get(
        "/api/site-admin/payments/export-csv",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    assert "text/csv" in resp.headers.get("content-type", "")
    lines = resp.text.strip().splitlines()
    assert lines[0] == "Organization,Email,Plan,Amount (CAD),Method,Invoice,Source,Date"


# ── T-08: POST record payment ─────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t08_post_record_payment(client: AsyncClient, db_session: AsyncSession):
    """T-08: POST /payments with valid body returns 201 with payment id."""
    token = await _site_admin_token(db_session)
    acc = await _make_account(db_session)

    payload = {
        "account_id": str(acc.id),
        "plan": "professional",
        "amount_cad": 349.00,
        "method": "Wire transfer",
        "invoice_no": "INV-TEST-001",
        "source": "admin",
        "status": "paid",
        "payment_date": date.today().isoformat(),
    }
    resp = await client.post(
        "/api/site-admin/payments",
        json=payload,
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 201
    body = resp.json()
    assert body["success"] is True
    assert "id" in body["data"]


# ── T-09: 403 for non-site_admin ─────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t09_list_payments_administrator_gets_403(client: AsyncClient, db_session: AsyncSession):
    """T-09: GET /payments as administrator role returns 403."""
    acc = await _make_account(db_session)
    token = await _admin_token(db_session, acc)
    resp = await client.get(
        "/api/site-admin/payments",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 403


# ── T-10: 401 for unauthenticated ────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t10_kpis_unauthenticated_returns_401(client: AsyncClient, db_session: AsyncSession):
    """T-10: GET /payments/kpis without auth returns 401."""
    resp = await client.get("/api/site-admin/payments/kpis")
    assert resp.status_code == 401


# ── T-11: invalid plan enum ───────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t11_invalid_plan_returns_422(client: AsyncClient, db_session: AsyncSession):
    """T-11: ?plan=invalid_plan returns 422 validation error."""
    token = await _site_admin_token(db_session)
    resp = await client.get(
        "/api/site-admin/payments?plan=invalid_plan",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 422


# ── T-12: page_size exceeds max ───────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t12_page_size_200_returns_422(client: AsyncClient, db_session: AsyncSession):
    """T-12: ?page_size=200 returns 422 (exceeds max 100)."""
    token = await _site_admin_token(db_session)
    resp = await client.get(
        "/api/site-admin/payments?page_size=200",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 422


# ── T-13: this_month KPI isolation ────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t13_this_month_kpi_isolates_to_current_month(client: AsyncClient, db_session: AsyncSession):
    """T-13: this_month KPI sums only current-calendar-month payments."""
    token = await _site_admin_token(db_session)
    now = datetime.now(timezone.utc)
    acc = await _make_account(db_session)

    # Payment this month
    await _make_payment(
        db_session, acc, amount=500.00,
        payment_date=date(now.year, now.month, 1),
    )
    # Payment last month (different year-month)
    last_month = date(now.year, now.month - 1, 1) if now.month > 1 else date(now.year - 1, 12, 1)
    await _make_payment(db_session, acc, amount=999.00, payment_date=last_month)

    resp = await client.get(
        "/api/site-admin/payments/kpis",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    this_month = resp.json()["data"]["this_month"]
    # Should include the 500.00, but the 999.00 is from last month
    assert this_month >= 500.00
    # Must NOT include the last-month payment in this_month
    total_received = resp.json()["data"]["total_received"]
    assert total_received >= 1499.00


# ── T-14: MRR only active subscriptions ──────────────────────────────────────


@pytest.mark.asyncio
async def test_t14_mrr_counts_only_active_subscriptions(client: AsyncClient, db_session: AsyncSession):
    """T-14: MRR includes only accounts with latest subscription status=active."""
    token = await _site_admin_token(db_session)
    acc_active = await _make_account(db_session, suffix="mrr-active", plan="professional")
    acc_inactive = await _make_account(db_session, suffix="mrr-inactive", plan="starter")

    # Active subscription
    db_session.add(Subscription(
        account_id=acc_active.id,
        plan="professional",
        status="active",
        amount_cad=Decimal("349.00"),
    ))
    # Inactive subscription
    db_session.add(Subscription(
        account_id=acc_inactive.id,
        plan="starter",
        status="cancelled",
        amount_cad=Decimal("149.00"),
    ))
    await db_session.flush()

    resp = await client.get(
        "/api/site-admin/payments/kpis",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    mrr = resp.json()["data"]["mrr"]
    # At minimum the active professional plan contributes 349
    assert mrr >= 349.0


# ── T-15: CSV empty dataset ───────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t15_csv_no_match_returns_header_only(client: AsyncClient, db_session: AsyncSession):
    """T-15: CSV export with ?q=nomatch returns header row only."""
    token = await _site_admin_token(db_session)
    resp = await client.get(
        "/api/site-admin/payments/export-csv?q=zzz-nomatch-xyz-999",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    lines = [line for line in resp.text.strip().splitlines() if line.strip()]
    assert len(lines) == 1  # header only


# ── T-16: PDF download — 200 + application/pdf ───────────────────────────────


def _pdf_text(pdf_bytes: bytes) -> str:
    """Decompress all FlateDecode content streams in a PDF and return joined text."""
    import zlib
    import re

    parts = []
    for m in re.finditer(rb"stream\r?\n(.+?)\r?\nendstream", pdf_bytes, re.DOTALL):
        raw = m.group(1)
        try:
            parts.append(zlib.decompress(raw).decode("latin-1", errors="replace"))
        except Exception:
            parts.append(raw.decode("latin-1", errors="replace"))
    return "\n".join(parts)


@pytest.mark.asyncio
async def test_t16_invoice_pdf_returns_pdf_bytes(client: AsyncClient, db_session: AsyncSession):
    """T-16: GET /payments/{id}/invoice-pdf returns 200, application/pdf, non-empty."""
    token = await _site_admin_token(db_session)
    acc = await _make_account(db_session)
    payment = await _make_payment(db_session, acc, invoice_no="INV-TEST-PDF-001")

    resp = await client.get(
        f"/api/site-admin/payments/{payment.id}/invoice-pdf",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    assert "application/pdf" in resp.headers.get("content-type", "")
    assert len(resp.content) > 500
    assert resp.content[:4] == b"%PDF"
    assert "indelis-invoice-" in resp.headers.get("content-disposition", "").lower()


# ── T-17: PDF content contains key fields ────────────────────────────────────


@pytest.mark.asyncio
async def test_t17_pdf_contains_invoice_fields(client: AsyncClient, db_session: AsyncSession):
    """T-17: PDF decompressed content contains invoice number, plan, and amount."""
    token = await _site_admin_token(db_session)
    acc = await _make_account(db_session, suffix="pdfcheck")
    payment = await _make_payment(
        db_session, acc,
        plan="professional",
        amount=349.00,
        invoice_no="INV-PDF-VERIFY",
    )

    resp = await client.get(
        f"/api/site-admin/payments/{payment.id}/invoice-pdf",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    assert resp.content[:4] == b"%PDF"
    text = _pdf_text(resp.content)
    assert "INV-PDF-VERIFY" in text
    assert "349" in text
    assert "Professional" in text


# ── T-18: PDF 404 for missing payment ────────────────────────────────────────


@pytest.mark.asyncio
async def test_t18_invoice_pdf_not_found(client: AsyncClient, db_session: AsyncSession):
    """T-18: GET /payments/{id}/invoice-pdf for non-existent id returns 404."""
    token = await _site_admin_token(db_session)
    resp = await client.get(
        f"/api/site-admin/payments/{uuid4()}/invoice-pdf",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 404


# ── T-19: PDF 403 for non-site_admin ─────────────────────────────────────────


@pytest.mark.asyncio
async def test_t19_invoice_pdf_non_admin_gets_403(client: AsyncClient, db_session: AsyncSession):
    """T-19: GET /payments/{id}/invoice-pdf as administrator returns 403."""
    acc = await _make_account(db_session)
    token = await _admin_token(db_session, acc)
    payment = await _make_payment(db_session, acc)

    resp = await client.get(
        f"/api/site-admin/payments/{payment.id}/invoice-pdf",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 403


# ── T-20: PDF paid stamp ──────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t20_pdf_paid_contains_paid_text(client: AsyncClient, db_session: AsyncSession):
    """T-20: PDF for status='paid' contains 'PAID' in decompressed stream."""
    token = await _site_admin_token(db_session)
    acc = await _make_account(db_session)
    payment = await _make_payment(db_session, acc, status="paid")

    resp = await client.get(
        f"/api/site-admin/payments/{payment.id}/invoice-pdf",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    assert "PAID" in _pdf_text(resp.content)


# ── T-21: PDF pending stamp ───────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_t21_pdf_pending_contains_pending_text(client: AsyncClient, db_session: AsyncSession):
    """T-21: PDF for status='pending' contains 'PENDING' in decompressed stream."""
    token = await _site_admin_token(db_session)
    acc = await _make_account(db_session)
    payment = await _make_payment(db_session, acc, status="pending")

    resp = await client.get(
        f"/api/site-admin/payments/{payment.id}/invoice-pdf",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 200
    assert "PENDING" in _pdf_text(resp.content)
