"""Settlement engine (services/money/settlement.py; ADR #017).

The pinned numeric case is April 2026 — the real month whose Wave
invoice (2026050) we hold: gross 55,492.20 / fees 647.88 /
commissions 3,937.17 / expenses 22,286.97 must yield split 13,546.48
under the full-precision rounding policy (round-each-stage yields
13,546.49 — the policy difference is load-bearing and pinned here).
"""

import uuid
from datetime import date
from decimal import Decimal
from types import SimpleNamespace

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool

from app.models import Base, Expense, Invoice, Party, Tenant
from app.services.errors import ServiceError
from app.services.money import settlement

APRIL = date(2026, 4, 1)
MAY = date(2026, 5, 1)


@pytest.fixture()
def s():
    engine = create_engine(
        "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
    )
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        yield session
    engine.dispose()


@pytest.fixture()
def books(s):
    """PlusROI tenant + BW party + an April shaped exactly like the real
    one (totals-wise) + a small May + untagged strays."""
    plusroi = Party(kind="org", name="PlusROI", slug="plusroi")
    bowden = Party(kind="org", name="Bowden Works", slug="bowden-works")
    client = Party(kind="org", name="Copernic", slug="copernic")
    s.add_all([plusroi, bowden, client])
    s.flush()
    tenant = Tenant(
        party_id=plusroi.id,
        settings={"bookkeeping_pct": 3.0, "partner_split": 0.5, "gst_pct": 5.0},
    )
    s.add(tenant)
    s.flush()

    def invoice(name, total, fee=None, commission=None, month=APRIL, status="paid"):
        s.add(
            Invoice(
                tenant_id=tenant.id,
                name=name,
                status=status,
                currency="CAD",
                manual_total=Decimal(total),
                fee=Decimal(fee) if fee else None,
                commission_amount=Decimal(commission) if commission else None,
                settlement_month=month,
                from_party_id=plusroi.id,
                to_party_id=client.id,
            )
        )

    def expense(amount, category, paid_by=None, month=APRIL):
        s.add(
            Expense(
                tenant_id=tenant.id,
                amount=Decimal(amount),
                currency="CAD",
                category=category,
                paid_by_party_id=paid_by,
                settlement_month=month,
                source="sheet",
            )
        )

    # April totals == the real month: gross 55492.20, fees 647.88,
    # commissions 3937.17, expenses 22286.97.
    invoice("A-1", "30000.00", fee="400.00", commission="2000.00")
    invoice("A-2", "20000.00", fee="247.88", commission="1937.17")
    invoice("A-3", "5492.20")
    expense("18026.09", "Labour", paid_by=plusroi.id)
    expense("2591.09", "Labour", paid_by=bowden.id)  # BW-fronted team cost
    expense("1025.65", "Other Expenses", paid_by=plusroi.id)
    # BW-fronted ad spend + a licence, month April:
    expense("644.14", "Client Advertising Spend", paid_by=bowden.id)

    # A clean small May.
    invoice("M-1", "1000.00", fee="10.00", month=MAY)
    expense("100.00", "Labour", paid_by=plusroi.id, month=MAY)

    # Untagged strays (outside every settlement).
    invoice("U-1", "999.99", month=None, status="open")
    expense("55.55", "Labour", paid_by=plusroi.id, month=None)

    s.flush()
    return SimpleNamespace(tenant=tenant, plusroi=plusroi, bowden=bowden)


def owner_actor(tenant_id):
    return SimpleNamespace(tenant_id=tenant_id, capabilities={"can_see_income": True})


def worker_actor(tenant_id):
    return SimpleNamespace(tenant_id=tenant_id, capabilities={"can_see_income": False})


# --- authz ----------------------------------------------------------------


def test_settlement_is_owner_only(s, books):
    with pytest.raises(ServiceError) as err:
        settlement.settlement_summary(s, worker_actor(books.tenant.id))
    assert err.value.code == "INCOME_FORBIDDEN"
    with pytest.raises(ServiceError) as err:
        settlement.settlement_detail(s, worker_actor(books.tenant.id), "2026-04")
    assert err.value.code == "INCOME_FORBIDDEN"


# --- the pinned April math --------------------------------------------------


