import pytest
from rest_framework.test import APIClient

from apps.companies.models import Department, Organization, OrganizationMembership
from apps.core.models import DepartmentMembership, Permission, Role, User, UserRole
from apps.procurements.models import Approval, BudgetCommitment, FinancePosting, GoodsReceipt, Invoice, Payment, ProcurementContract, ProcurementEvent, ProcurementPolicy, PurchaseOrder, Requisition, RequisitionQuote
from datetime import date, timedelta
from decimal import Decimal
from apps.asset_management.models import Vendor
from apps.finance.models import BudgetCategory, BudgetItem
from .conftest import seed_chart_of_accounts_for


def client_for(user):
    client = APIClient()
    client.force_authenticate(user)
    return client


def grant(user, organization, *codenames):
    role = Role.objects.create(name=f"Role {user.email}", slug=f"role-{user.pk}")
    for codename in codenames:
        domain, action = codename.split(".", 1)
        permission, _ = Permission.objects.get_or_create(domain=domain, action=action, defaults={"label": codename})
        role.permissions.add(permission)
    UserRole.objects.create(user=user, role=role, organization=organization)
    return role


@pytest.fixture
def org(db):
    organization = Organization.objects.create(name="Test Org")
    seed_chart_of_accounts_for(organization)
    return organization


def join(user, organization):
    OrganizationMembership.objects.get_or_create(organization=organization, user=user)
    return user


@pytest.mark.django_db
def test_staff_sees_own_department_requisitions_but_not_other_departments(org):
    research = Department.objects.create(organization=org, name="Research")
    operations = Department.objects.create(organization=org, name="Operations")
    researcher = join(User.objects.create_user(email="researcher@scope.test", password="p"), org)
    operator = join(User.objects.create_user(email="operator@scope.test", password="p"), org)
    DepartmentMembership.objects.create(user=researcher, department=research, role="staff")
    DepartmentMembership.objects.create(user=operator, department=operations, role="staff")
    own = Requisition.objects.create(organization=org, title="Research laptops", requested_by=researcher, requesting_department=research)
    other = Requisition.objects.create(organization=org, title="Office furniture", requested_by=operator, requesting_department=operations)

    response = client_for(researcher).get("/api/requisitions/")
    assert response.status_code == 200
    rows = response.json().get("results", response.json())
    ids = {row["id"].replace("-", "") for row in rows}
    assert str(own.id).replace("-", "") in ids
    assert str(other.id).replace("-", "") not in ids
    assert client_for(researcher).get(f"/api/requisitions/{other.id}/").status_code == 404


@pytest.mark.django_db
def test_director_has_global_requisition_visibility(org):
    department = Department.objects.create(organization=org, name="Operations")
    requester = join(User.objects.create_user(email="requester@scope.test", password="p"), org)
    director = join(User.objects.create_user(email="director@scope.test", password="p", organization_role="director"), org)
    requisition = Requisition.objects.create(organization=org, title="Office furniture", requested_by=requester, requesting_department=department)

    response = client_for(director).get(f"/api/requisitions/{requisition.id}/")
    assert response.status_code == 200


@pytest.mark.django_db
def test_approved_requisition_rejects_direct_status_edit_and_delete(org):
    user = join(User.objects.create_user(email="requester@immutable.test", password="p"), org)
    grant(user, org, "procurements.update", "procurements.delete")
    requisition = Requisition.objects.create(organization=org, title="Approved purchase", requested_by=user, status="approved")

    patch = client_for(user).patch(f"/api/requisitions/{requisition.id}/", {"status": "draft"}, format="json")
    delete = client_for(user).delete(f"/api/requisitions/{requisition.id}/")
    requisition.refresh_from_db()
    assert patch.status_code == 400
    assert delete.status_code == 405
    assert requisition.status == "approved"


@pytest.mark.django_db
def test_requester_cannot_approve_own_requisition_even_with_required_role(org):
    requester = join(User.objects.create_user(email="requester@sod.test", password="p"), org)
    role = grant(requester, org, "procurements.approve")
    requisition = Requisition.objects.create(organization=org, title="Own request", requested_by=requester, status="pending")
    approval = Approval.objects.create(requisition=requisition, required_role=role.slug, status="pending")

    response = client_for(requester).post(
        f"/api/procurement-approvals/{approval.id}/decide/",
        {"decision": "approve", "comment": "Looks fine"}, format="json",
    )
    approval.refresh_from_db()
    assert response.status_code == 403
    assert approval.status == "pending"


