"""Accounting module: models, double-entry posting, and derived endpoints."""

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, Transaction, JournalEntry, JournalLine, Sale, SalesAccountingSettings,
)

User = get_user_model()


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


@pytest.fixture
def accounts(client):
    org = client.organization
    cash = Account.objects.create(organization=org, code="1000", name="Cash and Bank", type="Asset", normal_balance="Debit")
    deferred = Account.objects.create(organization=org, code="2100", name="Deferred Grant Income", type="Liability", normal_balance="Credit")
    expense = Account.objects.create(organization=org, code="5000", name="Personnel Expense", type="Expense", normal_balance="Debit")
    return {"cash": cash, "deferred": deferred, "expense": expense}


@pytest.fixture
def fund(client):
    return Fund.objects.create(
        organization=client.organization,
        fund_id="FND-001", name="Community Health", fund_type="Restricted",
        award_amount=1_200_000_000, received_amount=700_000_000,
    )


def _balanced_journal(accounts, fund, posted=True):
    """A balanced grant-receipt voucher: debit cash, credit deferred income."""
    entry = JournalEntry.objects.create(
        organization=fund.organization,
        journal_no="JV-001", date=datetime.date(2026, 1, 10), reference="TRX-001",
        description="Grant installment", fund=fund, status="Posted" if posted else "Draft",
    )
    JournalLine.objects.create(journal=entry, account=accounts["cash"], debit_idr=700_000_000, credit_idr=0)
    JournalLine.objects.create(journal=entry, account=accounts["deferred"], debit_idr=0, credit_idr=700_000_000)
    return entry


@pytest.mark.django_db
class TestModels:
    def test_account_str(self, accounts):
        assert str(accounts["cash"]) == "1000 Cash and Bank"

    def test_journal_totals(self, accounts, fund):
        entry = _balanced_journal(accounts, fund)
        debit, credit = entry.totals()
        assert debit == credit == 700_000_000


@pytest.mark.django_db
class TestPermissions:
    def test_requires_auth(self):
        anon = APIClient()
        resp = anon.get("/api/accounting/accounts/")
        assert resp.status_code in (401, 403)


@pytest.mark.django_db
class TestJournalPosting:
    def test_post_balanced_succeeds(self, client, accounts, fund):
        entry = _balanced_journal(accounts, fund, posted=False)
        resp = client.post(f"/api/accounting/journal-entries/{entry.id}/post_entry/")
        assert resp.status_code == 200
        entry.refresh_from_db()
        assert entry.status == "Posted"

    def test_post_unbalanced_rejected(self, client, accounts, fund):
        entry = JournalEntry.objects.create(organization=client.organization, journal_no="JV-X", date=datetime.date(2026, 1, 1), fund=fund)
        JournalLine.objects.create(journal=entry, account=accounts["cash"], debit_idr=100, credit_idr=0)
        resp = client.post(f"/api/accounting/journal-entries/{entry.id}/post_entry/")
        assert resp.status_code == 400
        entry.refresh_from_db()
        assert entry.status == "Draft"

    def test_create_with_nested_lines(self, client, accounts, fund):
        payload = {
            "journal_no": "JV-002", "date": "2026-02-05", "reference": "TRX-002",
            "description": "Salary", "fund": str(fund.id), "budget_line": "Personnel",
            "lines": [
                {"account": str(accounts["expense"].id), "debit_idr": "35000000", "credit_idr": "0"},
                {"account": str(accounts["cash"].id), "debit_idr": "0", "credit_idr": "35000000"},
            ],
        }
        resp = client.post("/api/accounting/journal-entries/", payload, format="json")
        assert resp.status_code == 201, resp.content
        body = resp.json()
        assert body["is_balanced"] is True
        assert len(body["lines"]) == 2


