"""
Property-based tests for IS–SFP Cross-Statement Reconciliation (Property 6).

Property 6: Cross-Statement IS–SFP Reconciliation
For any period [start, end], the Income Statement net income SHALL equal
the change in the sum of all equity account balances
(equity_closing − equity_opening) for the same period (within ±0.01 KES).

**Validates: Requirements 6.1, 6.2**

Uses Hypothesis to generate periods and sets of balanced journal entries
(including period-closing entries that transfer net income to retained
earnings), then asserts ReconciliationService.verify_is_sfp_reconciliation()
returns reconciled=True.
"""

import uuid
from datetime import date, timedelta
from decimal import Decimal

from hypothesis import given, settings, strategies as st, assume
from hypothesis.extra.django import TestCase

from django.contrib.auth import get_user_model

from accounting.models import (
    Account,
    FiscalPeriod,
    JournalEntry,
    JournalEntryLine,
)
from accounting.services.accounting_service import AccountingService
from accounting.services.reconciliation_service import ReconciliationService
from users.models import Branch

User = get_user_model()


# ---------------------------------------------------------------------------
# Hypothesis strategies
# ---------------------------------------------------------------------------

def _amount_strategy(
    min_val: str = "1.00", max_val: str = "5000.00"
) -> st.SearchStrategy:
    """Strategy for a positive Decimal with exactly 2 decimal places."""
    return st.decimals(
        min_value=Decimal(min_val),
        max_value=Decimal(max_val),
        places=2,
        allow_nan=False,
        allow_infinity=False,
    )


@st.composite
def income_expense_amounts(draw):
    """
    Generate lists of income and expense amounts for one test run.

    Returns (income_amounts, expense_amounts) where each is a list of
    1–4 Decimal values summing to at most 20 000 KES total to keep
    tests fast.
    """
    n_income = draw(st.integers(min_value=1, max_value=4))
    n_expense = draw(st.integers(min_value=0, max_value=4))

    income_amounts = [draw(_amount_strategy()) for _ in range(n_income)]
    expense_amounts = [draw(_amount_strategy()) for _ in range(n_expense)]

    return income_amounts, expense_amounts


# ---------------------------------------------------------------------------
# Test case
# ---------------------------------------------------------------------------