@pytest.mark.django_db
def test_purchase_order_requires_approved_award_and_issue_creates_commitment(org):
    requester = join(User.objects.create_user(email="requester@po.test", password="p"), org)
    creator = join(User.objects.create_user(email="buyer@po.test", password="p", organization_role="director"), org)
    issuer = join(User.objects.create_user(email="manager@po.test", password="p", organization_role="director"), org)
    grant(creator, org, "procurements.create")
    vendor = Vendor.objects.create(organization=org, name="Selected Vendor")
    category = BudgetCategory.objects.create(organization=org, code="OPS", name="Operations")
    budget = BudgetItem.objects.create(category=category, name="Equipment", allocated_amount="2000")
    requisition = Requisition.objects.create(
        organization=org, title="Equipment", requested_by=requester, status="approved", total_amount="1100", budget_item=budget,
    )
    RequisitionQuote.objects.create(requisition=requisition, vendor=vendor, quoted_amount="1100", is_selected=True)

    created = client_for(creator).post("/api/purchase-orders/", {
        "order_number": "PO-SCOPE-1", "vendor": str(vendor.id), "requisition": str(requisition.id),
        "items": [{"name": "Equipment", "quantity": 1, "unit_price": 1000}],
        "subtotal": "1000", "tax": "100", "total": "1100",
    }, format="json")
    assert created.status_code == 201, created.content
    po_id = created.json()["id"]
    issued = client_for(issuer).post(f"/api/purchase-orders/{po_id}/issue/", {}, format="json")
    assert issued.status_code == 200, issued.content
    po = PurchaseOrder.objects.get(pk=po_id)
    assert po.status == "sent"
    assert BudgetCommitment.objects.get(purchase_order=po).amount == po.total
    assert ProcurementEvent.objects.filter(document_type="purchase_order", document_id=str(po.id), action="issued", actor=issuer).exists()


@pytest.mark.django_db
def test_purchase_order_creator_cannot_issue_own_order(org):
    requester = join(User.objects.create_user(email="requester@sod-po.test", password="p"), org)
    buyer = join(User.objects.create_user(email="buyer@sod-po.test", password="p"), org)
    grant(buyer, org, "procurements.create", "procurements.approve")
    vendor = Vendor.objects.create(organization=org, name="Vendor")
    category = BudgetCategory.objects.create(organization=org, code="OPS2", name="Operations 2")
    budget = BudgetItem.objects.create(category=category, name="Supplies", allocated_amount="2000")
    req = Requisition.objects.create(organization=org, title="Supplies", requested_by=requester, status="approved", budget_item=budget)
    RequisitionQuote.objects.create(requisition=req, vendor=vendor, quoted_amount="100", is_selected=True)
    po = PurchaseOrder.objects.create(organization=org, order_number="PO-SOD-1", vendor=vendor, requisition=req, subtotal=100, total=100, created_by=buyer)

    response = client_for(buyer).post(f"/api/purchase-orders/{po.id}/issue/", {}, format="json")
    assert response.status_code == 403


@pytest.mark.django_db
def test_payment_preparer_cannot_release_and_second_actor_can_release_matched_invoice(org):
    preparer = join(User.objects.create_user(email="preparer@payment.test", password="p"), org)
    releaser = join(User.objects.create_user(email="releaser@payment.test", password="p", organization_role="director"), org)
    grant(preparer, org, "procurements.create", "procurements.approve")
    vendor = Vendor.objects.create(organization=org, name="Payment Vendor")
    req = Requisition.objects.create(organization=org, title="Matched purchase", requested_by=preparer, status="approved")
    po = PurchaseOrder.objects.create(organization=org, order_number="PO-PAY-1", vendor=vendor, requisition=req, subtotal=100, total=100, status="sent")
    receipt = GoodsReceipt.objects.create(
        organization=org,
        purchase_order=po, receipt_number="GR-PAY-1", received_date=date.today(),
        status="received", items=[{"name": "Item", "quantity": 1, "unit_price": 100}], received_by=releaser,
    )
    invoice = Invoice.objects.create(
        organization=org,
        invoice_number="INV-PAY-1", vendor=vendor, purchase_order=po, goods_receipt=receipt,
        amount=100, total=100, due_date=date.today() + timedelta(days=30), status="approved",
    )

    created = client_for(preparer).post("/api/payments/", {
        "invoice": str(invoice.id), "amount": "100", "method": "bank_transfer",
    }, format="json")
    assert created.status_code == 201, created.content
    payment_id = created.json()["id"]
    own_release = client_for(preparer).post(f"/api/payments/{payment_id}/release/", {}, format="json")
    assert own_release.status_code == 403
    Payment.objects.filter(pk=payment_id).update(fund_approved_by=releaser)
    released = client_for(releaser).post(f"/api/payments/{payment_id}/release/", {"comment": "Matched documents reviewed"}, format="json")
    assert released.status_code == 200, released.content
    payment = Payment.objects.get(pk=payment_id)
    invoice.refresh_from_db()
    assert payment.status == "completed"
    assert invoice.status == "paid"
    assert FinancePosting.objects.filter(source_type="payment", source_id=str(payment.id).replace("-", ""), posting_type="payment").exists()