@pytest.mark.django_db
class TestDerivedEndpoints:
    def test_trial_balance_balanced(self, client, accounts, fund):
        _balanced_journal(accounts, fund)
        body = client.get("/api/accounting/trial-balance/").json()
        assert body["balanced"] is True
        assert float(body["total_debit_balance"]) == 700_000_000

    def test_general_ledger_only_posted(self, client, accounts, fund):
        _balanced_journal(accounts, fund, posted=False)  # Draft -> excluded
        body = client.get("/api/accounting/general-ledger/").json()
        assert body["results"] == []

    def test_dashboard_metrics(self, client, accounts, fund):
        _balanced_journal(accounts, fund)
        body = client.get("/api/accounting/dashboard/").json()
        assert float(body["metrics"]["total_received"]) == 700_000_000
        assert body["metrics"]["fund_count"] == 1

    def test_journal_control_balanced_row(self, client, accounts, fund):
        _balanced_journal(accounts, fund)
        rows = client.get("/api/accounting/journal-control/").json()
        assert rows[0]["balance_status"] == "Balanced"
        assert rows[0]["posting_status"] == "Posted"


@pytest.mark.django_db
class TestISAK335:
    """ISAK 335 statements derive from posted journals and must articulate."""

    def _expense_journal(self, accounts, fund):
        """Restricted-fund expense: debit expense, credit cash. Posted."""
        e = JournalEntry.objects.create(
            organization=fund.organization,
            journal_no="JV-EXP", date=datetime.date(2026, 2, 5), fund=fund, status="Posted",
        )
        JournalLine.objects.create(journal=e, account=accounts["expense"], debit_idr=35_000_000, credit_idr=0)
        JournalLine.objects.create(journal=e, account=accounts["cash"], debit_idr=0, credit_idr=35_000_000)
        return e

    def test_financial_position_balances(self, client, accounts, fund):
        _balanced_journal(accounts, fund)
        self._expense_journal(accounts, fund)
        body = client.get("/api/accounting/isak335/financial-position/").json()
        assert body["balanced"] is True
        # Total assets = total liabilities + total net assets (articulation).
        assert float(body["total_assets"]) == (
            float(body["total_liabilities"]) + float(body["net_assets"]["total"])
        )

    def test_activities_classifies_by_restriction(self, client, accounts, fund):
        # Restricted-fund expense lands in dengan_pembatasan.
        self._expense_journal(accounts, fund)
        body = client.get("/api/accounting/isak335/activities/").json()
        assert float(body["dengan_pembatasan"]["total_expense"]) == 35_000_000
        assert float(body["tanpa_pembatasan"]["total_expense"]) == 0
        assert float(body["total_surplus"]) == -35_000_000

    def test_changes_in_net_assets_articulates(self, client, accounts, fund):
        self._expense_journal(accounts, fund)
        body = client.get("/api/accounting/isak335/changes-net-assets/").json()
        # opening + change = closing for each class.
        for row in body["rows"]:
            assert float(row["opening"]) + float(row["change"]) == float(row["closing"])

    def test_cash_flow_ties_to_ending_cash(self, client, accounts, fund):
        # Mark cash as the cash basis; expense account is operating.
        accounts["cash"].is_cash = True
        accounts["cash"].save()
        _balanced_journal(accounts, fund)         # +700M cash (grant receipt, operating)
        self._expense_journal(accounts, fund)     # -35M cash (operating)
        body = client.get("/api/accounting/isak335/cash-flows/").json()
        assert float(body["net_change"]) == float(body["ending_cash"]) == 665_000_000
        assert float(body["operating"]) == 665_000_000

    def test_activities_splits_contribution_vs_exchange(self, client, accounts, fund):
        # Donor grant (Restricted fund) -> contribution income by default (blank revenue_type).
        grant_income = Account.objects.create(
            organization=client.organization, code="4000", name="Grant Income", type="Income", normal_balance="Credit",
        )
        exchange_income = Account.objects.create(
            organization=client.organization, code="4900", name="Book Sales Income", type="Income", normal_balance="Credit",
            revenue_type="exchange",
        )
        grant = JournalEntry.objects.create(
            organization=client.organization,
            journal_no="JV-GRANT", date=datetime.date(2026, 3, 1), fund=fund, status="Posted",
        )
        JournalLine.objects.create(journal=grant, account=accounts["cash"], debit_idr=10_000_000, credit_idr=0)
        JournalLine.objects.create(journal=grant, account=grant_income, debit_idr=0, credit_idr=10_000_000)

        sale = JournalEntry.objects.create(
            organization=client.organization,
            journal_no="JV-SALE", date=datetime.date(2026, 3, 2), status="Posted",  # no fund -> tanpa_pembatasan
        )
        JournalLine.objects.create(journal=sale, account=accounts["cash"], debit_idr=500_000, credit_idr=0)
        JournalLine.objects.create(journal=sale, account=exchange_income, debit_idr=0, credit_idr=500_000)

        body = client.get("/api/accounting/isak335/activities/").json()
        without = body["tanpa_pembatasan"]
        assert float(without["exchange_income"]) == 500_000
        assert float(without["contribution_income"]) == 0
        with_ = body["dengan_pembatasan"]
        assert float(with_["contribution_income"]) == 10_000_000
        assert float(with_["exchange_income"]) == 0


