"""Settlement "apply to month" (services/money/settlement_apply.py;
phase-3 slice 3, ADR #024): tag paid invoices + untagged expenses with a
settlement month; the FROZEN engine then reflects the month with zero
engine change. Money-safety skips (not-paid / already-tagged), hard 404
on ids outside the tenant, owner + sheet-owned gates, one audit row.

SQLite fixtures per the house style (mirrors tests/invoices_plus)."""

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

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

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

JULY = "2026-07"
JULY_1 = date(2026, 7, 1)

APP = {
    "books_owner": "app",
    "default_currency": "CAD",
    "bookkeeping_pct": 3.0,
    "partner_split": 0.5,
}
SHEET = {"books_owner": "sheet"}


@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()


def _tenant(s, name, slug, settings):
    party = Party(kind="org", name=name, slug=slug)
    s.add(party)
    s.flush()
    tenant = Tenant(party_id=party.id, settings=settings)
    s.add(tenant)
    s.flush()
    return tenant


def owner(tenant_id):
    return SimpleNamespace(
        tenant_id=tenant_id,
        user_id=None,
        real_user_id=None,
        person_party_id=None,
        is_viewing_as=False,
        allowed=lambda cap: True,
        capabilities={"can_see_income": True},
    )


def worker(tenant_id):
    return SimpleNamespace(
        tenant_id=tenant_id,
        user_id=None,
        real_user_id=None,
        person_party_id=None,
        is_viewing_as=False,
        allowed=lambda cap: False,
        capabilities={"can_see_income": False},
    )


@pytest.fixture()
def fx(s):
    plusroi = _tenant(s, "PlusROI", "plusroi", APP)
    other = _tenant(s, "Other Books", "other", APP)
    sheet = _tenant(s, "SheetCo", "sheetco", SHEET)
    return SimpleNamespace(plusroi=plusroi, other=other, sheet=sheet)


def _invoice(s, tenant_id, name, *, status="paid", amount="1000.00", month=None):
    invoice = Invoice(
        tenant_id=tenant_id,
        name=name,
        status=status,
        currency="CAD",
        manual_total=Decimal(amount),
        settlement_month=month,
    )
    s.add(invoice)
    s.flush()
    return invoice


def _expense(s, tenant_id, *, amount="100.00", month=None):
    expense = Expense(
        tenant_id=tenant_id,
        amount=Decimal(amount),
        currency="CAD",
        expense_date=date(2026, 7, 2),
        source="manual",
        settlement_month=month,
    )
    s.add(expense)
    s.flush()
    return expense


# ----------------------------------------------------------------- apply


def test_apply_tags_paid_invoices_and_expenses(s, fx):
    inv = _invoice(s, fx.plusroi.id, "Paid A")
    exp = _expense(s, fx.plusroi.id)
    res = apply_to_month(
        s,
        owner(fx.plusroi.id),
        {"month": JULY, "invoice_ids": [str(inv.id)], "expense_ids": [str(exp.id)]},
    )
    assert res["ok"] is True and res["month"] == JULY
    assert res["invoices_tagged"] == 1 and res["expenses_tagged"] == 1
    assert res["skipped_not_paid"] == 0 and res["skipped_already_tagged"] == 0
    s.refresh(inv), s.refresh(exp)
    assert inv.settlement_month == JULY_1
    assert exp.settlement_month == JULY_1


def test_frozen_engine_reflects_the_apply(s, fx):
    """Rian's month-end: apply, then the untouched engine computes the
    split from the freshly-tagged rows."""
    o = owner(fx.plusroi.id)
    inv = _invoice(s, fx.plusroi.id, "Paid A", amount="2000.00")
    exp = _expense(s, fx.plusroi.id, amount="500.00")

    before = settlement.settlement_summary(s, o)
    assert before["months"] == []
    assert before["unsettled"] == {"invoice_count": 1, "expense_count": 1}

    apply_to_month(
        s, o, {"month": JULY, "invoice_ids": [str(inv.id)], "expense_ids": [str(exp.id)]}
    )

    after = settlement.settlement_summary(s, o)
    assert after["unsettled"] == {"invoice_count": 0, "expense_count": 0}
    (month,) = after["months"]
    assert month["month"] == JULY
    assert month["gross"] == "2000.00"
    assert month["expenses"] == "500.00"
    # revenue 2000 → bookkeeping 3% = 60 → net 1440 → split 720
    assert month["bookkeeping"] == "60.00"
    assert month["net"] == "1440.00"
    assert month["split"] == "720.00"


