"""
The Record Payment modal (and the underlying POST /invoices/{id}/payment
endpoint) silently accepted an amount greater than the invoice's outstanding
balance — InvoiceService.record_payment clamped balance_due to 0 via
max(0, new_balance) with no error, so an accidental overpayment just quietly
marked the invoice "paid" and discarded the excess with no record of it.
Fixed by rejecting amount > balance_due before any state changes.
"""
from __future__ import annotations

from decimal import Decimal

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

from src.apps.billing.models.invoice import Invoice

INVOICES_URL = "/api/v1/billing/invoices"


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


async def _make_invoice(db: AsyncSession, tenant_id, *, total="1000.00", paid="0.00") -> Invoice:
    total_d = Decimal(total)
    paid_d = Decimal(paid)
    inv = Invoice(
        tenant_id=tenant_id,
        invoice_number=f"INV-CAP-{id(db)}-{total}-{paid}",
        status="outstanding" if paid_d == 0 else "partial",
        total_amount=total_d,
        paid_amount=paid_d,
        balance_due=total_d - paid_d,
    )
    db.add(inv)
    await db.flush()
    return inv


@pytest.mark.asyncio
async def test_payment_over_balance_is_rejected(
    client: AsyncClient, db_session: AsyncSession, admin_token: str, test_account,
):
    """POST /invoices/{id}/payment with amount > balance_due → 422, invoice untouched."""
    test_account.plan = "professional"  # billing routes are gated to professional/enterprise
    await db_session.flush()
    inv = await _make_invoice(db_session, test_account.id, total="2000.00", paid="0.00")

    resp = await client.post(
        f"{INVOICES_URL}/{inv.id}/payment",
        json={
            "amount": 2500,
            "payment_date": "2026-08-25",
            "payment_method": "Cash",
        },
        headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 422
    assert "outstanding balance" in resp.json()["message"].lower()

    result = await db_session.execute(select(Invoice).where(Invoice.id == inv.id))
    refreshed = result.scalar_one()
    assert refreshed.balance_due == Decimal("2000.00")
    assert refreshed.paid_amount == Decimal("0.00")
    assert refreshed.status == "outstanding"


@pytest.mark.asyncio
async def test_payment_equal_to_balance_is_accepted(
    client: AsyncClient, db_session: AsyncSession, admin_token: str, test_account,
):
    """POST /invoices/{id}/payment with amount == balance_due (full payoff) → 201, marked paid."""
    test_account.plan = "professional"  # billing routes are gated to professional/enterprise
    await db_session.flush()
    inv = await _make_invoice(db_session, test_account.id, total="2000.00", paid="0.00")

    resp = await client.post(
        f"{INVOICES_URL}/{inv.id}/payment",
        json={
            "amount": 2000,
            "payment_date": "2026-08-25",
            "payment_method": "Cash",
        },
        headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 201, resp.text
    data = resp.json()["data"]
    assert Decimal(str(data["balance_due"])) == Decimal("0.00")
    assert data["status"] == "paid"


@pytest.mark.asyncio
async def test_partial_payment_under_balance_is_accepted(
    client: AsyncClient, db_session: AsyncSession, admin_token: str, test_account,
):
    """POST /invoices/{id}/payment with amount < balance_due → 201, marked partial with correct remainder."""
    test_account.plan = "professional"  # billing routes are gated to professional/enterprise
    await db_session.flush()
    inv = await _make_invoice(db_session, test_account.id, total="2000.00", paid="0.00")

    resp = await client.post(
        f"{INVOICES_URL}/{inv.id}/payment",
        json={
            "amount": 500,
            "payment_date": "2026-08-25",
            "payment_method": "Cash",
        },
        headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 201, resp.text
    data = resp.json()["data"]
    assert Decimal(str(data["balance_due"])) == Decimal("1500.00")
    assert data["status"] == "partial"