@pytest.mark.django_db
class TestSale:
    @pytest.fixture
    def sales_settings(self, client, accounts):
        org = client.organization
        revenue = Account.objects.create(organization=org, code="4900", name="Book Sales Income", type="Income", normal_balance="Credit", revenue_type="exchange")
        cogs = Account.objects.create(organization=org, code="5900", name="Cost of Goods Sold", type="Expense", normal_balance="Debit")
        inventory = Account.objects.create(organization=org, code="1300", name="Inventory", type="Asset", normal_balance="Debit")
        return SalesAccountingSettings.objects.create(
            organization=org, cash_account=accounts["cash"], revenue_account=revenue,
            cogs_account=cogs, inventory_account=inventory,
        )

    @pytest.fixture
    def sellable_item(self, client):
        from apps.asset_management.models import StockItem, StockLevel, AssetLocation, StockMovement

        org = client.organization
        location = AssetLocation.objects.create(organization=org, name="Bookstore")
        stock_item = StockItem.objects.create(organization=org, name="Annual Report 2026", sku="BOOK-001")
        movement = StockMovement.objects.create(
            item=stock_item, location=location, movement_type="in", quantity=100, unit_cost=20_000,
        )
        movement.apply()
        stock_item.refresh_from_db()

        from apps.asset_management.models import SellableItem
        return SellableItem.objects.create(
            stock_item=stock_item, category="book", sale_price=50_000,
        )

    def test_post_sale_creates_balanced_journal_and_stock_out(self, client, sales_settings, sellable_item):
        sale = Sale.objects.create(
            organization=client.organization,
            sale_no="SALE-001", date=datetime.date(2026, 3, 1),
            item=sellable_item, quantity=5, unit_price=50_000, total_amount=250_000,
        )
        resp = client.post(f"/api/accounting/sales/{sale.id}/post_sale/")
        assert resp.status_code == 200, resp.content
        sale.refresh_from_db()
        assert sale.status == "Posted"
        assert sale.journal_entry is not None
        assert sale.journal_entry.status == "Posted"
        debit, credit = sale.journal_entry.totals()
        assert debit == credit == 250_000 + (5 * 20_000)

        sellable_item.stock_item.refresh_from_db()
        assert sellable_item.stock_item.total_quantity == 95

    def test_post_sale_twice_rejected(self, client, sales_settings, sellable_item):
        sale = Sale.objects.create(
            organization=client.organization,
            sale_no="SALE-002", date=datetime.date(2026, 3, 1),
            item=sellable_item, quantity=1, unit_price=50_000, total_amount=50_000,
        )
        client.post(f"/api/accounting/sales/{sale.id}/post_sale/")
        resp = client.post(f"/api/accounting/sales/{sale.id}/post_sale/")
        assert resp.status_code == 400