def test_apply_skips_unpaid_and_already_tagged(s, fx):
    o = owner(fx.plusroi.id)
    paid = _invoice(s, fx.plusroi.id, "Paid")
    pending = _invoice(s, fx.plusroi.id, "Pending", status="pending_approval")
    cancelled = _invoice(s, fx.plusroi.id, "Cancelled", status="cancelled")
    tagged = _invoice(s, fx.plusroi.id, "Tagged", month=date(2026, 6, 1))
    tagged_exp = _expense(s, fx.plusroi.id, month=date(2026, 6, 1))
    fresh_exp = _expense(s, fx.plusroi.id)

    res = apply_to_month(
        s,
        o,
        {
            "month": JULY,
            "invoice_ids": [str(paid.id), str(pending.id), str(cancelled.id), str(tagged.id)],
            "expense_ids": [str(tagged_exp.id), str(fresh_exp.id)],
        },
    )
    assert res["invoices_tagged"] == 1 and res["expenses_tagged"] == 1
    assert res["skipped_not_paid"] == 2
    assert res["skipped_already_tagged"] == 2
    # a tagged row is NEVER silently moved between months
    s.refresh(tagged), s.refresh(tagged_exp)
    assert tagged.settlement_month == date(2026, 6, 1)
    assert tagged_exp.settlement_month == date(2026, 6, 1)
    s.refresh(pending)
    assert pending.settlement_month is None


def test_apply_nothing_left_409(s, fx):
    pending = _invoice(s, fx.plusroi.id, "Pending", status="pending_approval")
    with pytest.raises(ServiceError) as err:
        apply_to_month(
            s, owner(fx.plusroi.id), {"month": JULY, "invoice_ids": [str(pending.id)]}
        )
    assert err.value.code == "NOTHING_TO_APPLY" and err.value.status == 409


@pytest.mark.parametrize(
    "payload,code",
    [
        ({"month": "bad"}, "BAD_MONTH"),
        ({"month": JULY}, "NO_SELECTION"),
        ({"month": JULY, "invoice_ids": ["nope"]}, "BAD_IDS"),
        ({"month": JULY, "invoice_ids": "x"}, "BAD_IDS"),
    ],
)
def test_apply_validation(s, fx, payload, code):
    with pytest.raises(ServiceError) as err:
        apply_to_month(s, owner(fx.plusroi.id), payload)
    assert err.value.code == code


def test_apply_foreign_ids_hard_404(s, fx):
    """An id outside the tenant fails the WHOLE apply — nothing tags."""
    mine = _invoice(s, fx.plusroi.id, "Mine")
    foreign = _invoice(s, fx.other.id, "Foreign")
    s.commit()
    with pytest.raises(ServiceError) as err:
        apply_to_month(
            s,
            owner(fx.plusroi.id),
            {"month": JULY, "invoice_ids": [str(mine.id), str(foreign.id)]},
        )
    assert err.value.code == "INVOICES_NOT_FOUND" and err.value.status == 404
    s.rollback()
    assert s.get(Invoice, mine.id).settlement_month is None
    assert s.get(Invoice, foreign.id).settlement_month is None

    foreign_exp = _expense(s, fx.other.id)
    with pytest.raises(ServiceError) as err:
        apply_to_month(
            s,
            owner(fx.plusroi.id),
            {"month": JULY, "expense_ids": [str(foreign_exp.id)]},
        )
    assert err.value.code == "EXPENSES_NOT_FOUND" and err.value.status == 404


def test_apply_owner_and_sheet_gates(s, fx):
    with pytest.raises(ServiceError) as err:
        apply_to_month(
            s, worker(fx.plusroi.id), {"month": JULY, "invoice_ids": [str(uuid.uuid4())]}
        )
    assert err.value.code == "NOT_ALLOWED" and err.value.status == 403

    with pytest.raises(ServiceError) as err:
        apply_to_month(
            s, owner(fx.sheet.id), {"month": JULY, "invoice_ids": [str(uuid.uuid4())]}
        )
    assert err.value.code == "SHEET_OWNED_READ_ONLY" and err.value.status == 409


def test_apply_writes_one_audit_row(s, fx):
    inv = _invoice(s, fx.plusroi.id, "Paid A")
    exp = _expense(s, fx.plusroi.id)
    apply_to_month(
        s,
        owner(fx.plusroi.id),
        {"month": JULY, "invoice_ids": [str(inv.id)], "expense_ids": [str(exp.id)]},
    )
    audit = s.scalars(
        select(AuditLog).where(AuditLog.action == "settlement.apply")
    ).one()
    assert audit.after["month"] == JULY
    assert audit.after["invoice_ids"] == [str(inv.id)]
    assert audit.after["expense_ids"] == [str(exp.id)]
    assert audit.before["invoices"] == [
        {"id": str(inv.id), "settlement_month": None}
    ]