@pytest.mark.django_db
def test_project_payment_requires_project_manager_approval_before_fat_release(org):
    from apps.projects.models import Project, ProjectTeamMember
    preparer = join(User.objects.create_user(email="finance.staff@payment.test", password="p"), org)
    project_manager = join(User.objects.create_user(email="pm@payment.test", password="p"), org)
    fat_manager = join(User.objects.create_user(email="fat.manager@payment.test", password="p"), org)
    grant(preparer, org, "procurements.create")
    grant(project_manager, org, "procurements.approve")
    fat_role, _ = Role.objects.get_or_create(
        slug="fat-manager",
        defaults={"name": "F.A.T Manager"},
    )
    for domain, action in (("procurements", "view"), ("procurements", "approve")):
        permission, _ = Permission.objects.get_or_create(
            domain=domain,
            action=action,
            defaults={"label": f"{domain}.{action}"},
        )
        fat_role.permissions.add(permission)
    UserRole.objects.create(user=fat_manager, role=fat_role, organization=org)
    project = Project.objects.create(organization=org, name="Controlled Project")
    ProjectTeamMember.objects.create(project=project, user=project_manager, role="manager")
    vendor = Vendor.objects.create(organization=org, name="Controlled Vendor")
    req = Requisition.objects.create(organization=org, title="Project expense", requested_by=project_manager, project=project, status="approved")
    po = PurchaseOrder.objects.create(organization=org, order_number="PO-PROJECT-PAY", vendor=vendor, requisition=req, subtotal=100, total=100, status="sent")
    receipt = GoodsReceipt.objects.create(organization=org, purchase_order=po, receipt_number="GR-PROJECT-PAY", received_date=date.today(), status="received", items=[{"name": "Service", "quantity": 1, "unit_price": 100}], received_by=project_manager)
    invoice = Invoice.objects.create(organization=org, invoice_number="INV-PROJECT-PAY", vendor=vendor, purchase_order=po, goods_receipt=receipt, amount=100, total=100, due_date=date.today(), status="approved")
    payment = Payment.objects.create(organization=org, invoice=invoice, amount=100, method="bank_transfer", prepared_by=preparer)

    blocked = client_for(fat_manager).post(f"/api/payments/{payment.id}/release/", {}, format="json")
    assert blocked.status_code == 400
    approved = client_for(project_manager).post(f"/api/payments/{payment.id}/approve-funds/", {"comment": "Deliverable accepted"}, format="json")
    assert approved.status_code == 200, approved.content
    released = client_for(fat_manager).post(f"/api/payments/{payment.id}/release/", {"comment": "Final release reviewed"}, format="json")
    assert released.status_code == 200, released.content
    payment.refresh_from_db()
    assert payment.fund_approved_by == project_manager
    assert payment.released_by == fat_manager


@pytest.mark.django_db
def test_invoice_approval_emits_one_idempotent_accrual_posting(org):
    from apps.accounting.models import Account

    approver = join(User.objects.create_user(email="approver@posting.test", password="p", organization_role="director"), org)
    receiver = join(User.objects.create_user(email="receiver@posting.test", password="p"), org)
    vendor = Vendor.objects.create(organization=org, name="Posting Vendor")
    category = BudgetCategory.objects.create(organization=org, code="POSTING", name="Posting Test", expense_account=Account.objects.get(organization=org, code="5400"))
    budget = BudgetItem.objects.create(category=category, name="Posting budget", allocated_amount=1000)
    req = Requisition.objects.create(organization=org, title="Posting purchase", requested_by=receiver, status="approved", budget_item=budget)
    po = PurchaseOrder.objects.create(
        organization=org,
        order_number="PO-POST-1", vendor=vendor, requisition=req, status="sent",
        items=[{"sku": "A", "quantity": 1, "unit_price": 100}], subtotal=100, total=110,
    )
    receipt = GoodsReceipt.objects.create(
        organization=org,
        purchase_order=po, receipt_number="GR-POST-1", received_date=date.today(), status="received",
        received_by=receiver, items=[{"sku": "A", "quantity": 1, "unit_price": 100}],
    )
    invoice = Invoice.objects.create(
        organization=org,
        invoice_number="INV-POST-1", vendor=vendor, purchase_order=po, goods_receipt=receipt,
        items=[{"sku": "A", "quantity": 1, "unit_price": 100}], amount=100, tax=10, total=110,
        due_date=date.today() + timedelta(days=30), status="pending",
    )

    response = client_for(approver).post(f"/api/invoices/{invoice.id}/approve/", {}, format="json")
    assert response.status_code == 200, response.content
    posting = FinancePosting.objects.get(source_type="invoice", source_id=str(invoice.id), posting_type="accrual")
    assert posting.amount == invoice.total
    assert Decimal(posting.payload["expense"]) == Decimal("100")
    assert Decimal(posting.payload["tax"]) == Decimal("10")


