"""
Plot status must track the sales pipeline: a plot is put "on hold" as soon
as a proposal for it goes out (so a second agent can't sell it while it's
under negotiation), and only promoted to "reserved" once the buyer has
signed the contract AND paid at least 50% of the total — signing alone is
not enough.
"""
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.plots.models.plot import Plot
from src.apps.sections.models.section import Section

PROPOSALS_URL = "/api/v1/sales/proposals"
CONTRACTS_URL = "/api/v1/sales/contracts"
INVOICES_URL = "/api/v1/billing/invoices"

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, ref="A-1", status="vacant") -> Plot:
    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 _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 _create_and_send_proposal(client, token, tenant_id, plot_id) -> dict:
    resp = await client.post(
        PROPOSALS_URL,
        json={
            "to_email": "family@example.ca",
            "subject": "Proposal",
            "cover_note": "Please review.",
            "plot_id": str(plot_id),
            "line_items": [
                {"description": "Burial plot", "quantity": 1, "unit_price": "2500.00"},
            ],
        },
        headers=_headers(token, tenant_id),
    )
    assert resp.status_code == 201, resp.text
    proposal = resp.json()["data"]
    send_resp = await client.post(
        f"{PROPOSALS_URL}/{proposal['id']}/send", headers=_headers(token, tenant_id)
    )
    assert send_resp.status_code == 200, send_resp.text
    return proposal


async def _create_contract(
    client, token, tenant_id, plot_id, *, 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": "full",
            "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(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


# ── Proposal sent → plot goes on hold ────────────────────────────────────────

@pytest.mark.asyncio
async def test_proposal_sent_puts_plot_on_hold(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    plot = await _make_plot(db_session, test_account)
    await _create_and_send_proposal(client, admin_token, test_account.id, plot.id)

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


# ── Contract signed, no/insufficient payment → plot stays on hold ───────────
# (issue → sign is the real order: a contract is sent out for the buyer to
# sign, not signed and then re-issued.)

@pytest.mark.asyncio
async def test_contract_signed_without_payment_holds_plot_not_reserves(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    plot = await _make_plot(db_session, test_account)
    contract = await _create_contract(client, admin_token, test_account.id, plot.id)
    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"


@pytest.mark.asyncio
async def test_contract_signed_with_under_50_percent_payment_stays_on_hold(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    plot = await _make_plot(db_session, test_account)
    contract = await _create_contract(
        client, admin_token, test_account.id, plot.id, unit_price="2000.00"
    )
    issued = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    await _sign_contract(client, admin_token, test_account.id, contract["id"])

    invoice_id = issued["invoices_created"][0]["id"]
    await _pay_invoice(client, admin_token, test_account.id, invoice_id, "500")

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


# ── Contract signed + >=50% paid → plot reserved ─────────────────────────────

@pytest.mark.asyncio
async def test_contract_signed_with_50_percent_payment_reserves_plot(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    plot = await _make_plot(db_session, test_account)
    contract = await _create_contract(
        client, admin_token, test_account.id, plot.id, unit_price="2000.00"
    )
    issued = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    await _sign_contract(client, admin_token, test_account.id, contract["id"])

    invoice = issued["invoices_created"][0]
    half = (Decimal(str(invoice["amount"])) / 2).quantize(Decimal("0.01"))
    await _pay_invoice(client, admin_token, test_account.id, invoice["id"], half)

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


@pytest.mark.asyncio
async def test_contract_signed_with_full_payment_reserves_plot(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    plot = await _make_plot(db_session, test_account)
    contract = await _create_contract(
        client, admin_token, test_account.id, plot.id, unit_price="1000.00"
    )
    issued = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    await _sign_contract(client, admin_token, test_account.id, contract["id"])

    invoice = issued["invoices_created"][0]
    await _pay_invoice(client, admin_token, test_account.id, invoice["id"], invoice["amount"])

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


@pytest.mark.asyncio
async def test_payment_recorded_before_signing_promotes_on_sign(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    """A buyer can pay the deposit link before actually signing — the plot
    should reserve as soon as signing happens, since the money was already
    there."""
    plot = await _make_plot(db_session, test_account)
    contract = await _create_contract(
        client, admin_token, test_account.id, plot.id, unit_price="1000.00"
    )
    issued = await _issue_contract(client, admin_token, test_account.id, contract["id"])
    invoice = issued["invoices_created"][0]
    await _pay_invoice(client, admin_token, test_account.id, invoice["id"], invoice["amount"])

    # Not signed yet — a payment on an unsigned contract doesn't touch the
    # plot at all (this contract wasn't created via a proposal, so nothing
    # has put it on hold yet either).
    assert await _plot_status(db_session, plot.id) == "vacant"

    await _sign_contract(client, admin_token, test_account.id, contract["id"])

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


# ── Sales-pipeline changes never downgrade a stronger existing status ───────

@pytest.mark.asyncio
async def test_signing_never_downgrades_an_occupied_plot(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    plot = await _make_plot(db_session, test_account, status="occupied")
    contract = await _create_contract(client, admin_token, test_account.id, plot.id)
    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) == "occupied"


@pytest.mark.asyncio
async def test_signing_an_already_reserved_plot_stays_reserved(
    client: AsyncClient, admin_token: str, test_account, db_session: AsyncSession
):
    """Contract creation doesn't validate plot availability (unlike proposals),
    so a second contract can be signed against an already-reserved plot —
    signing it must not knock the plot back down to 'hold'."""
    plot = await _make_plot(db_session, test_account, status="reserved")
    contract = await _create_contract(client, admin_token, test_account.id, plot.id)
    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) == "reserved"
