"""
Property-based tests for the Double-Entry Invariant (Property 3).

Property 3: Double-Entry Invariant
For any set of posted JournalEntry records, the Trial Balance total debits
SHALL equal the Trial Balance total credits (within ±0.01 KES).

**Validates: Requirements 7.1, 7.2**

Uses Hypothesis to generate arbitrary sequences of balanced journal entries,
posts each via AccountingService.post_journal_entry, then asserts
ReconciliationService.verify_tb_balance().balanced is True.
"""

import uuid
from datetime import date
from decimal import Decimal

from hypothesis import given, settings, strategies as st
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 _valid_decimal(min_val: str = "1.00", max_val: str = "10000.00") -> st.SearchStrategy:
    """Return a strategy for positive Decimals with 2 dp."""
    return st.decimals(
        min_value=Decimal(min_val),
        max_value=Decimal(max_val),
        places=2,
        allow_nan=False,
        allow_infinity=False,
    )


@st.composite
def balanced_entry_amounts(draw):
    """
    Generate a list of (debit_account_index, credit_account_index, amount) tuples
    that together form a balanced journal entry.

    Strategy: pick one debit leg and one credit leg, same amount.
    Indices refer to the 4 accounts in the test fixture:
        0 = asset, 1 = equity, 2 = income, 3 = expense
    """
    # Debit side: asset or expense (indices 0, 3)
    debit_idx = draw(st.sampled_from([0, 3]))
    # Credit side: equity or income (indices 1, 2)
    credit_idx = draw(st.sampled_from([1, 2]))
    amount = draw(_valid_decimal())
    return (debit_idx, credit_idx, amount)


@st.composite
def entry_sequence(draw):
    """Generate a non-empty list of balanced entry specs (up to 5 entries)."""
    num = draw(st.integers(min_value=1, max_value=5))
    return [draw(balanced_entry_amounts()) for _ in range(num)]


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

