"""
Property-based tests for loan calculation methods.

These tests use Hypothesis to verify universal properties that should hold
across all valid inputs, ensuring correctness of calculation logic.

Feature: comprehensive-reports-and-fixes
"""

from hypothesis import given, strategies as st, settings
from hypothesis.extra.django import TestCase
from django.utils import timezone
from datetime import timedelta
from decimal import Decimal
import uuid

from loans.models import Loan, LoanProduct, LoanApplication, Repayment, PenaltyCharge
from users.models import CustomUser


# Custom strategies for generating test data
@st.composite
def decimal_amount_strategy(draw, min_value=1000, max_value=100000):
    """Generate a valid decimal amount for loan calculations"""
    return draw(st.decimals(
        min_value=Decimal(str(min_value)),
        max_value=Decimal(str(max_value)),
        places=2,
        allow_nan=False,
        allow_infinity=False
    ))


@st.composite
def loan_amounts_strategy(draw):
    """Generate valid loan amounts (principal, interest, processing fee)"""
    principal = draw(decimal_amount_strategy(min_value=1000, max_value=100000))
    interest = draw(decimal_amount_strategy(min_value=0, max_value=50000))
    processing_fee = draw(decimal_amount_strategy(min_value=0, max_value=5000))
    return principal, interest, processing_fee


@st.composite
def repayment_amounts_strategy(draw, max_total):
    """Generate a list of repayment amounts that don't exceed max_total"""
    num_repayments = draw(st.integers(min_value=0, max_value=10))
    if num_repayments == 0:
        return []
    
    # Generate repayment amounts that sum to at most max_total
    amounts = []
    remaining = max_total
    for i in range(num_repayments):
        if remaining <= 0:
            break
        # Each repayment is between 100 and remaining/2 (to allow multiple repayments)
        max_amount = min(remaining, max_total / 2) if i < num_repayments - 1 else remaining
        amount = draw(st.decimals(
            min_value=Decimal('100'),
            max_value=max(Decimal('100'), max_amount),
            places=2,
            allow_nan=False,
            allow_infinity=False
        ))
        amounts.append(amount)
        remaining -= amount
    
    return amounts


@st.composite
def penalty_amounts_strategy(draw):
    """Generate a list of penalty amounts"""
    num_penalties = draw(st.integers(min_value=0, max_value=5))
    amounts = []
    for _ in range(num_penalties):
        amount = draw(decimal_amount_strategy(min_value=100, max_value=5000))
        amounts.append(amount)
    return amounts


