"""PRD §5.4 FRFM046 / FRFM049 — synchronous GL posting via post_save signal."""
from __future__ import annotations

from datetime import timedelta
from decimal import Decimal
from unittest.mock import patch

import pytest
from django.utils import timezone

from apps.core.audit.models import AuditLog
from apps.core.audit.tasks import persist_audit_log
from apps.rims.finance.models import (
    Budget,
    BudgetLine,
    DisbursementExecution,
    DisbursementRequest,
    GLEntry,
)
from apps.rims.finance.services_gl import post_gl_entries
from apps.rims.grants.models import Application, Award, GrantCall


@pytest.fixture(autouse=True)
def _sync_audit():
    with patch("apps.core.audit.tasks.write_audit_log.delay", side_effect=lambda **kw: persist_audit_log(**kw)):
        with patch(
            "apps.core.audit.mixins.transaction.on_commit",
            side_effect=lambda fn: fn(),
        ):
            yield


def _execution(applicant_user, institution, finance_user) -> DisbursementExecution:
    call = GrantCall.objects.create(
        title=f"6F-gl call {timezone.now().timestamp()}",
        slug=f"6f-gl-{int(timezone.now().timestamp() * 1000) % 10_000_000}",
        call_type="grant",
        opens_at=timezone.now() - timedelta(days=200),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    award = Award.objects.create(
        application=app,
        amount=Decimal("4000"),
        currency="USD",
        awarded_at=timezone.now(),
        project_end_date=timezone.now().date() + timedelta(days=365),
    )
    budget = Budget.objects.create(
        award=award, name="GL budget", currency="USD", account_reference="6100-EXP",
    )
    BudgetLine.objects.create(budget=budget, label="Line", amount=Decimal("4000"))
    request = DisbursementRequest.objects.create(
        budget=budget,
        amount=Decimal("750.00"),
        requested_by=finance_user,
        status=DisbursementRequest.Status.APPROVED,
    )
    return DisbursementExecution.objects.create(
        request=request,
        status=DisbursementExecution.Status.SCHEDULED,
    )


@pytest.mark.django_db
def test_post_gl_entries_writes_balanced_pair_and_audits(
    applicant_user, institution, finance_user
):
    """PRD §5.4 FRFM046 — direct service call writes debit + credit + audit row."""
    execution = _execution(applicant_user, institution, finance_user)
    entries = post_gl_entries(execution, actor=finance_user)
    assert len(entries) == 2
    debit, credit = entries
    assert debit.debit == Decimal("750.00")
    assert debit.credit == Decimal("0")
    assert credit.credit == Decimal("750.00")
    assert credit.debit == Decimal("0")
    # Balanced pair
    total_dr = sum(e.debit for e in entries)
    total_cr = sum(e.credit for e in entries)
    assert total_dr == total_cr
    assert AuditLog.objects.filter(action="GL_POSTED").exists()


@pytest.mark.django_db
def test_post_save_signal_posts_on_executed_transition(
    applicant_user, institution, finance_user
):
    """PRD §5.4 FRFM046 — signal fires when execution flips to EXECUTED."""
    execution = _execution(applicant_user, institution, finance_user)
    assert not GLEntry.objects.filter(execution=execution).exists()
    execution.status = DisbursementExecution.Status.EXECUTED
    execution.executed_at = timezone.now()
    execution.save()
    # The post_save signal should have created two GLEntry rows.
    assert GLEntry.objects.filter(execution=execution).count() == 2
    execution.refresh_from_db()
    assert execution.gl_posting_status == "posted"


@pytest.mark.django_db
def test_signal_failure_leaves_status_pending_no_500(
    applicant_user, institution, finance_user
):
    """PRD §5.4 FRFM046 — when the signal call raises, gl_posting_status
    stays pending and the save completes without propagating the error."""
    execution = _execution(applicant_user, institution, finance_user)
    # Patch at the source module (signals.py imports locally inside the handler).
    with patch(
        "apps.rims.finance.services_gl.post_gl_entries",
        side_effect=RuntimeError("simulated GL failure"),
    ):
        execution.status = DisbursementExecution.Status.EXECUTED
        execution.executed_at = timezone.now()
        # Must NOT raise into the caller.
        execution.save()
    execution.refresh_from_db()
    assert execution.gl_posting_status == "pending"
    assert not GLEntry.objects.filter(execution=execution).exists()
