"""
Tests for INDL-19 — Billing & Subscription Module.
Covers:
  T-01  GET /subscription returns plan data for administrator
  T-02  GET /subscription 403 for staff role
  T-03  GET /subscription 403 for manager role
  T-04  GET /subscription fields shape
  T-05  PATCH /subscription/plan updates plan (no Stripe, no subscription record)
  T-06  PATCH /subscription/plan creates Subscription record when needed
  T-07  PATCH /subscription/plan 422 for invalid plan
  T-08  PATCH /subscription/plan 403 for staff
  T-09  PATCH /subscription/plan calls Stripe when stripe_subscription_id set
  T-10  GET /invoices returns paginated billing invoice list
  T-11  GET /invoices empty list for tenant with no invoices
  T-12  GET /invoices filter by status=paid
  T-13  GET /invoices pagination works (page_size=2)
  T-14  GET /invoices/export returns CSV with correct headers
  T-15  POST /invoices/{id}/payment-intent 201 for failed invoice (Stripe mocked)
  T-16  POST /invoices/{id}/payment-intent 403 for non-failed invoice
  T-17  POST /invoices/{id}/payment-intent 404 for non-existent invoice
  T-18  POST /invoices/{id}/confirm-payment marks invoice paid (Stripe mocked)
  T-19  POST /invoices/{id}/confirm-payment error when intent not succeeded
  T-20  GET /invoices/{id}/pdf returns PDF bytes
  T-21  GET /invoices/{id}/pdf 404 for non-existent invoice
"""
import pytest
from decimal import Decimal
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

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

from src.apps.tenants.models.account import Account
from src.apps.tenants.models.subscription import Subscription
from src.apps.billing.models.invoice import Invoice
from src.apps.auth.models.user import User
from src.core.security import hash_password, create_access_token, build_token_payload


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

async def _make_account(db: AsyncSession, *, plan: str = "professional") -> Account:
    uid = str(uuid4())[-8:]
    acc = Account(
        organization_name=f"Billing Cemetery {uid}",
        subdomain=f"billing-{uid}",
        contact_email=f"billing-{uid}@test.ca",
        plan=plan,
        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"user-{role}-{uuid4().hex[:6]}@test.ca",
        password_hash=hash_password("Test1234!"),
        first_name="Test",
        last_name="User",
        role=role,
        status="active",
    )
    db.add(user)
    await db.flush()
    token = create_access_token(build_token_payload(user, account))
    return token


async def _make_subscription(
    db: AsyncSession,
    account: Account,
    *,
    plan: str = "professional",
    stripe_sub_id: str = None,
) -> Subscription:
    from datetime import datetime, timezone, timedelta

    sub = Subscription(
        account_id=account.id,
        plan=plan,
        status="active",
        amount_cad=Decimal("349.00"),
        billing_cycle="monthly",
        current_period_start=datetime.now(timezone.utc),
        current_period_end=datetime.now(timezone.utc) + timedelta(days=30),
        stripe_subscription_id=stripe_sub_id,
    )
    db.add(sub)
    await db.flush()
    return sub


async def _make_invoice(
    db: AsyncSession,
    account: Account,
    *,
    status: str = "paid",
    total: float = 349.00,
    inv_number: str = None,
) -> Invoice:
    uid = str(uuid4())[-6:]
    inv = Invoice(
        tenant_id=account.id,
        invoice_number=inv_number or f"INV-2026-{uid}",
        status=status,
        total_amount=Decimal(str(total)),
        paid_amount=Decimal(str(total)) if status == "paid" else Decimal("0"),
        balance_due=Decimal("0") if status == "paid" else Decimal(str(total)),
    )
    db.add(inv)
    await db.flush()
    return inv


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


# ── T-01: GET /subscription returns plan data for administrator ───────────────