class TestTotalAmountCalculation(TestCase):
    """
    Test Property 13: Total Amount Calculation
    
    Feature: comprehensive-reports-and-fixes, Property 13: Total Amount Calculation
    Validates: Requirements 3.11, 9.1
    
    For any loan, the total_amount should always equal 
    principal_amount + interest_amount + processing_fee.
    """
    
    def setUp(self):
        """Set up test data"""
        # Create test user
        self.user = CustomUser.objects.create_user(
            username='testuser_calc',
            email='test_calc@example.com',
            phone_number='+254700000001',
            first_name='Test',
            last_name='User'
        )
        
        # Create test loan product
        self.product = LoanProduct.objects.create(
            name='Test Product',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
    
    @settings(max_examples=50, deadline=None)
    @given(loan_amounts=loan_amounts_strategy())
    def test_total_amount_equals_sum_of_components(self, loan_amounts):
        """
        Property: For any loan, total_amount should equal 
        principal_amount + interest_amount + processing_fee.
        """
        principal, interest, processing_fee = loan_amounts
        
        # Create loan application
        app = LoanApplication.objects.create(
            application_number=f'APP-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Calculate expected total
        expected_total = principal + interest + processing_fee
        
        # Create loan with specified amounts
        loan = Loan.objects.create(
            loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=self.user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=expected_total,  # Set to expected value
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Verify total_amount equals sum of components
        assert loan.total_amount == expected_total, \
            f"Total amount {loan.total_amount} != expected {expected_total} " \
            f"(principal={principal} + interest={interest} + processing_fee={processing_fee})"
        
        # Cleanup
        loan.delete()
        app.delete()
    
    @settings(max_examples=50, deadline=None)
    @given(loan_amounts=loan_amounts_strategy())
    def test_recalculate_amounts_updates_total_correctly(self, loan_amounts):
        """
        Property: After calling recalculate_amounts(), total_amount should equal
        principal_amount + interest_amount + processing_fee.
        """
        principal, interest, processing_fee = loan_amounts
        
        # Create loan application
        app = LoanApplication.objects.create(
            application_number=f'APP-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan with incorrect total_amount
        loan = Loan.objects.create(
            loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=self.user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=Decimal('0.00'),  # Intentionally wrong
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Call recalculate_amounts
        loan.recalculate_amounts()
        
        # Verify total_amount is now correct
        expected_total = principal + interest + processing_fee
        assert loan.total_amount == expected_total, \
            f"After recalculate_amounts(), total {loan.total_amount} != expected {expected_total}"
        
        # Cleanup
        loan.delete()
        app.delete()


class TestOutstandingAmountCalculation(TestCase):
    """
    Test Property 25: Outstanding Amount Calculation
    
    Feature: comprehensive-reports-and-fixes, Property 25: Outstanding Amount Calculation
    Validates: Requirements 9.2
    
    For any loan, the outstanding_amount should always equal
    total_amount + total_penalties - amount_paid.
    """
    
    def setUp(self):
        """Set up test data"""
        # Create test user
        self.user = CustomUser.objects.create_user(
            username='testuser_outstanding',
            email='test_outstanding@example.com',
            phone_number='+254700000002',
            first_name='Test',
            last_name='User'
        )
        
        # Create test loan product
        self.product = LoanProduct.objects.create(
            name='Test Product Outstanding',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
    
    @settings(max_examples=50, deadline=None)
    @given(
        loan_amounts=loan_amounts_strategy(),
        repayment_amounts=st.data(),
        penalty_amounts=penalty_amounts_strategy()
    )
    def test_outstanding_amount_equals_formula(self, loan_amounts, repayment_amounts, penalty_amounts):
        """
        Property: For any loan, outstanding_amount should equal
        total_amount + total_penalties - amount_paid.
        """
        principal, interest, processing_fee = loan_amounts
        total_amount = principal + interest + processing_fee
        
        # Generate repayment amounts that don't exceed total_amount
        repayments = repayment_amounts.draw(repayment_amounts_strategy(max_total=total_amount))
        
        # Create loan application
        app = LoanApplication.objects.create(
            application_number=f'APP-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan
        loan = Loan.objects.create(
            loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=self.user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Create repayments
        amount_paid = Decimal('0.00')
        for i, amount in enumerate(repayments):
            Repayment.objects.create(
                loan=loan,
                amount=amount,
                payment_method='mpesa',
                receipt_number=f'RCP-TEST-{uuid.uuid4().hex[:8]}',
                payment_date=timezone.now() - timedelta(days=i)
            )
            amount_paid += amount
        
        # Create penalties
        total_penalties = Decimal('0.00')
        for amount in penalty_amounts:
            PenaltyCharge.objects.create(
                loan=loan,
                amount=amount,
                penalty_rate=Decimal('5.0'),
                days_overdue=1,
                outstanding_amount=total_amount,
                is_automatic=True
            )
            total_penalties += amount
        
        # Calculate expected outstanding amount
        expected_outstanding = total_amount + total_penalties - amount_paid
        
        # Get actual outstanding amount (property)
        actual_outstanding = loan.outstanding_amount
        
        # Verify outstanding_amount equals formula
        assert actual_outstanding == expected_outstanding, \
            f"Outstanding amount {actual_outstanding} != expected {expected_outstanding} " \
            f"(total={total_amount} + penalties={total_penalties} - paid={amount_paid})"
        
        # Cleanup
        loan.delete()
        app.delete()


class TestAmountPaidCalculation(TestCase):
    """
    Test Property 26: Amount Paid Calculation
    
    Feature: comprehensive-reports-and-fixes, Property 26: Amount Paid Calculation
    Validates: Requirements 9.3
    
    For any loan, the amount_paid should equal the sum of all repayment amounts.
    """
    
    def setUp(self):
        """Set up test data"""
        # Create test user
        self.user = CustomUser.objects.create_user(
            username='testuser_paid',
            email='test_paid@example.com',
            phone_number='+254700000003',
            first_name='Test',
            last_name='User'
        )
        
        # Create test loan product
        self.product = LoanProduct.objects.create(
            name='Test Product Paid',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
    
    @settings(max_examples=50, deadline=None)
    @given(
        loan_amounts=loan_amounts_strategy(),
        repayment_amounts=st.data()
    )
    def test_amount_paid_equals_sum_of_repayments(self, loan_amounts, repayment_amounts):
        """
        Property: For any loan, amount_paid should equal the sum of all repayment amounts.
        """
        principal, interest, processing_fee = loan_amounts
        total_amount = principal + interest + processing_fee
        
        # Generate repayment amounts that don't exceed total_amount
        repayments = repayment_amounts.draw(repayment_amounts_strategy(max_total=total_amount))
        
        # Create loan application
        app = LoanApplication.objects.create(
            application_number=f'APP-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan
        loan = Loan.objects.create(
            loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=self.user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Create repayments and calculate expected sum
        expected_amount_paid = Decimal('0.00')
        for i, amount in enumerate(repayments):
            Repayment.objects.create(
                loan=loan,
                amount=amount,
                payment_method='mpesa',
                receipt_number=f'RCP-TEST-{uuid.uuid4().hex[:8]}',
                payment_date=timezone.now() - timedelta(days=i)
            )
            expected_amount_paid += amount
        
        # Get actual amount_paid (property)
        actual_amount_paid = loan.amount_paid
        
        # Verify amount_paid equals sum of repayments
        assert actual_amount_paid == expected_amount_paid, \
            f"Amount paid {actual_amount_paid} != expected {expected_amount_paid} " \
            f"(sum of {len(repayments)} repayments)"
        
        # Special case: if no repayments, amount_paid should be zero
        if len(repayments) == 0:
            assert actual_amount_paid == Decimal('0.00'), \
                f"Loan with no repayments should have amount_paid = 0, got {actual_amount_paid}"
        
        # Cleanup
        loan.delete()
        app.delete()
    
    @settings(max_examples=50, deadline=None)
    @given(loan_amounts=loan_amounts_strategy())
    def test_amount_paid_zero_for_no_repayments(self, loan_amounts):
        """
        Property: For any loan with no repayments, amount_paid should equal zero.
        """
        principal, interest, processing_fee = loan_amounts
        total_amount = principal + interest + processing_fee
        
        # Create loan application
        app = LoanApplication.objects.create(
            application_number=f'APP-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan with no repayments
        loan = Loan.objects.create(
            loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=self.user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Verify amount_paid is zero
        assert loan.amount_paid == Decimal('0.00'), \
            f"Loan with no repayments should have amount_paid = 0, got {loan.amount_paid}"
        
        # Cleanup
        loan.delete()
        app.delete()


class TestAmountPaidInvariant(TestCase):
    """
    Test Property 29: Amount Paid Invariant
    
    Feature: comprehensive-reports-and-fixes, Property 29: Amount Paid Invariant
    Validates: Requirements 9.9
    
    For any loan at any point in time, amount_paid should never exceed
    total_amount + total_penalties.
    """
    
    @settings(max_examples=50, deadline=None)
    @given(
        loan_amounts=loan_amounts_strategy(),
        repayment_amounts=st.data(),
        penalty_amounts=penalty_amounts_strategy()
    )
    def test_amount_paid_never_exceeds_total_plus_penalties(self, loan_amounts, repayment_amounts, penalty_amounts):
        """
        Property: For any loan, amount_paid should never exceed total_amount + total_penalties.
        
        This is a critical invariant that ensures data integrity - we cannot have
        received more payment than what is owed.
        """
        principal, interest, processing_fee = loan_amounts
        total_amount = principal + interest + processing_fee
        
        # Create test user with unique phone number
        user = CustomUser.objects.create_user(
            username=f'testuser_{uuid.uuid4().hex[:8]}',
            email=f'test_{uuid.uuid4().hex[:8]}@example.com',
            phone_number=f'+2547{uuid.uuid4().hex[:8]}',
            first_name='Test',
            last_name='User'
        )
        
        # Create test loan product
        product = LoanProduct.objects.create(
            name=f'Test Product {uuid.uuid4().hex[:6]}',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        # Create loan application
        app = LoanApplication.objects.create(
            application_number=f'APP-{uuid.uuid4().hex[:6]}',
            borrower=user,
            loan_product=product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan
        loan = Loan.objects.create(
            loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Create penalties first to know total owed
        total_penalties = Decimal('0.00')
        for amount in penalty_amounts:
            PenaltyCharge.objects.create(
                loan=loan,
                amount=amount,
                penalty_rate=Decimal('5.0'),
                days_overdue=1,
                outstanding_amount=total_amount,
                is_automatic=True
            )
            total_penalties += amount
        
        # Calculate maximum allowed payment
        max_allowed = total_amount + total_penalties
        
        # Generate repayment amounts that don't exceed max_allowed
        repayments = repayment_amounts.draw(repayment_amounts_strategy(max_total=max_allowed))
        
        # Create repayments
        for i, amount in enumerate(repayments):
            Repayment.objects.create(
                loan=loan,
                amount=amount,
                payment_method='mpesa',
                receipt_number=f'RCP-TEST-{uuid.uuid4().hex[:8]}',
                payment_date=timezone.now() - timedelta(days=i)
            )
        
        # Get actual values
        amount_paid = loan.amount_paid
        total_owed = total_amount + total_penalties
        
        # Verify invariant: amount_paid <= total_amount + total_penalties
        assert amount_paid <= total_owed, \
            f"INVARIANT VIOLATION: amount_paid ({amount_paid}) exceeds " \
            f"total_amount + total_penalties ({total_owed}). " \
            f"Details: total={total_amount}, penalties={total_penalties}, paid={amount_paid}"
    
    @settings(max_examples=50, deadline=None)
    @given(loan_amounts=loan_amounts_strategy())
    def test_amount_paid_invariant_with_no_penalties(self, loan_amounts):
        """
        Property: For any loan with no penalties, amount_paid should never exceed total_amount.
        
        This is a special case of the invariant where total_penalties = 0.
        """
        principal, interest, processing_fee = loan_amounts
        total_amount = principal + interest + processing_fee
        
        # Create test user with unique phone number
        user = CustomUser.objects.create_user(
            username=f'testuser_{uuid.uuid4().hex[:8]}',
            email=f'test_{uuid.uuid4().hex[:8]}@example.com',
            phone_number=f'+2547{uuid.uuid4().hex[:8]}',
            first_name='Test',
            last_name='User'
        )
        
        # Create test loan product
        product = LoanProduct.objects.create(
            name=f'Test Product {uuid.uuid4().hex[:6]}',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        # Create loan application
        app = LoanApplication.objects.create(
            application_number=f'APP-{uuid.uuid4().hex[:6]}',
            borrower=user,
            loan_product=product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan
        loan = Loan.objects.create(
            loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Create a single repayment that doesn't exceed total
        repayment_amount = min(total_amount, total_amount / Decimal('2'))
        Repayment.objects.create(
            loan=loan,
            amount=repayment_amount,
            payment_method='mpesa',
            receipt_number=f'RCP-TEST-{uuid.uuid4().hex[:8]}',
            payment_date=timezone.now()
        )
        
        # Get actual values
        amount_paid = loan.amount_paid
        
        # Verify invariant: amount_paid <= total_amount (no penalties)
        assert amount_paid <= total_amount, \
            f"INVARIANT VIOLATION: amount_paid ({amount_paid}) exceeds " \
            f"total_amount ({total_amount}) with no penalties"


class TestOutstandingAmountInvariant(TestCase):
    """
    Test Property 30: Outstanding Amount Invariant
    
    Feature: comprehensive-reports-and-fixes, Property 30: Outstanding Amount Invariant
    Validates: Requirements 9.10
    
    For any loan at any point in time, outstanding_amount should never be negative.
    """
    
    @settings(max_examples=50, deadline=None)
    @given(
        loan_amounts=loan_amounts_strategy(),
        repayment_amounts=st.data(),
        penalty_amounts=penalty_amounts_strategy()
    )
    def test_outstanding_amount_never_negative(self, loan_amounts, repayment_amounts, penalty_amounts):
        """
        Property: For any loan, outstanding_amount should never be negative.
        
        This is a critical invariant that ensures data integrity - we cannot owe
        a negative amount (borrower cannot owe us less than zero).
        """
        principal, interest, processing_fee = loan_amounts
        total_amount = principal + interest + processing_fee
        
        # Create test user with unique phone number
        user = CustomUser.objects.create_user(
            username=f'testuser_{uuid.uuid4().hex[:8]}',
            email=f'test_{uuid.uuid4().hex[:8]}@example.com',
            phone_number=f'+2547{uuid.uuid4().hex[:8]}',
            first_name='Test',
            last_name='User'
        )
        
        # Create test loan product
        product = LoanProduct.objects.create(
            name=f'Test Product {uuid.uuid4().hex[:6]}',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        # Create loan application
        app = LoanApplication.objects.create(
            application_number=f'APP-{uuid.uuid4().hex[:6]}',
            borrower=user,
            loan_product=product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan
        loan = Loan.objects.create(
            loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Create penalties
        total_penalties = Decimal('0.00')
        for amount in penalty_amounts:
            PenaltyCharge.objects.create(
                loan=loan,
                amount=amount,
                penalty_rate=Decimal('5.0'),
                days_overdue=1,
                outstanding_amount=total_amount,
                is_automatic=True
            )
            total_penalties += amount
        
        # Calculate maximum allowed payment to ensure outstanding >= 0
        max_allowed = total_amount + total_penalties
        
        # Generate repayment amounts that don't exceed max_allowed
        repayments = repayment_amounts.draw(repayment_amounts_strategy(max_total=max_allowed))
        
        # Create repayments
        for i, amount in enumerate(repayments):
            Repayment.objects.create(
                loan=loan,
                amount=amount,
                payment_method='mpesa',
                receipt_number=f'RCP-TEST-{uuid.uuid4().hex[:8]}',
                payment_date=timezone.now() - timedelta(days=i)
            )
        
        # Get outstanding amount
        outstanding = loan.outstanding_amount
        
        # Verify invariant: outstanding_amount >= 0
        assert outstanding >= Decimal('0.00'), \
            f"INVARIANT VIOLATION: outstanding_amount ({outstanding}) is negative. " \
            f"Details: total={total_amount}, penalties={total_penalties}, " \
            f"paid={loan.amount_paid}, outstanding={outstanding}"
    
    @settings(max_examples=50, deadline=None)
    @given(loan_amounts=loan_amounts_strategy())
    def test_outstanding_amount_equals_zero_when_fully_paid(self, loan_amounts):
        """
        Property: For any loan where amount_paid equals total_amount + total_penalties,
        outstanding_amount should equal zero (not negative).
        
        This tests the boundary case where the loan is exactly paid off.
        """
        principal, interest, processing_fee = loan_amounts
        total_amount = principal + interest + processing_fee
        
        # Create test user with unique phone number
        user = CustomUser.objects.create_user(
            username=f'testuser_{uuid.uuid4().hex[:8]}',
            email=f'test_{uuid.uuid4().hex[:8]}@example.com',
            phone_number=f'+2547{uuid.uuid4().hex[:8]}',
            first_name='Test',
            last_name='User'
        )
        
        # Create test loan product
        product = LoanProduct.objects.create(
            name=f'Test Product {uuid.uuid4().hex[:6]}',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        # Create loan application
        app = LoanApplication.objects.create(
            application_number=f'APP-{uuid.uuid4().hex[:6]}',
            borrower=user,
            loan_product=product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan
        loan = Loan.objects.create(
            loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Create a repayment that exactly pays off the loan
        Repayment.objects.create(
            loan=loan,
            amount=total_amount,
            payment_method='mpesa',
            receipt_number=f'RCP-TEST-{uuid.uuid4().hex[:8]}',
            payment_date=timezone.now()
        )
        
        # Get outstanding amount
        outstanding = loan.outstanding_amount
        
        # Verify outstanding is exactly zero (not negative)
        assert outstanding == Decimal('0.00'), \
            f"Fully paid loan should have outstanding = 0, got {outstanding}. " \
            f"Details: total={total_amount}, paid={loan.amount_paid}"
        
        # Verify it's not negative
        assert outstanding >= Decimal('0.00'), \
            f"INVARIANT VIOLATION: outstanding_amount ({outstanding}) is negative for fully paid loan"
    
    @settings(max_examples=50, deadline=None)
    @given(loan_amounts=loan_amounts_strategy())
    def test_outstanding_amount_positive_for_unpaid_loan(self, loan_amounts):
        """
        Property: For any loan with no repayments, outstanding_amount should equal
        total_amount (and be positive).
        
        This tests that unpaid loans have positive outstanding amounts.
        """
        principal, interest, processing_fee = loan_amounts
        total_amount = principal + interest + processing_fee
        
        # Create test user with unique phone number
        user = CustomUser.objects.create_user(
            username=f'testuser_{uuid.uuid4().hex[:8]}',
            email=f'test_{uuid.uuid4().hex[:8]}@example.com',
            phone_number=f'+2547{uuid.uuid4().hex[:8]}',
            first_name='Test',
            last_name='User'
        )
        
        # Create test loan product
        product = LoanProduct.objects.create(
            name=f'Test Product {uuid.uuid4().hex[:6]}',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        # Create loan application
        app = LoanApplication.objects.create(
            application_number=f'APP-{uuid.uuid4().hex[:6]}',
            borrower=user,
            loan_product=product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan with no repayments
        loan = Loan.objects.create(
            loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Get outstanding amount
        outstanding = loan.outstanding_amount
        
        # Verify outstanding equals total_amount (no payments, no penalties)
        assert outstanding == total_amount, \
            f"Unpaid loan should have outstanding = total_amount ({total_amount}), got {outstanding}"
        
        # Verify it's positive
        assert outstanding > Decimal('0.00'), \
            f"Unpaid loan should have positive outstanding amount, got {outstanding}"
        
        # Verify invariant: outstanding >= 0
        assert outstanding >= Decimal('0.00'), \
            f"INVARIANT VIOLATION: outstanding_amount ({outstanding}) is negative"

