"""The BW-endpoint guard on PlusROI-lifecycle invoices (ADR #024, #025).

``services/money/invoices.py`` speaks open|sent|paid: ``update_invoice``
runs everything through ``clean_status`` (coerces anything else to
``open``), ``apply_entries`` attaches `time`-kind ``invoice_lines`` (a
PlusROI receivable is supposed to carry none), and ``delete_invoice``
destroys the row outright. All three are correct for BW documents and
destructive for a PlusROI receivable mid-lifecycle, so the shared
``_reject_plus_lifecycle`` guard refuses (409 PLUS_LIFECYCLE_INVOICE)
whenever the invoice's CURRENT status is PlusROI-shaped
(pending_approval/ready/cancelled/delayed); the shared open/sent/paid
vocabulary stays editable/appliable/deletable here exactly as before.

Lightweight SQLite tests (the tests/invoices/test_parallel_run_guard
pattern) — the guard fires on the loaded row before any write."""

import uuid
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, Invoice, Party, Tenant
from app.services.bulk import BulkSelection
from app.services.errors import ServiceError
from app.services.money import invoices as svc


@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 tenant(s):
    party = Party(kind="org", name="PlusROI", slug=f"plusroi-{uuid.uuid4().hex[:8]}")
    s.add(party)
    s.flush()
    tenant = Tenant(party_id=party.id, settings={"books_owner": "app"})
    s.add(tenant)
    s.flush()
    return tenant


def _owner(tenant_id):
    """A full-shape owner actor — update_invoice's serialization path
    runs scope(), which reads role/super-admin flags."""
    return SimpleNamespace(
        tenant_id=tenant_id,
        user_id=None,
        real_user_id=None,
        is_viewing_as=False,
        is_super_admin=True,
        role="owner",
        allowed=lambda cap: True,
        capabilities={"can_see_income": True},
    )


def _invoice(s, tenant_id, status):
    invoice = Invoice(
        tenant_id=tenant_id,
        name=f"{status} invoice",
        status=status,
        currency="CAD",
        manual_total=Decimal("100.00"),
    )
    s.add(invoice)
    s.flush()
    return invoice


@pytest.mark.parametrize(
    "status", ["pending_approval", "ready", "cancelled", "delayed"]
)
def test_bw_patch_refuses_plus_lifecycle_invoices(s, tenant, status):
    invoice = _invoice(s, tenant.id, status)
    s.commit()
    for patch in ({"name": "renamed"}, {"status": "paid"}, {"manual_total": "1"}):
        with pytest.raises(ServiceError) as err:
            svc.update_invoice(s, _owner(tenant.id), invoice.id, patch)
        assert err.value.code == "PLUS_LIFECYCLE_INVOICE"
        assert err.value.status == 409
    s.rollback()
    fresh = s.get(Invoice, invoice.id)
    assert fresh.status == status  # never coerced
    assert fresh.name == f"{status} invoice"


@pytest.mark.parametrize("status", ["open", "sent", "paid"])
def test_bw_statuses_still_editable(s, tenant, status):
    """The shared open/sent/paid vocabulary is untouched by the guard —
    a normal BW invoice edits exactly as before."""
    invoice = _invoice(s, tenant.id, status)
    res = svc.update_invoice(
        s, _owner(tenant.id), invoice.id, {"name": f"renamed {status}"}
    )
    assert res["name"] == f"renamed {status}"
    assert res["status"] == status


def test_bw_open_to_sent_transition_still_works(s, tenant):
    invoice = _invoice(s, tenant.id, "open")
    res = svc.update_invoice(
        s, _owner(tenant.id), invoice.id, {"status": "sent"}
    )
    assert res["status"] == "sent"
    assert res["sent_at"] is not None


@pytest.mark.parametrize(
    "status", ["pending_approval", "ready", "cancelled", "delayed"]
)
def test_apply_entries_refuses_plus_lifecycle_invoices(s, tenant, status):
    invoice = _invoice(s, tenant.id, status)
    s.commit()
    with pytest.raises(ServiceError) as err:
        svc.apply_entries(
            s, _owner(tenant.id), invoice.id, BulkSelection(expected_count=0, ids=[])
        )
    assert err.value.code == "PLUS_LIFECYCLE_INVOICE"
    assert err.value.status == 409
    s.rollback()
    fresh = s.get(Invoice, invoice.id)
    assert fresh.status == status  # never coerced, nothing attached


@pytest.mark.parametrize("status", ["open", "sent"])
def test_apply_entries_still_works_on_bw_statuses(s, tenant, status):
    """Shared vocabulary is unaffected — an empty selection against a
    normal BW invoice runs the full apply path (0 matched, 0 applied)
    instead of tripping the new guard. `paid` is excluded here: it
    already 409s with INVOICE_PAID, independent of this guard."""
    invoice = _invoice(s, tenant.id, status)
    res = svc.apply_entries(
        s, _owner(tenant.id), invoice.id, BulkSelection(expected_count=0, ids=[])
    )
    assert res["matched"] == 0
    assert res["applied"] == 0


@pytest.mark.parametrize(
    "status", ["pending_approval", "ready", "cancelled", "delayed"]
)
def test_delete_invoice_refuses_plus_lifecycle_invoices(s, tenant, status):
    invoice = _invoice(s, tenant.id, status)
    s.commit()
    with pytest.raises(ServiceError) as err:
        svc.delete_invoice(s, _owner(tenant.id), invoice.id)
    assert err.value.code == "PLUS_LIFECYCLE_INVOICE"
    assert err.value.status == 409
    s.rollback()
    assert s.get(Invoice, invoice.id) is not None  # never deleted


@pytest.mark.parametrize("status", ["open", "sent", "paid"])
def test_delete_invoice_still_works_on_bw_statuses(s, tenant, status):
    """A zero-line BW invoice still deletes exactly as before (the
    pre-existing INVOICE_HAS_LINES guard is untouched by this change)."""
    invoice = _invoice(s, tenant.id, status)
    res = svc.delete_invoice(s, _owner(tenant.id), invoice.id)
    assert res["ok"] is True
    assert res["deleted"] == str(invoice.id)