class DoubleEntryInvariantPropertyTest(TestCase):
    """
    Property 3: Double-Entry Invariant
    **Validates: Requirements 7.1, 7.2**

    For any sequence of balanced journal entries posted via
    AccountingService.post_journal_entry, the Trial Balance generated by
    ReconciliationService.verify_tb_balance must report balanced=True
    (total_debits == total_credits within ±0.01 KES).
    """

    def setUp(self):
        """Create the minimal fixture that every hypothesis example reuses."""
        self.user = User.objects.create_user(
            username=f"tb_prop_user_{uuid.uuid4().hex[:8]}",
            email=f"tb_prop_{uuid.uuid4().hex[:8]}@example.com",
            password="testpass123",
        )

        self.branch = Branch.objects.create(
            name=f"PropTestBranch_{uuid.uuid4().hex[:6]}",
            code=f"PTB{uuid.uuid4().hex[:4].upper()}",
        )

        # Open fiscal period covering a wide date range so any generated
        # transaction_date falls inside it.
        self.fiscal_period = FiscalPeriod.objects.create(
            name="Property Test Period",
            period_type="monthly",
            start_date=date(2020, 1, 1),
            end_date=date(2030, 12, 31),
            status="open",
        )

        # Four accounts: one per type used by the strategies.
        self.accounts = [
            Account.objects.create(
                code=f"1{uuid.uuid4().hex[:5]}",
                name="Prop Asset",
                account_type="asset",
                subtype="current_asset",
                description="Property test asset account",
                is_active=True,
                created_by=self.user,
            ),
            Account.objects.create(
                code=f"2{uuid.uuid4().hex[:5]}",
                name="Prop Equity",
                account_type="equity",
                description="Property test equity account",
                is_active=True,
                created_by=self.user,
            ),
            Account.objects.create(
                code=f"3{uuid.uuid4().hex[:5]}",
                name="Prop Income",
                account_type="income",
                description="Property test income account",
                is_active=True,
                created_by=self.user,
            ),
            Account.objects.create(
                code=f"4{uuid.uuid4().hex[:5]}",
                name="Prop Expense",
                account_type="expense",
                description="Property test expense account",
                is_active=True,
                created_by=self.user,
            ),
        ]

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

    def _post_entry(self, debit_idx: int, credit_idx: int, amount: Decimal) -> None:
        """Build and post a single balanced journal entry."""
        je = JournalEntry.objects.create(
            reference_number=f"JE-PROP-{uuid.uuid4().hex[:10]}",
            transaction_date=date(2024, 6, 15),
            description=f"Property test entry {uuid.uuid4().hex[:6]}",
            branch=self.branch,
            created_by=self.user,
            status="draft",
        )
        JournalEntryLine.objects.create(
            journal_entry=je,
            account=self.accounts[debit_idx],
            description="Debit leg",
            debit_amount=amount,
            credit_amount=Decimal("0.00"),
            line_number=1,
        )
        JournalEntryLine.objects.create(
            journal_entry=je,
            account=self.accounts[credit_idx],
            description="Credit leg",
            debit_amount=Decimal("0.00"),
            credit_amount=amount,
            line_number=2,
        )
        self.accounting_service.post_journal_entry(je, self.user)

    @settings(max_examples=50, deadline=None)
    @given(entries=entry_sequence())
    def test_tb_balanced_after_arbitrary_entry_sequence(self, entries):
        """
        Property 3: Double-Entry Invariant
        **Validates: Requirements 7.1, 7.2**

        For any sequence of balanced journal entries, the Trial Balance
        MUST report balanced=True (total_debits == total_credits within ±0.01 KES).
        """
        # Post all generated entries
        for debit_idx, credit_idx, amount in entries:
            self._post_entry(debit_idx, credit_idx, amount)

        # Verify Trial Balance is balanced
        result = self.reconciliation_service.verify_tb_balance(
            as_of_date=date(2024, 6, 15),
            branch=None,  # all branches
        )

        assert result.balanced is True, (
            f"Trial Balance is not balanced after {len(entries)} posted entries. "
            f"Total debits: {result.total_debits}, "
            f"Total credits: {result.total_credits}, "
            f"Difference: {result.difference}"
        )
        assert result.difference < Decimal("0.01"), (
            f"Trial Balance difference {result.difference} exceeds tolerance of 0.01 KES"
        )

    @settings(max_examples=30, deadline=None)
    @given(amount=_valid_decimal())
    def test_tb_balanced_after_single_entry(self, amount):
        """
        Property 3 (single-entry case): a single balanced journal entry
        must keep the TB balanced.

        **Validates: Requirements 7.1, 7.2**
        """
        self._post_entry(
            debit_idx=0,   # asset (debit-normal)
            credit_idx=2,  # income (credit-normal)
            amount=amount,
        )

        result = self.reconciliation_service.verify_tb_balance(
            as_of_date=date(2024, 6, 15),
        )

        assert result.balanced is True, (
            f"TB imbalanced after a single entry of {amount}. "
            f"Debits={result.total_debits}, Credits={result.total_credits}"
        )

    @settings(max_examples=30, deadline=None)
    @given(entries=entry_sequence())
    def test_tb_totals_equal_within_tolerance(self, entries):
        """
        Property 3 (tolerance check): total_debits and total_credits
        must agree to within ±0.01 KES, not just an exact integer match.

        **Validates: Requirements 7.2**
        """
        for debit_idx, credit_idx, amount in entries:
            self._post_entry(debit_idx, credit_idx, amount)

        result = self.reconciliation_service.verify_tb_balance(
            as_of_date=date(2024, 6, 15),
        )

        assert abs(result.total_debits - result.total_credits) < Decimal("0.01"), (
            f"TB debit/credit mismatch exceeds 0.01 KES tolerance. "
            f"Debits={result.total_debits}, Credits={result.total_credits}"
        )
