"""Laporan Penerimaan dan Pengeluaran — per-project receipts/disbursements report."""

import datetime

import pytest
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient

from apps.accounting.models import Account, Fund, JournalEntry, JournalLine
from apps.projects.models import Project

User = get_user_model()

URL = "/api/accounting/receipts-disbursements/"


@pytest.fixture
def client(db):
    admin = User.objects.create_superuser(email="rd-admin@test.local", password="x")
    c = APIClient()
    c.force_authenticate(admin)
    c.organization = admin.organizations.first()
    return c


@pytest.fixture
def chart(client):
    """Cash, income, and a grouped expense tree: two direct groups + overhead."""
    org = client.organization
    mk = lambda **kw: Account.objects.create(organization=org, **kw)
    cash = mk(code="1000", name="Cash and Bank", type="Asset", normal_balance="Debit", is_cash=True)
    advance = mk(code="1200", name="Employee / Activity Advance", type="Asset", normal_balance="Debit")
    income = mk(code="4000", name="Donasi", type="Income", normal_balance="Credit")

    personnel = mk(code="5A00", name="Biaya Personel", type="Expense", normal_balance="Debit", expense_class="direct")
    ops = mk(code="5B00", name="Operasional Riset", type="Expense", normal_balance="Debit", expense_class="direct")
    overhead = mk(code="5D00", name="Beban Overhead", type="Expense", normal_balance="Debit", expense_class="indirect")

    salary = mk(code="5000", name="Gaji Peneliti", type="Expense", normal_balance="Debit",
                parent=personnel, expense_class="direct")
    consultant = mk(code="5300", name="Peneliti Mitra", type="Expense", normal_balance="Debit",
                    parent=personnel, expense_class="direct")
    travel = mk(code="5100", name="Perjalanan", type="Expense", normal_balance="Debit",
                parent=ops, expense_class="direct")
    office = mk(code="5400", name="Institusi", type="Expense", normal_balance="Debit",
                parent=overhead, expense_class="indirect")
    return {
        "cash": cash, "advance": advance, "income": income,
        "salary": salary, "consultant": consultant, "travel": travel, "office": office,
    }


@pytest.fixture
def project(client):
    return Project.objects.create(organization=client.organization, name="DfdLab Tahun Ke-3")


@pytest.fixture
def fund(client, project):
    return Fund.objects.create(
        organization=client.organization, fund_id="FND-RD1", name="DfdLab Grant",
        fund_type="Restricted", project=project, award_amount=1_000_000_000,
    )


def _post(fund, no, date, pairs):
    """Post a balanced voucher; `pairs` is [(account, debit, credit), ...]."""
    entry = JournalEntry.objects.create(
        organization=fund.organization, journal_no=no, date=date,
        fund=fund, status="Posted",
    )
    for account, debit, credit in pairs:
        JournalLine.objects.create(journal=entry, account=account, debit_idr=debit, credit_idr=credit)
    return entry


@pytest.mark.django_db
def test_requires_a_project_selector(client):
    assert client.get(URL).status_code == 400


@pytest.mark.django_db
def test_unknown_project_is_rejected(client):
    res = client.get(URL, {"project": "00000000-0000-0000-0000-000000000000"})
    assert res.status_code == 400


@pytest.mark.django_db
def test_groups_expenses_and_totals_like_the_reference_sheet(client, chart, project, fund):
    # Receipt, then spend across two direct groups and one overhead account.
    _post(fund, "JV-1", datetime.date(2026, 2, 1), [
        (chart["cash"], 900_000_000, 0), (chart["income"], 0, 900_000_000),
    ])
    _post(fund, "JV-2", datetime.date(2026, 2, 10), [
        (chart["salary"], 300_000_000, 0), (chart["cash"], 0, 300_000_000),
    ])
    _post(fund, "JV-3", datetime.date(2026, 2, 11), [
        (chart["consultant"], 100_000_000, 0), (chart["cash"], 0, 100_000_000),
    ])
    _post(fund, "JV-4", datetime.date(2026, 2, 12), [
        (chart["travel"], 50_000_000, 0), (chart["cash"], 0, 50_000_000),
    ])
    _post(fund, "JV-5", datetime.date(2026, 2, 13), [
        (chart["office"], 25_000_000, 0), (chart["cash"], 0, 25_000_000),
    ])

    body = client.get(URL, {"project": str(project.id)}).json()

    assert body["total_receipts"] == 900000000
    # Two direct groups: Biaya Personel (300M + 100M) and Operasional Riset (50M).
    personnel = next(g for g in body["direct_expenses"] if g["code"] == "5A00")
    assert personnel["amount"] == 400000000
    assert [l["code"] for l in personnel["lines"]] == ["5000", "5300"]
    assert body["total_direct_expenses"] == 450000000
    assert body["total_indirect_expenses"] == 25000000
    # The reference sheet's three derived totals.
    assert body["total_disbursements"] == 475000000
    assert body["closing_balance"] == 425000000
    assert body["fund_balance"] == 425000000