@pytest.mark.django_db
def test_cancelling_issued_po_releases_commitment_and_keeps_audit_history(org):
    actor = join(User.objects.create_user(email="director@cancel.test", password="p", organization_role="director"), org)
    vendor = Vendor.objects.create(organization=org, name="Cancel Vendor")
    category = BudgetCategory.objects.create(organization=org, code="CANCEL", name="Cancellation")
    budget = BudgetItem.objects.create(category=category, name="Cancelled order", allocated_amount=1000)
    req = Requisition.objects.create(organization=org, title="Cancelled order", requested_by=actor, status="approved", budget_item=budget)
    po = PurchaseOrder.objects.create(organization=org, order_number="PO-CANCEL-1", vendor=vendor, requisition=req, status="sent", subtotal=500, total=500)
    commitment = BudgetCommitment.objects.create(purchase_order=po, budget_item=budget, amount=500)

    response = client_for(actor).post(f"/api/purchase-orders/{po.id}/cancel/", {"reason": "Supplier cannot deliver"}, format="json")
    assert response.status_code == 200, response.content
    po.refresh_from_db(); commitment.refresh_from_db()
    assert po.status == "cancelled"
    assert commitment.status == "released"
    assert ProcurementEvent.objects.filter(document_id=str(po.id), action="cancelled", reason="Supplier cannot deliver").exists()


@pytest.mark.django_db
def test_competitive_method_requires_minimum_quotes_before_award(org):
    ProcurementPolicy.objects.create(organization=org, direct_purchase_limit=100, formal_sourcing_limit=1000, minimum_quotes=3)
    actor = join(User.objects.create_user(email="buyer@sourcing.test", password="p", organization_role="director"), org)
    req = Requisition.objects.create(
        organization=org, title="Competitive purchase", requested_by=actor, status="approved",
        total_amount=500, procurement_method="three_quotes",
    )
    vendors = [Vendor.objects.create(organization=org, name=f"Vendor {index}") for index in range(2)]
    quote = RequisitionQuote.objects.create(requisition=req, vendor=vendors[0], quoted_amount=400)
    RequisitionQuote.objects.create(requisition=req, vendor=vendors[1], quoted_amount=450)

    blocked = client_for(actor).post(f"/api/requisition-quotes/{quote.id}/award/", {}, format="json")
    assert blocked.status_code == 400
    Vendor.objects.create(organization=org, name="Vendor 3")
    third = Vendor.objects.get(organization=org, name="Vendor 3")
    RequisitionQuote.objects.create(requisition=req, vendor=third, quoted_amount=475)
    awarded = client_for(actor).post(f"/api/requisition-quotes/{quote.id}/award/", {}, format="json")
    assert awarded.status_code == 200


@pytest.mark.django_db
def test_single_source_requires_exception_justification(org):
    actor = join(User.objects.create_user(email="director@exception.test", password="p", organization_role="director"), org)
    vendor = Vendor.objects.create(organization=org, name="Only Vendor")
    req = Requisition.objects.create(
        organization=org, title="Single source", requested_by=actor, status="approved", total_amount=500,
        procurement_method="single_source", exception_justification="",
    )
    quote = RequisitionQuote.objects.create(requisition=req, vendor=vendor, quoted_amount=500)
    response = client_for(actor).post(f"/api/requisition-quotes/{quote.id}/award/", {}, format="json")
    assert response.status_code == 400


@pytest.mark.django_db
def test_vendor_bank_details_are_visible_only_to_finance_authority(org):
    staff = join(User.objects.create_user(email="staff@vendor.test", password="p"), org)
    finance = join(User.objects.create_user(email="finance@vendor.test", password="p"), org)
    grant(finance, org, "finance.approve")
    vendor = Vendor.objects.create(organization=org, name="Confidential Vendor", bank_details={"account": "123456"})

    staff_response = client_for(staff).get(f"/api/vendors/{vendor.id}/")
    finance_response = client_for(finance).get(f"/api/vendors/{vendor.id}/")
    assert "bank_details" not in staff_response.json()
    assert finance_response.json()["bank_details"] == {"account": "123456"}