@pytest.mark.asyncio
async def test_get_subscription_administrator(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="professional")
    token = await _make_user(db_session, acc, role="administrator")
    await _make_subscription(db_session, acc, plan="professional")

    resp = await client.get(
        "/api/v1/billing/subscription",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200
    data = resp.json()["data"]
    assert data["plan_name"] == "Professional"
    assert data["plan_slug"] == "professional"
    assert data["price_monthly"] == 349
    assert data["currency"] == "CAD"


# ── T-02: GET /subscription 403 for staff role ────────────────────────────────

@pytest.mark.asyncio
async def test_get_subscription_staff_forbidden(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="staff")

    resp = await client.get(
        "/api/v1/billing/subscription",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 403


# ── T-03: GET /subscription 403 for manager role ─────────────────────────────

@pytest.mark.asyncio
async def test_get_subscription_manager_forbidden(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="manager")

    resp = await client.get(
        "/api/v1/billing/subscription",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 403


# ── T-04: GET /subscription fields shape ─────────────────────────────────────

@pytest.mark.asyncio
async def test_get_subscription_fields_shape(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="starter")
    token = await _make_user(db_session, acc, role="administrator")

    resp = await client.get(
        "/api/v1/billing/subscription",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200
    data = resp.json()["data"]
    for field in ("plan_name", "plan_slug", "price_monthly", "currency", "status"):
        assert field in data, f"Missing field: {field}"
    assert data["plan_name"] == "Starter"
    assert data["price_monthly"] == 149


# ── T-05: PATCH /subscription/plan updates plan without Stripe ───────────────

@pytest.mark.asyncio
async def test_update_plan_no_stripe(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="starter")
    token = await _make_user(db_session, acc, role="administrator")

    resp = await client.patch(
        "/api/v1/billing/subscription/plan",
        json={"plan": "professional"},
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200
    data = resp.json()["data"]
    assert data["plan_slug"] == "professional"
    assert data["plan_name"] == "Professional"
    assert data["price_monthly"] == 349

    await db_session.refresh(acc)
    assert acc.plan == "professional"


# ── T-06: PATCH /subscription/plan creates subscription row ──────────────────

@pytest.mark.asyncio
async def test_update_plan_updates_existing_subscription(
    client: AsyncClient, db_session: AsyncSession
):
    acc = await _make_account(db_session, plan="starter")
    token = await _make_user(db_session, acc, role="administrator")
    sub = await _make_subscription(db_session, acc, plan="starter")

    resp = await client.patch(
        "/api/v1/billing/subscription/plan",
        json={"plan": "enterprise"},
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200
    data = resp.json()["data"]
    assert data["plan_slug"] == "enterprise"
    assert data["price_monthly"] == 749

    await db_session.refresh(sub)
    assert sub.plan == "enterprise"
    assert float(sub.amount_cad) == 749.0


# ── T-07: PATCH /subscription/plan 422 for invalid plan ──────────────────────

@pytest.mark.asyncio
async def test_update_plan_invalid_plan(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")

    resp = await client.patch(
        "/api/v1/billing/subscription/plan",
        json={"plan": "ultimate"},
        headers=_headers(token, acc),
    )
    assert resp.status_code in (400, 422)


# ── T-08: PATCH /subscription/plan 403 for staff ─────────────────────────────

@pytest.mark.asyncio
async def test_update_plan_staff_forbidden(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="staff")

    resp = await client.patch(
        "/api/v1/billing/subscription/plan",
        json={"plan": "enterprise"},
        headers=_headers(token, acc),
    )
    assert resp.status_code == 403


# ── T-09: PATCH /subscription/plan calls Stripe (mocked) ─────────────────────

@pytest.mark.asyncio
async def test_update_plan_calls_stripe(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="professional")
    token = await _make_user(db_session, acc, role="administrator")
    await _make_subscription(db_session, acc, plan="professional", stripe_sub_id="sub_test_123")

    mock_stripe_sub = MagicMock()
    mock_stripe_sub.__getitem__ = lambda self, key: {"items": {"data": [{"id": "si_test_item"}]}}[key]

    with (
        patch("stripe.Subscription.retrieve", return_value=mock_stripe_sub),
        patch("stripe.Subscription.modify") as mock_modify,
    ):
        resp = await client.patch(
            "/api/v1/billing/subscription/plan",
            json={"plan": "enterprise"},
            headers=_headers(token, acc),
        )

    assert resp.status_code == 200
    mock_modify.assert_called_once()
    call_kwargs = mock_modify.call_args
    assert "proration_behavior" in call_kwargs.kwargs or any(
        "proration_behavior" in str(a) for a in call_kwargs.args
    )


# ── T-10: GET /invoices returns paginated billing invoice list ────────────────

@pytest.mark.asyncio
async def test_list_invoices_returns_billing_shape(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")
    await _make_invoice(db_session, acc, status="paid", inv_number="INV-2026-06")
    await _make_invoice(db_session, acc, status="failed", inv_number="INV-2026-07")

    resp = await client.get(
        "/api/v1/billing/invoices",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200
    body = resp.json()
    items = body["data"]
    assert isinstance(items, list)
    assert len(items) >= 2
    item = items[0]
    for field in ("id", "invoice_number", "date", "plan_name", "amount", "currency", "status"):
        assert field in item, f"Missing field: {field}"
    assert item["currency"] == "CAD"


# ── T-11: GET /invoices empty list for new tenant ────────────────────────────

@pytest.mark.asyncio
async def test_list_invoices_empty(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")

    resp = await client.get(
        "/api/v1/billing/invoices",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200
    assert resp.json()["data"] == []


# ── T-12: GET /invoices filter by status=paid ────────────────────────────────

@pytest.mark.asyncio
async def test_list_invoices_filter_status(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")
    await _make_invoice(db_session, acc, status="paid", inv_number="INV-PAID-01")
    await _make_invoice(db_session, acc, status="failed", inv_number="INV-FAIL-01")

    resp = await client.get(
        "/api/v1/billing/invoices?status=paid",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200
    items = resp.json()["data"]
    assert all(i["status"] == "paid" for i in items)
    numbers = [i["invoice_number"] for i in items]
    assert "INV-PAID-01" in numbers
    assert "INV-FAIL-01" not in numbers


# ── T-13: GET /invoices pagination ───────────────────────────────────────────

@pytest.mark.asyncio
async def test_list_invoices_pagination(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")
    for i in range(5):
        await _make_invoice(db_session, acc, status="paid", inv_number=f"INV-P-{i:03d}")

    resp = await client.get(
        "/api/v1/billing/invoices?page=1&page_size=2",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200
    body = resp.json()
    assert len(body["data"]) == 2
    assert body["total"] >= 5
    assert body["pages"] >= 3


# ── T-14: GET /invoices/export returns CSV ───────────────────────────────────

@pytest.mark.asyncio
async def test_export_invoices_csv(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="professional")
    token = await _make_user(db_session, acc, role="administrator")
    await _make_invoice(db_session, acc, status="paid", inv_number="INV-CSV-01")

    resp = await client.get(
        "/api/v1/billing/invoices/export",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200
    assert "text/csv" in resp.headers["content-type"]
    text = resp.text
    header_line = text.splitlines()[0]
    assert "Invoice" in header_line
    assert "Date" in header_line
    assert "Plan" in header_line
    assert "Amount" in header_line
    assert "Status" in header_line
    assert "INV-CSV-01" in text


# ── T-15: POST /invoices/{id}/payment-intent for failed invoice (mocked) ─────

@pytest.mark.asyncio
async def test_create_payment_intent(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")
    inv = await _make_invoice(db_session, acc, status="failed", total=349.00)

    mock_intent = MagicMock()
    mock_intent.id = "pi_test_12345"
    mock_intent.client_secret = "pi_test_12345_secret_xyz"

    with patch("stripe.PaymentIntent.create", return_value=mock_intent):
        resp = await client.post(
            f"/api/v1/billing/invoices/{inv.id}/payment-intent",
            headers=_headers(token, acc),
        )

    assert resp.status_code == 201
    data = resp.json()["data"]
    assert "client_secret" in data
    assert data["client_secret"] == "pi_test_12345_secret_xyz"


# ── T-16: POST /invoices/{id}/payment-intent 403 for non-failed invoice ──────

@pytest.mark.asyncio
async def test_create_payment_intent_not_failed(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")
    inv = await _make_invoice(db_session, acc, status="paid")

    resp = await client.post(
        f"/api/v1/billing/invoices/{inv.id}/payment-intent",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 403


# ── T-17: POST /invoices/{id}/payment-intent 404 for missing invoice ─────────

@pytest.mark.asyncio
async def test_create_payment_intent_not_found(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")

    resp = await client.post(
        f"/api/v1/billing/invoices/{uuid4()}/payment-intent",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 404


# ── T-18: POST /invoices/{id}/confirm-payment marks invoice paid ──────────────

@pytest.mark.asyncio
async def test_confirm_payment_marks_paid(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")
    inv = await _make_invoice(db_session, acc, status="failed", total=349.00)

    mock_intent = MagicMock()
    mock_intent.status = "succeeded"

    with patch("stripe.PaymentIntent.retrieve", return_value=mock_intent):
        resp = await client.post(
            f"/api/v1/billing/invoices/{inv.id}/confirm-payment",
            json={"payment_intent_id": "pi_test_123"},
            headers=_headers(token, acc),
        )

    assert resp.status_code == 200
    data = resp.json()["data"]
    assert data["status"] == "paid"
    assert data["invoice_id"] == str(inv.id)

    await db_session.refresh(inv)
    assert inv.status == "paid"
    assert inv.balance_due == Decimal("0")


# ── T-19: POST /invoices/{id}/confirm-payment error when not succeeded ────────

@pytest.mark.asyncio
async def test_confirm_payment_intent_not_succeeded(
    client: AsyncClient, db_session: AsyncSession
):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")
    inv = await _make_invoice(db_session, acc, status="failed", total=349.00)

    mock_intent = MagicMock()
    mock_intent.status = "requires_payment_method"

    with patch("stripe.PaymentIntent.retrieve", return_value=mock_intent):
        resp = await client.post(
            f"/api/v1/billing/invoices/{inv.id}/confirm-payment",
            json={"payment_intent_id": "pi_test_fail"},
            headers=_headers(token, acc),
        )

    assert resp.status_code in (400, 422)


# ── T-20: GET /invoices/{id}/pdf returns PDF bytes ────────────────────────────

@pytest.mark.asyncio
async def test_download_invoice_pdf(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")
    inv = await _make_invoice(db_session, acc, status="paid", inv_number="INV-PDF-01")

    resp = await client.get(
        f"/api/v1/billing/invoices/{inv.id}/pdf",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 200
    assert "application/pdf" in resp.headers["content-type"]
    assert resp.content[:4] == b"%PDF"  # PDF magic bytes
    assert len(resp.content) > 500  # non-trivial PDF


# ── T-21: GET /invoices/{id}/pdf 404 for non-existent invoice ────────────────

@pytest.mark.asyncio
async def test_download_pdf_not_found(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session)
    token = await _make_user(db_session, acc, role="administrator")

    resp = await client.get(
        f"/api/v1/billing/invoices/{uuid4()}/pdf",
        headers=_headers(token, acc),
    )
    assert resp.status_code == 404
