"""A split-payment contract's still-unpaid instalment must be cancelled once
the plot it reserved is released back to Available — the deal fell through
(Mark as Lost) or staff manually reverted the plot from the Cemetery Map /
Edit Plot page — so the customer is never left owing on a sale that no
longer exists. Never touches an invoice that's already fully paid.
"""
from __future__ import annotations

import uuid

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

from src.apps.billing.models.invoice import Invoice
from src.apps.plots.models.plot import Plot
from src.apps.records.models.burial_info import BurialInfo
from src.apps.records.models.record import Record
from src.apps.sales.models.opportunity import Opportunity
from src.apps.sections.models.section import Section

pytestmark = pytest.mark.asyncio

CONTRACTS_URL = "/api/v1/sales/contracts"
INVOICES_URL = "/api/v1/billing/invoices"
OPPS_URL = "/api/v1/sales/opportunities"
PLOTS_URL = "/api/v1/plots"

FAKE_SIG = (
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk"
    "+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
)


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


async def _make_plot(db: AsyncSession, account, status: str = "hold") -> Plot:
    ref = f"P-{uuid.uuid4().hex[:6]}"
    section = Section(tenant_id=account.id, code=ref[0], name=f"Section {ref[0]}")
    db.add(section)
    await db.flush()
    plot = Plot(tenant_id=account.id, plot_ref=ref, section_id=section.id, status=status)
    db.add(plot)
    await db.flush()
    return plot


async def _create_contract(
    client, token, tenant_id, plot_id, *, payment_plan_type="deposit_50", unit_price="2000.00",
) -> dict:
    resp = await client.post(
        CONTRACTS_URL,
        json={
            "plot_id": str(plot_id),
            "contract_type": "pre_need",
            "purchaser_name": "Mary McLeod",
            "purchaser_email": "buyer@example.ca",
            "payment_plan_type": payment_plan_type,
            "line_items": [
                {"description": "Burial plot", "quantity": 1, "unit_price": unit_price},
            ],
        },
        headers=_headers(token, tenant_id),
    )
    assert resp.status_code == 201, resp.text
    return resp.json()["data"]


async def _issue_contract(client, token, tenant_id, contract_id) -> dict:
    resp = await client.patch(
        f"{CONTRACTS_URL}/{contract_id}/issue", headers=_headers(token, tenant_id)
    )
    assert resp.status_code == 200, resp.text
    return resp.json()["data"]


async def _sign_contract(client, token, tenant_id, contract_id) -> None:
    resp = await client.post(
        f"{CONTRACTS_URL}/{contract_id}/sign",
        json={"purchaser_signature_b64": FAKE_SIG},
        headers=_headers(token, tenant_id),
    )
    assert resp.status_code == 200, resp.text


async def _pay_invoice_in_full(client, token, tenant_id, invoice_id, amount) -> None:
    resp = await client.post(
        f"{INVOICES_URL}/{invoice_id}/payment",
        json={"amount": str(amount), "payment_date": "2026-08-25", "payment_method": "Cash"},
        headers=_headers(token, tenant_id),
    )
    assert resp.status_code == 201, resp.text


async def _invoice(db: AsyncSession, invoice_id) -> Invoice:
    result = await db.execute(select(Invoice).where(Invoice.id == invoice_id))
    return result.scalar_one()


async def _plot_status(db: AsyncSession, plot_id) -> str:
    result = await db.execute(select(Plot).where(Plot.id == plot_id))
    return result.scalar_one().status