@pytest.mark.django_db
def test_requisition_workspace_exposes_one_scoped_lifecycle_read_model(org):
    user = join(User.objects.create_user(email="workspace@procurement.test", password="p"), org)
    requisition = Requisition.objects.create(organization=org, title="Lifecycle", requested_by=user)
    response = client_for(user).get(f"/api/requisitions/{requisition.id}/workspace/")
    assert response.status_code == 200
    assert response.json()["requisition"]["title"] == "Lifecycle"
    assert response.json()["stages"]["requisition"] == "draft"
    assert response.json()["purchase_orders"] == []
    assert response.json()["events"] == []


@pytest.mark.django_db
def test_signing_contract_uses_dual_control_and_commits_budget(org):
    requester = join(User.objects.create_user(email="requester@contract.test", password="p"), org)
    creator = join(User.objects.create_user(email="creator@contract.test", password="p", organization_role="director"), org)
    signer = join(User.objects.create_user(email="signer@contract.test", password="p", organization_role="director"), org)
    vendor = Vendor.objects.create(organization=org, name="Contract Vendor")
    category = BudgetCategory.objects.create(organization=org, code="CONSULT", name="Consulting")
    budget = BudgetItem.objects.create(category=category, name="Services", allocated_amount=5000)
    req = Requisition.objects.create(organization=org, title="Services", requested_by=requester, status="approved", budget_item=budget)
    RequisitionQuote.objects.create(requisition=req, vendor=vendor, quoted_amount=2000, is_selected=True)
    contract = ProcurementContract.objects.create(
        organization=org,
        title="Consulting", contract_number="CON-SOD-1", vendor=vendor, requisition=req,
        value=2000, start_date=date.today(), status="draft", created_by=creator,
    )

    own = client_for(creator).post(f"/api/procurement-contracts/{contract.id}/sign/", {}, format="json")
    assert own.status_code == 403
    signed = client_for(signer).post(f"/api/procurement-contracts/{contract.id}/sign/", {}, format="json")
    assert signed.status_code == 200
    contract.refresh_from_db()
    assert contract.status == "active"
    assert BudgetCommitment.objects.get(contract=contract).amount == contract.value


@pytest.mark.django_db
def test_partial_receipt_and_partial_invoice_match_at_line_level(org):
    vendor = Vendor.objects.create(organization=org, name="Partial Vendor")
    requester = join(User.objects.create_user(email="partial@match.test", password="p"), org)
    req = Requisition.objects.create(organization=org, title="Partial order", requested_by=requester, status="approved")
    po = PurchaseOrder.objects.create(
        organization=org,
        order_number="PO-PARTIAL-1", vendor=vendor, requisition=req,
        items=[{"sku": "LAPTOP", "name": "Laptop", "quantity": 10, "unit_price": 100}],
        subtotal=1000, total=1000, status="sent",
    )
    receipt = GoodsReceipt.objects.create(
        organization=org,
        purchase_order=po, receipt_number="GR-PARTIAL-1", received_date=date.today(), status="partial",
        items=[{"sku": "LAPTOP", "name": "Laptop", "quantity": 5, "unit_price": 100}], received_by=requester,
    )
    invoice = Invoice.objects.create(
        organization=org,
        invoice_number="INV-PARTIAL-1", vendor=vendor, purchase_order=po, goods_receipt=receipt,
        items=[{"sku": "LAPTOP", "name": "Laptop", "quantity": 5, "unit_price": 100}],
        amount=500, total=500, due_date=date.today() + timedelta(days=30),
    )
    assert invoice.match_status == "matched"
    assert invoice.match_detail()["lines"][0]["quantity_ok"] is True


@pytest.mark.django_db
def test_receipt_cannot_accept_more_than_ordered_quantity(org):
    receiver = join(User.objects.create_user(email="receiver@quantity.test", password="p", organization_role="director"), org)
    vendor = Vendor.objects.create(organization=org, name="Quantity Vendor")
    req = Requisition.objects.create(organization=org, title="Quantity", requested_by=receiver, status="approved")
    po = PurchaseOrder.objects.create(organization=org, order_number="PO-QTY-1", vendor=vendor, requisition=req, items=[{"sku": "A", "quantity": 5, "unit_price": 10}], subtotal=50, total=50, status="sent")
    receipt = GoodsReceipt.objects.create(organization=org, purchase_order=po, receipt_number="GR-QTY-1", received_date=date.today(), items=[{"sku": "A", "quantity": 6, "unit_price": 10}])
    response = client_for(receiver).post(f"/api/goods-receipts/{receipt.id}/accept/", {"decision": "received"}, format="json")
    assert response.status_code == 400
    receipt.refresh_from_db()
    assert receipt.status == "pending"