class ISSFPReconciliationPropertyTest(TestCase):
    """
    Property 6: Cross-Statement IS–SFP Reconciliation
    **Validates: Requirements 6.1, 6.2**

    For any period and any set of posted balanced journal entries whose net
    effect is transferred to retained earnings via a closing entry, the
    Income Statement net income MUST equal the change in total equity
    (equity_closing − equity_opening) within ±0.01 KES.
    """

    def setUp(self):
        """
        Create the minimal shared fixture.

        Hypothesis runs setUp/tearDown around each example so every
        example gets a clean database state (hypothesis.extra.django.TestCase
        wraps each example in a transaction that is rolled back).
        """
        suffix = uuid.uuid4().hex[:8]

        self.user = User.objects.create_user(
            username=f"is_sfp_user_{suffix}",
            email=f"is_sfp_{suffix}@example.com",
            password="testpass123",
        )

        self.branch = Branch.objects.create(
            name=f"ISSFPBranch_{suffix}",
            code=f"IS{suffix[:5].upper()}",
        )

        # Open fiscal period wide enough for any test date.
        self.fiscal_period = FiscalPeriod.objects.create(
            name="IS-SFP Property Test Period",
            period_type="monthly",
            start_date=date(2020, 1, 1),
            end_date=date(2030, 12, 31),
            status="open",
        )

        # --- Chart of Accounts for the property tests ---

        # Asset account — used as the "cash" counterpart for income receipts.
        self.cash_account = Account.objects.create(
            code=f"10{suffix[:4]}",
            name="PropTest Cash",
            account_type="asset",
            subtype="current_asset",
            description="Property test cash account",
            is_active=True,
            created_by=self.user,
        )

        # Liability account — used as a counterpart for expense payments.
        self.payable_account = Account.objects.create(
            code=f"20{suffix[:4]}",
            name="PropTest Payable",
            account_type="liability",
            subtype="current_liability",
            description="Property test payable account",
            is_active=True,
            created_by=self.user,
        )

        # Equity / retained earnings — receives the period closing entry.
        self.retained_earnings = Account.objects.create(
            code=f"30{suffix[:4]}",
            name="PropTest Retained Earnings",
            account_type="equity",
            description="Property test retained earnings",
            is_active=True,
            created_by=self.user,
        )

        # Income account — credit-normal; period activity = revenue earned.
        self.income_account = Account.objects.create(
            code=f"40{suffix[:4]}",
            name="PropTest Income",
            account_type="income",
            description="Property test income account",
            is_active=True,
            created_by=self.user,
        )

        # Expense account — debit-normal; period activity = costs incurred.
        self.expense_account = Account.objects.create(
            code=f"50{suffix[:4]}",
            name="PropTest Expense",
            account_type="expense",
            description="Property test expense account",
            is_active=True,
            created_by=self.user,
        )

        self.accounting_service = AccountingService()
        self.reconciliation_service = ReconciliationService()

        # Fixed test period (within the open fiscal period).
        self.start_date = date(2024, 3, 1)
        self.end_date = date(2024, 3, 31)

    # -----------------------------------------------------------------------
    # Helper methods
    # -----------------------------------------------------------------------

    def _ref(self, prefix: str = "JE") -> str:
        """Generate a unique journal-entry reference number."""
        return f"{prefix}-{uuid.uuid4().hex[:12].upper()}"

    def _post_income_entry(self, amount: Decimal, tx_date: date) -> None:
        """
        Post a balanced income entry:
            Dr Cash (asset)   amount
            Cr Income         amount
        This increases the IS income by `amount` and leaves equity unchanged.
        """
        je = JournalEntry.objects.create(
            reference_number=self._ref("INC"),
            transaction_date=tx_date,
            description="Income receipt",
            branch=self.branch,
            created_by=self.user,
            status="draft",
        )
        JournalEntryLine.objects.create(
            journal_entry=je,
            account=self.cash_account,
            description="Cash in",
            debit_amount=amount,
            credit_amount=Decimal("0.00"),
            line_number=1,
        )
        JournalEntryLine.objects.create(
            journal_entry=je,
            account=self.income_account,
            description="Income earned",
            debit_amount=Decimal("0.00"),
            credit_amount=amount,
            line_number=2,
        )
        self.accounting_service.post_journal_entry(je, self.user)

    def _post_expense_entry(self, amount: Decimal, tx_date: date) -> None:
        """
        Post a balanced expense entry:
            Dr Expense          amount
            Cr Payable          amount
        This increases the IS expense by `amount` and leaves equity unchanged.
        """
        je = JournalEntry.objects.create(
            reference_number=self._ref("EXP"),
            transaction_date=tx_date,
            description="Expense incurred",
            branch=self.branch,
            created_by=self.user,
            status="draft",
        )
        JournalEntryLine.objects.create(
            journal_entry=je,
            account=self.expense_account,
            description="Expense",
            debit_amount=amount,
            credit_amount=Decimal("0.00"),
            line_number=1,
        )
        JournalEntryLine.objects.create(
            journal_entry=je,
            account=self.payable_account,
            description="Payable",
            debit_amount=Decimal("0.00"),
            credit_amount=amount,
            line_number=2,
        )
        self.accounting_service.post_journal_entry(je, self.user)

    def _post_closing_entry(
        self, net_income: Decimal, tx_date: date
    ) -> None:
        """
        Post the period-closing entry that transfers net income to retained
        earnings, making equity_change == IS net_income.

        If net_income > 0 (profitable period):
            Dr Income (zero out)     total_income
            Cr Expense (zero out)    total_expense
            Cr Retained Earnings     net_income
        Implemented as two separate balanced entries for simplicity:

        Entry A — close income account:
            Dr Income         total_income
            Cr Retained Earn  total_income

        Entry B — close expense account:
            Dr Retained Earn  total_expense
            Cr Expense        total_expense

        Net effect on retained earnings = total_income − total_expense = net_income.
        """
        # Calculate period totals from the income statement
        is_report = self.reconciliation_service.report_service.generate_income_statement(
            self.start_date, self.end_date, None
        )
        total_income = is_report["totals"]["gross_income"]
        total_expense = is_report["totals"]["total_expenses"]

        # Only post closing entries when there is activity to close.
        if total_income > Decimal("0.00"):
            # Entry A: close income → retained earnings
            je_a = JournalEntry.objects.create(
                reference_number=self._ref("CLO-A"),
                transaction_date=tx_date,
                description="Period closing — income",
                branch=self.branch,
                created_by=self.user,
                status="draft",
            )
            JournalEntryLine.objects.create(
                journal_entry=je_a,
                account=self.income_account,
                description="Close income",
                debit_amount=total_income,
                credit_amount=Decimal("0.00"),
                line_number=1,
            )
            JournalEntryLine.objects.create(
                journal_entry=je_a,
                account=self.retained_earnings,
                description="Transfer to retained earnings",
                debit_amount=Decimal("0.00"),
                credit_amount=total_income,
                line_number=2,
            )
            self.accounting_service.post_journal_entry(je_a, self.user)

        if total_expense > Decimal("0.00"):
            # Entry B: close expenses ← retained earnings
            je_b = JournalEntry.objects.create(
                reference_number=self._ref("CLO-B"),
                transaction_date=tx_date,
                description="Period closing — expense",
                branch=self.branch,
                created_by=self.user,
                status="draft",
            )
            JournalEntryLine.objects.create(
                journal_entry=je_b,
                account=self.retained_earnings,
                description="Fund expense from retained earnings",
                debit_amount=total_expense,
                credit_amount=Decimal("0.00"),
                line_number=1,
            )
            JournalEntryLine.objects.create(
                journal_entry=je_b,
                account=self.expense_account,
                description="Close expense",
                debit_amount=Decimal("0.00"),
                credit_amount=total_expense,
                line_number=2,
            )
            self.accounting_service.post_journal_entry(je_b, self.user)

    # -----------------------------------------------------------------------
    # Property tests
    # -----------------------------------------------------------------------

    @settings(max_examples=40, deadline=None)
    @given(amounts=income_expense_amounts())
    def test_is_sfp_reconciled_after_income_expense_and_closing(self, amounts):
        """
        Property 6: Cross-Statement IS–SFP Reconciliation
        **Validates: Requirements 6.1, 6.2**

        For any combination of income and expense entries followed by a
        period-closing entry (transferring net income to retained earnings),
        verify_is_sfp_reconciliation().reconciled MUST be True and
        difference MUST be < 0.01 KES.
        """
        income_amounts, expense_amounts = amounts

        # Post income receipts within the test period
        tx_date = date(2024, 3, 15)
        for amount in income_amounts:
            self._post_income_entry(amount, tx_date)

        # Post expense payments within the test period
        for amount in expense_amounts:
            self._post_expense_entry(amount, tx_date)

        # Post the period-closing entry on the last day of the period so that
        # it falls within [start_date, end_date] for the IS but after the
        # income/expense entries.
        closing_date = self.end_date

        # Calculate expected net income before posting closing entry
        is_report_pre = self.reconciliation_service.report_service.generate_income_statement(
            self.start_date, self.end_date, None
        )
        expected_net_income = is_report_pre["totals"]["net_income"]

        self._post_closing_entry(expected_net_income, closing_date)

        # -------------------------------------------------------------------
        # Assert Property 6: IS net_income == equity_change
        # -------------------------------------------------------------------
        result = self.reconciliation_service.verify_is_sfp_reconciliation(
            start_date=self.start_date,
            end_date=self.end_date,
            branch=None,
        )

        assert result.reconciled is True, (
            f"IS–SFP reconciliation failed.\n"
            f"  IS net income:   {result.is_net_income}\n"
            f"  Equity opening:  {result.equity_opening}\n"
            f"  Equity closing:  {result.equity_closing}\n"
            f"  Equity change:   {result.equity_change}\n"
            f"  Difference:      {result.difference}\n"
            f"  Income amounts:  {income_amounts}\n"
            f"  Expense amounts: {expense_amounts}"
        )
        assert result.difference < Decimal("0.01"), (
            f"Reconciliation difference {result.difference} exceeds ±0.01 KES tolerance."
        )

    @settings(max_examples=30, deadline=None)
    @given(income_amount=_amount_strategy())
    def test_single_income_entry_with_closing_reconciles(self, income_amount):
        """
        Property 6 (single income entry): one income receipt + closing entry
        must produce a reconciled IS–SFP result.

        **Validates: Requirements 6.1, 6.2**
        """
        tx_date = date(2024, 3, 10)
        self._post_income_entry(income_amount, tx_date)
        self._post_closing_entry(income_amount, self.end_date)

        result = self.reconciliation_service.verify_is_sfp_reconciliation(
            start_date=self.start_date,
            end_date=self.end_date,
        )

        assert result.reconciled is True, (
            f"Single income entry did not reconcile.\n"
            f"  Income amount:  {income_amount}\n"
            f"  IS net income:  {result.is_net_income}\n"
            f"  Equity change:  {result.equity_change}\n"
            f"  Difference:     {result.difference}"
        )

    @settings(max_examples=20, deadline=None)
    @given(
        income_amount=_amount_strategy(),
        expense_amount=_amount_strategy(),
    )
    def test_income_and_expense_with_closing_reconciles(
        self, income_amount, expense_amount
    ):
        """
        Property 6 (income + expense): income receipt, expense payment, and
        a closing entry transferring net income to retained earnings must
        produce a reconciled IS–SFP result.

        **Validates: Requirements 6.1, 6.2**
        """
        tx_date = date(2024, 3, 20)
        self._post_income_entry(income_amount, tx_date)
        self._post_expense_entry(expense_amount, tx_date)
        self._post_closing_entry(income_amount - expense_amount, self.end_date)

        result = self.reconciliation_service.verify_is_sfp_reconciliation(
            start_date=self.start_date,
            end_date=self.end_date,
        )

        assert result.reconciled is True, (
            f"Income + expense scenario did not reconcile.\n"
            f"  Income:        {income_amount}\n"
            f"  Expense:       {expense_amount}\n"
            f"  IS net income: {result.is_net_income}\n"
            f"  Equity change: {result.equity_change}\n"
            f"  Difference:    {result.difference}"
        )

    @settings(max_examples=20, deadline=None)
    @given(amount=_amount_strategy())
    def test_zero_activity_period_reconciles(self, amount):
        """
        Property 6 (no-op period): if no entries are posted in the period,
        net income is zero and equity change is zero — the result must be
        reconciled.

        **Validates: Requirements 6.1, 6.2**
        """
        # Post an entry BEFORE the test period so it doesn't affect period
        # activity, but does give equity a non-zero opening balance.
        pre_period_date = self.start_date - timedelta(days=5)

        # Post an opening equity entry before the period starts
        je_pre = JournalEntry.objects.create(
            reference_number=self._ref("PRE"),
            transaction_date=pre_period_date,
            description="Pre-period equity injection",
            branch=self.branch,
            created_by=self.user,
            status="draft",
        )
        JournalEntryLine.objects.create(
            journal_entry=je_pre,
            account=self.cash_account,
            description="Cash in",
            debit_amount=amount,
            credit_amount=Decimal("0.00"),
            line_number=1,
        )
        JournalEntryLine.objects.create(
            journal_entry=je_pre,
            account=self.retained_earnings,
            description="Equity",
            debit_amount=Decimal("0.00"),
            credit_amount=amount,
            line_number=2,
        )
        self.accounting_service.post_journal_entry(je_pre, self.user)

        # No entries in the test period — net income and equity change both 0.
        result = self.reconciliation_service.verify_is_sfp_reconciliation(
            start_date=self.start_date,
            end_date=self.end_date,
        )

        assert result.reconciled is True, (
            f"Zero-activity period did not reconcile.\n"
            f"  IS net income:  {result.is_net_income}\n"
            f"  Equity change:  {result.equity_change}\n"
            f"  Difference:     {result.difference}"
        )
        assert result.is_net_income == Decimal("0.00"), (
            f"Expected zero IS net income for empty period, got {result.is_net_income}"
        )
        assert result.equity_change == Decimal("0.00"), (
            f"Expected zero equity change for empty period, got {result.equity_change}"
        )