def test_april_matches_the_real_wave_invoice(s, books):
    detail = settlement.settlement_detail(s, owner_actor(books.tenant.id), "2026-04")
    assert detail["gross"] == "55492.20"
    assert detail["fees"] == "647.88"
    assert detail["commissions"] == "3937.17"
    assert detail["revenue"] == "50907.15"
    # April expenses here: 18026.09 + 2591.09 + 1025.65 + 644.14 = 22286.97
    assert detail["expenses"] == "22286.97"
    assert detail["bookkeeping"] == "1527.21"  # 3% of 50907.15 = 1527.2145
    assert detail["net"] == "27092.97"
    # THE pin: full-precision split = 13546.48275 -> 13546.48 (the real
    # invoice 2026050 line). Round-each-stage would say 13546.49.
    assert detail["split"] == "13546.48"


def test_summary_months_sorted_and_unsettled_counted(s, books):
    summary = settlement.settlement_summary(s, owner_actor(books.tenant.id))
    assert [m["month"] for m in summary["months"]] == ["2026-05", "2026-04"]
    assert summary["unsettled"] == {"invoice_count": 1, "expense_count": 1}
    may = summary["months"][0]
    # 1000 - 10 = 990 revenue; book 29.70; net 860.30; split 430.15
    assert may["revenue"] == "990.00"
    assert may["bookkeeping"] == "29.70"
    assert may["net"] == "860.30"
    assert may["split"] == "430.15"


# --- reimbursements + the suggested BW invoice -------------------------------


def test_reimbursements_only_non_tenant_payers(s, books):
    detail = settlement.settlement_detail(s, owner_actor(books.tenant.id), "2026-04")
    payers = {r["paid_by"] for r in detail["reimbursements"]}
    assert payers == {"Bowden Works"}  # PlusROI's own payments never appear
    amounts = {r["category"]: r["amount"] for r in detail["reimbursements"]}
    assert amounts == {"Labour": "2591.09", "Client Advertising Spend": "644.14"}


def test_suggested_invoice_lines_and_gst(s, books):
    detail = settlement.settlement_detail(s, owner_actor(books.tenant.id), "2026-04")
    inv = detail["suggested_invoice"]
    by_label = {line["label"]: line["amount"] for line in inv["lines"]}
    assert by_label == {
        "PlusROI Net Profit Split": "13546.48",
        "BowdenWorks Team Cost": "2591.09",  # BW-fronted Labour
        "PPC Spend": "644.14",  # BW-fronted ad spend
    }
    subtotal = Decimal("13546.48") + Decimal("2591.09") + Decimal("644.14")
    assert inv["subtotal"] == str(subtotal)
    assert inv["gst"] == str((subtotal * Decimal("0.05")).quantize(Decimal("0.01")))
    assert Decimal(inv["total"]) == subtotal + Decimal(inv["gst"])


def test_licences_fallback_bucket(s, books):
    s.add(
        Expense(
            tenant_id=books.tenant.id,
            amount=Decimal("204.16"),
            currency="CAD",
            category="General Licenses",
            paid_by_party_id=books.bowden.id,
            settlement_month=APRIL,
            source="sheet",
        )
    )
    s.flush()
    detail = settlement.settlement_detail(s, owner_actor(books.tenant.id), "2026-04")
    by_label = {line["label"]: line["amount"] for line in detail["suggested_invoice"]["lines"]}
    assert by_label["Licences / Fees"] == "204.16"


# --- input edges --------------------------------------------------------------


def test_bad_month_rejected(s, books):
    with pytest.raises(ServiceError) as err:
        settlement.settlement_detail(s, owner_actor(books.tenant.id), "not-a-month")
    assert err.value.code == "BAD_MONTH"


def test_unknown_month_404(s, books):
    with pytest.raises(ServiceError) as err:
        settlement.settlement_detail(s, owner_actor(books.tenant.id), "2019-01")
    assert err.value.code == "SETTLEMENT_MONTH_NOT_FOUND"


def test_tenant_isolation(s, books):
    other_party = Party(kind="org", name="OtherCo", slug="otherco")
    s.add(other_party)
    s.flush()
    other = Tenant(party_id=other_party.id, settings={})
    s.add(other)
    s.flush()
    s.add(
        Invoice(
            tenant_id=other.id,
            name="X-1",
            status="paid",
            currency="CAD",
            manual_total=Decimal("77777.77"),
            settlement_month=APRIL,
        )
    )
    s.flush()
    detail = settlement.settlement_detail(s, owner_actor(books.tenant.id), "2026-04")
    assert detail["gross"] == "55492.20"  # the other tenant's rows are invisible


def test_zero_uuid_unused_guard():
    # parse_month is the only pure helper; pin its contract.
    assert settlement.parse_month("2026-07") == date(2026, 7, 1)
    with pytest.raises(ServiceError):
        settlement.parse_month("2026-13")
    _ = uuid  # keep the import honest for fixture use above