@pytest.mark.django_db
def test_issuing_purchase_order_posts_encumbrance_journal_entry(org):
    from apps.accounting.models import JournalEntry

    requester = join(User.objects.create_user(email="requester@encumbrance.test", password="p"), org)
    issuer = join(User.objects.create_user(email="issuer@encumbrance.test", password="p", organization_role="director"), org)
    vendor = Vendor.objects.create(organization=org, name="Encumbrance Vendor")
    category = BudgetCategory.objects.create(organization=org, code="ENC", name="Encumbrance Test")
    budget = BudgetItem.objects.create(category=category, name="Equipment", allocated_amount="2000")
    req = Requisition.objects.create(organization=org, title="Encumbrance test", requested_by=requester, status="approved", total_amount="500", budget_item=budget)
    RequisitionQuote.objects.create(requisition=req, vendor=vendor, quoted_amount="500", is_selected=True)
    po = PurchaseOrder.objects.create(organization=org, order_number="PO-ENCUMBER-1", vendor=vendor, requisition=req, subtotal=500, total=500)

    response = client_for(issuer).post(f"/api/purchase-orders/{po.id}/issue/", {}, format="json")
    assert response.status_code == 200, response.content
    journal = JournalEntry.objects.get(organization=org, journal_no="ENC-PO-ENCUMBER-1")
    assert journal.status == "Posted"
    debit, credit = journal.totals()
    assert debit == credit == Decimal("500")
    assert set(journal.lines.values_list("account__code", flat=True)) == {"1900", "2900"}


@pytest.mark.django_db
def test_cancelling_purchase_order_reverses_encumbrance_journal_entry(org):
    from apps.accounting.models import JournalEntry

    actor = join(User.objects.create_user(email="director@encumbrance-cancel.test", password="p", organization_role="director"), org)
    vendor = Vendor.objects.create(organization=org, name="Encumbrance Cancel Vendor")
    category = BudgetCategory.objects.create(organization=org, code="ENC-CANCEL", name="Encumbrance Cancel Test")
    budget = BudgetItem.objects.create(category=category, name="Cancelled encumbered order", allocated_amount=1000)
    req = Requisition.objects.create(organization=org, title="Cancelled encumbered order", requested_by=actor, status="approved", budget_item=budget)
    po = PurchaseOrder.objects.create(organization=org, order_number="PO-ENCUMBER-CANCEL-1", vendor=vendor, requisition=req, status="sent", subtotal=500, total=500)
    BudgetCommitment.objects.create(purchase_order=po, budget_item=budget, amount=500)

    response = client_for(actor).post(f"/api/purchase-orders/{po.id}/cancel/", {"reason": "No longer needed"}, format="json")
    assert response.status_code == 200, response.content
    journal = JournalEntry.objects.get(organization=org, journal_no="ENC-REV-PO-ENCUMBER-CANCEL-1")
    debit, credit = journal.totals()
    assert debit == credit == Decimal("500")
    assert journal.lines.get(account__code="1900").credit_idr == Decimal("500")
    assert journal.lines.get(account__code="2900").debit_idr == Decimal("500")


@pytest.mark.django_db
def test_approve_invoice_without_expense_account_is_blocked(org):
    approver = join(User.objects.create_user(email="approver@expense-account.test", password="p", organization_role="director"), org)
    receiver = join(User.objects.create_user(email="receiver@expense-account.test", password="p"), org)
    vendor = Vendor.objects.create(organization=org, name="Expense Account Vendor")
    category = BudgetCategory.objects.create(organization=org, code="NOACCT", name="No Account Category")
    budget = BudgetItem.objects.create(category=category, name="Unmapped budget", allocated_amount=1000)
    req = Requisition.objects.create(organization=org, title="Unmapped purchase", requested_by=receiver, status="approved", budget_item=budget)
    po = PurchaseOrder.objects.create(
        organization=org,
        order_number="PO-NOACCT-1", vendor=vendor, requisition=req, status="sent",
        items=[{"sku": "A", "quantity": 1, "unit_price": 100}], subtotal=100, total=110,
    )
    receipt = GoodsReceipt.objects.create(
        organization=org,
        purchase_order=po, receipt_number="GR-NOACCT-1", received_date=date.today(), status="received",
        received_by=receiver, items=[{"sku": "A", "quantity": 1, "unit_price": 100}],
    )
    invoice = Invoice.objects.create(
        organization=org,
        invoice_number="INV-NOACCT-1", vendor=vendor, purchase_order=po, goods_receipt=receipt,
        items=[{"sku": "A", "quantity": 1, "unit_price": 100}], amount=100, tax=10, total=110,
        due_date=date.today() + timedelta(days=30), status="pending",
    )
    response = client_for(approver).post(f"/api/invoices/{invoice.id}/approve/", {}, format="json")
    assert response.status_code == 400
    assert "expense account" in str(response.content).lower()