@pytest.mark.django_db
def test_opening_balance_excludes_the_window_and_carries_forward(client, chart, project, fund):
    _post(fund, "JV-1", datetime.date(2026, 1, 5), [
        (chart["cash"], 200_000_000, 0), (chart["income"], 0, 200_000_000),
    ])
    _post(fund, "JV-2", datetime.date(2026, 3, 5), [
        (chart["cash"], 100_000_000, 0), (chart["income"], 0, 100_000_000),
    ])

    body = client.get(URL, {
        "project": str(project.id), "date_from": "2026-02-01", "date_to": "2026-12-31",
    }).json()

    # January's receipt is opening balance, not in-period income.
    assert body["opening_balance"] == 200000000
    assert body["total_receipts"] == 100000000
    assert body["closing_balance"] == 300000000


@pytest.mark.django_db
def test_advances_are_netted_out_of_cash_and_equivalents(client, chart, project, fund):
    _post(fund, "JV-1", datetime.date(2026, 2, 1), [
        (chart["cash"], 500_000_000, 0), (chart["income"], 0, 500_000_000),
    ])
    # Advance issued but not yet settled: still a receivable at period end.
    _post(fund, "JV-2", datetime.date(2026, 2, 8), [
        (chart["advance"], 20_000_000, 0), (chart["cash"], 0, 20_000_000),
    ])

    body = client.get(URL, {"project": str(project.id)}).json()

    assert body["advances"] == 20000000
    assert body["closing_balance"] == 500000000
    assert body["cash_and_equivalents"] == 480000000


@pytest.mark.django_db
def test_draft_journals_and_other_projects_are_excluded(client, chart, project, fund):
    _post(fund, "JV-1", datetime.date(2026, 2, 1), [
        (chart["cash"], 400_000_000, 0), (chart["income"], 0, 400_000_000),
    ])
    # Draft voucher on the same fund must not count.
    draft = JournalEntry.objects.create(
        organization=fund.organization, journal_no="JV-D", date=datetime.date(2026, 2, 2),
        fund=fund, status="Draft",
    )
    JournalLine.objects.create(journal=draft, account=chart["income"], debit_idr=0, credit_idr=99_000_000)

    # A second project's fund must not bleed in.
    other_project = Project.objects.create(organization=client.organization, name="Other")
    other_fund = Fund.objects.create(
        organization=client.organization, fund_id="FND-RD2", name="Other Grant",
        fund_type="Restricted", project=other_project,
    )
    _post(other_fund, "JV-2", datetime.date(2026, 2, 3), [
        (chart["cash"], 77_000_000, 0), (chart["income"], 0, 77_000_000),
    ])

    body = client.get(URL, {"project": str(project.id)}).json()
    assert body["total_receipts"] == 400000000


@pytest.mark.django_db
def test_matches_funds_by_free_text_project_code(client, chart):
    """Most ledger rows still tag the project with Fund.project_code."""
    fund = Fund.objects.create(
        organization=client.organization, fund_id="FND-RD3", name="Tagged Grant",
        fund_type="Restricted", project_code="PRJ-HEALTH",
    )
    _post(fund, "JV-1", datetime.date(2026, 2, 1), [
        (chart["cash"], 150_000_000, 0), (chart["income"], 0, 150_000_000),
    ])

    body = client.get(URL, {"project_code": "PRJ-HEALTH"}).json()
    assert body["total_receipts"] == 150000000
    assert body["project"]["code"] == "PRJ-HEALTH"


@pytest.mark.django_db
def test_account_parent_must_be_a_top_level_header(client, chart):
    """Grouping is one level deep — the report renders group -> lines only."""
    salary = chart["salary"]          # already a child of 5A00
    travel = chart["travel"]
    res = client.patch(
        f"/api/accounting/accounts/{travel.id}/", {"parent": str(salary.id)}, format="json",
    )
    assert res.status_code == 400

    # Self-parenting is rejected too.
    res = client.patch(
        f"/api/accounting/accounts/{travel.id}/", {"parent": str(travel.id)}, format="json",
    )
    assert res.status_code == 400


@pytest.mark.django_db
def test_counts_cash_receipts_booked_to_deferred_income(client, chart, project, fund):
    """A grant instalment lands as a liability (Deferred Grant Income), not as
    Income — it is recognised only as it is spent. The report is cash-basis, so
    the inflow must still show as Penerimaan."""
    deferred = Account.objects.create(
        organization=client.organization, code="2100", name="Deferred Grant Income",
        type="Liability", normal_balance="Credit",
    )
    _post(fund, "JV-1", datetime.date(2026, 1, 10), [
        (chart["cash"], 700_000_000, 0), (deferred, 0, 700_000_000),
    ])

    body = client.get(URL, {"project": str(project.id)}).json()

    assert body["total_receipts"] == 700000000
    assert [r["code"] for r in body["receipts"]] == ["2100"]


@pytest.mark.django_db
def test_expense_payments_are_not_counted_as_receipts(client, chart, project, fund):
    """Cash going out must never register as a receipt."""
    _post(fund, "JV-1", datetime.date(2026, 2, 1), [
        (chart["cash"], 500_000_000, 0), (chart["income"], 0, 500_000_000),
    ])
    _post(fund, "JV-2", datetime.date(2026, 2, 5), [
        (chart["salary"], 90_000_000, 0), (chart["cash"], 0, 90_000_000),
    ])

    body = client.get(URL, {"project": str(project.id)}).json()

    assert body["total_receipts"] == 500000000
    assert body["total_direct_expenses"] == 90000000
    assert body["closing_balance"] == 410000000