async def test_mark_lost_cancels_the_unpaid_second_instalment_of_a_reserved_plot(
    client, admin_token: str, test_account, db_session: AsyncSession,
):
    plot = await _make_plot(db_session, test_account, status="hold")
    contract = await _create_contract(client, admin_token, test_account.id, plot.id)
    issued = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    await _sign_contract(client, admin_token, test_account.id, contract["id"])

    invoices_created = issued["invoices_created"]
    assert len(invoices_created) == 2
    first_invoice_id, second_invoice_id = invoices_created[0]["id"], invoices_created[1]["id"]

    # Pay off the deposit (50%) — this is what promotes the plot to reserved.
    await _pay_invoice_in_full(
        client, admin_token, test_account.id, first_invoice_id, invoices_created[0]["amount"],
    )
    assert await _plot_status(db_session, plot.id) == "reserved"

    opp = Opportunity(
        tenant_id=test_account.id, plot_id=plot.id, stage="contract_sent", family_name="Mary McLeod",
    )
    db_session.add(opp)
    await db_session.flush()

    resp = await client.post(
        f"{OPPS_URL}/{opp.id}/mark-lost",
        json={"loss_reason": "timing"},
        headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 200, resp.text

    assert await _plot_status(db_session, plot.id) == "vacant"

    first = await _invoice(db_session, first_invoice_id)
    assert first.status == "paid"  # untouched — the customer already paid this one

    second = await _invoice(db_session, second_invoice_id)
    assert second.status == "canceled"
    assert second.balance_due == 0


async def test_manually_reverting_a_reserved_plot_to_available_cancels_the_unpaid_invoice(
    client, admin_token: str, test_account, db_session: AsyncSession,
):
    """Same cascade, triggered from the Cemetery Map / Edit Plot page instead
    of Mark as Lost — reserved -> vacant is an allowed transition
    (PLOT_STATUS_TRANSITIONS) and carries the same "sale fell through"
    business meaning regardless of which side of the app drove it."""
    plot = await _make_plot(db_session, test_account, status="hold")
    contract = await _create_contract(client, admin_token, test_account.id, plot.id)
    issued = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    await _sign_contract(client, admin_token, test_account.id, contract["id"])

    invoices_created = issued["invoices_created"]
    first_invoice_id, second_invoice_id = invoices_created[0]["id"], invoices_created[1]["id"]
    await _pay_invoice_in_full(
        client, admin_token, test_account.id, first_invoice_id, invoices_created[0]["amount"],
    )
    assert await _plot_status(db_session, plot.id) == "reserved"

    resp = await client.patch(
        f"{PLOTS_URL}/{plot.id}", json={"status": "vacant"}, headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 200, resp.text

    second = await _invoice(db_session, second_invoice_id)
    assert second.status == "canceled"
    assert second.balance_due == 0


async def test_mark_lost_cancels_a_single_unpaid_invoice_on_a_held_plot(
    client, admin_token: str, test_account, db_session: AsyncSession,
):
    """Generalizes beyond deposit_50 — a plain 'full' payment plan contract
    (plot stays on hold until fully paid) whose invoice was never paid at all
    must also be cancelled when the deal is lost."""
    plot = await _make_plot(db_session, test_account, status="hold")
    contract = await _create_contract(
        client, admin_token, test_account.id, plot.id, payment_plan_type="full",
    )
    issued = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    await _sign_contract(client, admin_token, test_account.id, contract["id"])
    assert await _plot_status(db_session, plot.id) == "hold"

    invoice_id = issued["invoices_created"][0]["id"]

    opp = Opportunity(
        tenant_id=test_account.id, plot_id=plot.id, stage="contract_sent", family_name="Mary McLeod",
    )
    db_session.add(opp)
    await db_session.flush()

    resp = await client.post(
        f"{OPPS_URL}/{opp.id}/mark-lost",
        json={"loss_reason": "timing"},
        headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 200, resp.text

    assert await _plot_status(db_session, plot.id) == "vacant"
    invoice = await _invoice(db_session, invoice_id)
    assert invoice.status == "canceled"


async def test_cannot_record_a_payment_against_a_cancelled_invoice(
    client, admin_token: str, test_account, db_session: AsyncSession,
):
    plot = await _make_plot(db_session, test_account, status="hold")
    contract = await _create_contract(client, admin_token, test_account.id, plot.id)
    issued = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    await _sign_contract(client, admin_token, test_account.id, contract["id"])

    invoices_created = issued["invoices_created"]
    first_invoice_id, second_invoice_id = invoices_created[0]["id"], invoices_created[1]["id"]
    await _pay_invoice_in_full(
        client, admin_token, test_account.id, first_invoice_id, invoices_created[0]["amount"],
    )

    opp = Opportunity(
        tenant_id=test_account.id, plot_id=plot.id, stage="contract_sent", family_name="Mary McLeod",
    )
    db_session.add(opp)
    await db_session.flush()
    resp = await client.post(
        f"{OPPS_URL}/{opp.id}/mark-lost",
        json={"loss_reason": "timing"},
        headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 200, resp.text

    resp = await client.post(
        f"{INVOICES_URL}/{second_invoice_id}/payment",
        json={"amount": "10.00", "payment_date": "2026-08-25", "payment_method": "Cash"},
        headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 403, resp.text


async def test_mark_lost_from_partial_payment_received_releases_plot_deletes_record_and_cancels_invoice(
    client, admin_token: str, test_account, db_session: AsyncSession,
):
    """End-to-end reproduction of the Sales page flow: a family in the
    'Partial Payment Received' stage (50% paid, plot reserved, a pre-need
    record already on file) backs out — Mark as Lost must release the plot,
    remove the record, and cancel the unpaid second instalment, all at once."""
    plot = await _make_plot(db_session, test_account, status="hold")
    record = Record(
        tenant_id=test_account.id, plot_id=plot.id, first_name="Rajesh", last_name="Joshi",
        status="active",
    )
    db_session.add(record)
    await db_session.flush()
    db_session.add(BurialInfo(
        record_id=record.id, tenant_id=test_account.id, interment_type="pre_need_no_service_yet",
    ))
    await db_session.flush()

    contract = await _create_contract(client, admin_token, test_account.id, plot.id)
    issued = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    await _sign_contract(client, admin_token, test_account.id, contract["id"])

    invoices_created = issued["invoices_created"]
    first_invoice_id, second_invoice_id = invoices_created[0]["id"], invoices_created[1]["id"]
    await _pay_invoice_in_full(
        client, admin_token, test_account.id, first_invoice_id, invoices_created[0]["amount"],
    )
    assert await _plot_status(db_session, plot.id) == "reserved"

    opp = Opportunity(
        tenant_id=test_account.id, plot_id=plot.id, stage="partial_payment_received",
        family_name="Rajesh Joshi",
    )
    db_session.add(opp)
    await db_session.flush()

    resp = await client.post(
        f"{OPPS_URL}/{opp.id}/mark-lost",
        json={"loss_reason": "timing"},
        headers=_headers(admin_token, test_account.id),
    )
    assert resp.status_code == 200, resp.text
    assert resp.json()["data"]["stage"] == "lost"

    assert await _plot_status(db_session, plot.id) == "vacant"

    await db_session.refresh(record)
    assert record.deleted_at is not None

    second = await _invoice(db_session, second_invoice_id)
    assert second.status == "canceled"
    assert second.balance_due == 0

    first = await _invoice(db_session, first_invoice_id)
    assert first.status == "paid"