@pytest.mark.django_db
def test_approve_invoice_and_release_payment_post_correct_ledger_entries(org):
    from apps.accounting.models import Account, JournalEntry

    cash = Account.objects.get(organization=org, code="1000")
    ap = Account.objects.get(organization=org, code="2000")
    expense = Account.objects.get(organization=org, code="5400")

    approver = join(User.objects.create_user(email="approver@ledger.test", password="p", organization_role="director"), org)
    releaser = join(User.objects.create_user(email="releaser@ledger.test", password="p", organization_role="director"), org)
    receiver = join(User.objects.create_user(email="receiver@ledger.test", password="p"), org)
    vendor = Vendor.objects.create(organization=org, name="Ledger Vendor")
    category = BudgetCategory.objects.create(organization=org, code="LEDGER", name="Ledger Test", expense_account=expense)
    budget = BudgetItem.objects.create(category=category, name="Ledger budget", allocated_amount=1000)
    req = Requisition.objects.create(organization=org, title="Ledger purchase", requested_by=receiver, status="approved", budget_item=budget)
    po = PurchaseOrder.objects.create(
        organization=org,
        order_number="PO-LEDGER-1", vendor=vendor, requisition=req, status="sent",
        items=[{"sku": "A", "quantity": 1, "unit_price": 100}], subtotal=100, total=110,
    )
    receipt = GoodsReceipt.objects.create(
        organization=org,
        purchase_order=po, receipt_number="GR-LEDGER-1", received_date=date.today(), status="received",
        received_by=receiver, items=[{"sku": "A", "quantity": 1, "unit_price": 100}],
    )
    invoice = Invoice.objects.create(
        organization=org,
        invoice_number="INV-LEDGER-1", vendor=vendor, purchase_order=po, goods_receipt=receipt,
        items=[{"sku": "A", "quantity": 1, "unit_price": 100}], amount=100, tax=10, total=110,
        due_date=date.today() + timedelta(days=30), status="pending",
    )
    commitment = BudgetCommitment.objects.create(purchase_order=po, budget_item=budget, amount=110)

    approved = client_for(approver).post(f"/api/invoices/{invoice.id}/approve/", {}, format="json")
    assert approved.status_code == 200, approved.content
    accrual = JournalEntry.objects.get(organization=org, journal_no=f"ACR-{invoice.invoice_number}")
    assert accrual.lines.get(account=expense).debit_idr == Decimal("110")
    assert accrual.lines.get(account=ap).credit_idr == Decimal("110")

    payment = Payment.objects.create(organization=org, invoice=invoice, amount=110, method="bank_transfer", prepared_by=receiver, fund_approved_by=releaser)
    released = client_for(releaser).post(f"/api/payments/{payment.id}/release/", {"comment": "Reviewed and matched"}, format="json")
    assert released.status_code == 200, released.content
    payment_journal = JournalEntry.objects.get(organization=org, journal_no=f"PMT-{str(payment.id)[:8]}")
    assert payment_journal.lines.get(account=ap).debit_idr == Decimal("110")
    assert payment_journal.lines.get(account=cash).credit_idr == Decimal("110")

    commitment.refresh_from_db()
    assert commitment.status == "consumed"
    reversal = JournalEntry.objects.get(organization=org, journal_no=f"ENC-REV-{po.order_number}")
    assert reversal.lines.get(account__code="1900").credit_idr == Decimal("110")
    assert reversal.lines.get(account__code="2900").debit_idr == Decimal("110")


@pytest.mark.django_db
def test_reconcile_budget_commitments_finds_drift(org):
    from apps.procurements.reconciliation import reconcile_budget_commitments

    category = BudgetCategory.objects.create(organization=org, code="RECON", name="Reconciliation Test")
    budget = BudgetItem.objects.create(category=category, name="Drifted budget", allocated_amount=1000)
    requester = join(User.objects.create_user(email="recon@test.local", password="p"), org)
    # An active commitment with no matching GL encumbrance line is pure drift.
    BudgetCommitment.objects.create(budget_item=budget, amount=250, purchase_order=PurchaseOrder.objects.create(
        organization=org,
        order_number="PO-RECON-1", vendor=Vendor.objects.create(organization=org, name="Recon Vendor"),
        requisition=Requisition.objects.create(organization=org, title="Recon", requested_by=requester, budget_item=budget),
        subtotal=250, total=250,
    ))

    findings = reconcile_budget_commitments(org)
    match = next(f for f in findings if f["budget_item_id"] == str(budget.id))
    assert match["active_commitments"] == Decimal("250")
    assert match["gl_encumbrance_balance"] == Decimal("0")
    assert match["drift"] == Decimal("250")


@pytest.mark.django_db
def test_budget_reconciliation_endpoint_matches_after_issue(org):
    requester = join(User.objects.create_user(email="requester@recon-endpoint.test", password="p"), org)
    issuer = join(User.objects.create_user(email="issuer@recon-endpoint.test", password="p", organization_role="director"), org)
    vendor = Vendor.objects.create(organization=org, name="Recon Endpoint Vendor")
    category = BudgetCategory.objects.create(organization=org, code="RECON-EP", name="Reconciliation Endpoint Test")
    budget = BudgetItem.objects.create(category=category, name="Endpoint budget", allocated_amount=1000)
    req = Requisition.objects.create(organization=org, title="Endpoint recon", requested_by=requester, status="approved", total_amount="300", budget_item=budget)
    RequisitionQuote.objects.create(requisition=req, vendor=vendor, quoted_amount="300", is_selected=True)
    po = PurchaseOrder.objects.create(organization=org, order_number="PO-RECON-EP-1", vendor=vendor, requisition=req, subtotal=300, total=300)

    issued = client_for(issuer).post(f"/api/purchase-orders/{po.id}/issue/", {}, format="json")
    assert issued.status_code == 200, issued.content

    response = client_for(issuer).get("/api/procurement-reports/budget-reconciliation/")
    assert response.status_code == 200, response.content
    drifted_ids = {f["budget_item_id"] for f in response.json()["findings"]}
    assert str(budget.id) not in drifted_ids


@pytest.mark.django_db
def test_terminating_contract_releases_commitment_and_reverses_encumbrance(org):
    from apps.accounting.models import JournalEntry

    actor = join(User.objects.create_user(email="director@terminate.test", password="p", organization_role="director"), org)
    vendor = Vendor.objects.create(organization=org, name="Terminate Vendor")
    category = BudgetCategory.objects.create(organization=org, code="TERMINATE", name="Terminate Test")
    budget = BudgetItem.objects.create(category=category, name="Terminated services", allocated_amount=1000)
    req = Requisition.objects.create(organization=org, title="Terminated services", requested_by=actor, status="approved", budget_item=budget)
    contract = ProcurementContract.objects.create(
        organization=org,
        title="Consulting", contract_number="CON-TERM-1", vendor=vendor, requisition=req,
        value=500, start_date=date.today(), status="active",
    )
    commitment = BudgetCommitment.objects.create(contract=contract, budget_item=budget, amount=500)

    response = client_for(actor).post(f"/api/procurement-contracts/{contract.id}/terminate/", {"reason": "Vendor breach"}, format="json")
    assert response.status_code == 200, response.content
    contract.refresh_from_db(); commitment.refresh_from_db()
    assert contract.status == "terminated"
    assert commitment.status == "released"
    assert ProcurementEvent.objects.filter(document_id=str(contract.id), action="terminated", reason="Vendor breach").exists()

    reversal = JournalEntry.objects.get(organization=org, journal_no=f"ENC-REV-{contract.contract_number}")
    assert reversal.lines.get(account__code="1900").credit_idr == Decimal("500")
    assert reversal.lines.get(account__code="2900").debit_idr == Decimal("500")


@pytest.mark.django_db
def test_terminate_requires_reason_and_active_status(org):
    actor = join(User.objects.create_user(email="director@terminate-guard.test", password="p", organization_role="director"), org)
    vendor = Vendor.objects.create(organization=org, name="Terminate Guard Vendor")
    contract = ProcurementContract.objects.create(
        organization=org,
        title="Draft contract", contract_number="CON-TERM-GUARD-1", vendor=vendor,
        value=200, start_date=date.today(), status="draft",
    )

    no_reason = client_for(actor).post(f"/api/procurement-contracts/{contract.id}/terminate/", {}, format="json")
    assert no_reason.status_code == 400

    not_active = client_for(actor).post(f"/api/procurement-contracts/{contract.id}/terminate/", {"reason": "test"}, format="json")
    assert not_active.status_code == 400
    contract.refresh_from_db()
    assert contract.status == "draft"
