"""
Property-Based Tests for Loan Recalculation Consistency

This module contains property-based tests to verify that loan recalculation
maintains consistency across all operations (edits, penalties, payments).

Feature: comprehensive-reports-and-fixes
"""

import os
import sys
import django
from decimal import Decimal
from hypothesis import given, strategies as st, settings
from hypothesis.strategies import decimals

# Setup Django
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'branch_system.settings')
django.setup()

from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from loans.models import Loan, LoanApplication, LoanProduct, PenaltyCharge, Repayment
from hypothesis.extra.django import TestCase as HypothesisTestCase

User = get_user_model()


class RecalculationPropertyTests(HypothesisTestCase):
    """
    Property-based tests for loan recalculation consistency.
    
    Tests Properties 27 and 28 related to monetary rounding and recalculation consistency.
    """
    
    def setUp(self):
        """Set up test data"""
        # Create or get test user
        self.user, created = User.objects.get_or_create(
            username='testuser_recalc',
            defaults={
                'email': 'test_recalc@example.com',
                'password': 'testpass123',
                'first_name': 'Test',
                'last_name': 'User',
                'phone_number': '+254712345679'
            }
        )
        
        # Create loan product
        self.loan_product, created = LoanProduct.objects.get_or_create(
            name='Test Product Recalc',
            product_type='boost',
            defaults={
                'description': 'Test product for property tests',
                'min_amount': Decimal('1000.00'),
                'max_amount': Decimal('100000.00'),
                'interest_rate': Decimal('10.00'),
                'processing_fee': Decimal('5.00'),
                'min_duration': 7,
                'max_duration': 90,
                'available_repayment_methods': ['monthly']
            }
        )
    
    @given(
        principal=decimals(min_value='1000.00', max_value='100000.00', places=2),
        interest=decimals(min_value='0.00', max_value='50000.00', places=2),
        processing_fee=decimals(min_value='0.00', max_value='5000.00', places=2)
    )
    @settings(max_examples=20, deadline=None)
    def test_property_27_monetary_rounding(self, principal, interest, processing_fee):
        """
        Feature: comprehensive-reports-and-fixes, Property 27: Monetary Rounding
        
        For any monetary value displayed or stored, it should be rounded to exactly 2 decimal places.
        
        Validates: Requirements 9.5
        """
        # Create loan application
        application = LoanApplication.objects.create(
            borrower=self.user,
            loan_product=self.loan_product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test loan',
            interest_amount=interest,
            processing_fee_amount=processing_fee,
            total_amount=principal + interest + processing_fee
        )
        
        # Create loan
        loan = Loan.objects.create(
            application=application,
            borrower=self.user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=principal + interest + processing_fee,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timezone.timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Verify all monetary fields are rounded to 2 decimal places
        # Check that the string representation has exactly 2 decimal places
        principal_str = str(loan.principal_amount)
        interest_str = str(loan.interest_amount)
        processing_fee_str = str(loan.processing_fee)
        total_str = str(loan.total_amount)
        
        # Each should have exactly 2 decimal places
        self.assertEqual(len(principal_str.split('.')[-1]), 2, 
                        f"Principal {principal_str} should have exactly 2 decimal places")
        self.assertEqual(len(interest_str.split('.')[-1]), 2,
                        f"Interest {interest_str} should have exactly 2 decimal places")
        self.assertEqual(len(processing_fee_str.split('.')[-1]), 2,
                        f"Processing fee {processing_fee_str} should have exactly 2 decimal places")
        self.assertEqual(len(total_str.split('.')[-1]), 2,
                        f"Total {total_str} should have exactly 2 decimal places")
        
        # Verify calculated properties are also rounded
        amount_paid = loan.amount_paid
        outstanding = loan.outstanding_amount
        
        amount_paid_str = str(amount_paid)
        outstanding_str = str(outstanding)
        
        self.assertEqual(len(amount_paid_str.split('.')[-1]), 2,
                        f"Amount paid {amount_paid_str} should have exactly 2 decimal places")
        self.assertEqual(len(outstanding_str.split('.')[-1]), 2,
                        f"Outstanding {outstanding_str} should have exactly 2 decimal places")
        
        # Clean up
        loan.delete()
        application.delete()
    
    @given(
        principal=decimals(min_value='1000.00', max_value='100000.00', places=2),
        interest=decimals(min_value='100.00', max_value='50000.00', places=2),
        processing_fee=decimals(min_value='50.00', max_value='5000.00', places=2),
        new_principal=decimals(min_value='1000.00', max_value='100000.00', places=2),
        new_interest=decimals(min_value='100.00', max_value='50000.00', places=2),
        new_processing_fee=decimals(min_value='50.00', max_value='5000.00', places=2)
    )
    @settings(max_examples=20, deadline=None)
    def test_property_28_recalculation_consistency_on_edit(self, principal, interest, processing_fee,
                                                           new_principal, new_interest, new_processing_fee):
        """
        Feature: comprehensive-reports-and-fixes, Property 28: Recalculation Consistency
        
        For any loan, when amounts are edited, penalties are added, or payments are recorded,
        all dependent amounts (total_amount, amount_paid, outstanding_amount) should be
        recalculated to maintain consistency with their formulas.
        
        Validates: Requirements 9.6, 9.7, 9.8
        """
        # Create loan application
        application = LoanApplication.objects.create(
            borrower=self.user,
            loan_product=self.loan_product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test loan',
            interest_amount=interest,
            processing_fee_amount=processing_fee,
            total_amount=principal + interest + processing_fee
        )
        
        # Create loan
        loan = Loan.objects.create(
            application=application,
            borrower=self.user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=principal + interest + processing_fee,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timezone.timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Verify initial calculation
        expected_total = principal + interest + processing_fee
        self.assertEqual(loan.total_amount, expected_total.quantize(Decimal('0.01')),
                        "Initial total_amount should equal principal + interest + processing_fee")
        
        # Edit loan amounts
        loan.principal_amount = new_principal
        loan.interest_amount = new_interest
        loan.processing_fee = new_processing_fee
        loan.save()
        
        # Reload from database to get updated values
        loan.refresh_from_db()
        
        # Verify recalculation after edit
        expected_new_total = new_principal + new_interest + new_processing_fee
        self.assertEqual(loan.total_amount, expected_new_total.quantize(Decimal('0.01')),
                        "After edit, total_amount should be recalculated as principal + interest + processing_fee")
        
        # Verify outstanding_amount is consistent
        expected_outstanding = loan.total_amount + loan.total_penalties - loan.amount_paid
        self.assertEqual(loan.outstanding_amount, expected_outstanding.quantize(Decimal('0.01')),
                        "Outstanding amount should equal total + penalties - amount_paid")
        
        # Clean up
        loan.delete()
        application.delete()
    
    @given(
        principal=decimals(min_value='1000.00', max_value='100000.00', places=2),
        penalty_amount=decimals(min_value='10.00', max_value='5000.00', places=2)
    )
    @settings(max_examples=20, deadline=None)
    def test_property_28_recalculation_consistency_on_penalty(self, principal, penalty_amount):
        """
        Feature: comprehensive-reports-and-fixes, Property 28: Recalculation Consistency (Penalty)
        
        When a penalty is added, outstanding_amount should be recalculated to include the penalty.
        
        Validates: Requirements 9.6, 9.7, 9.8
        """
        # Create loan application
        interest = principal * Decimal('0.10')
        processing_fee = principal * Decimal('0.05')
        
        application = LoanApplication.objects.create(
            borrower=self.user,
            loan_product=self.loan_product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test loan',
            interest_amount=interest,
            processing_fee_amount=processing_fee,
            total_amount=principal + interest + processing_fee
        )
        
        # Create loan
        loan = Loan.objects.create(
            application=application,
            borrower=self.user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=principal + interest + processing_fee,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timezone.timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Record initial outstanding amount
        initial_outstanding = loan.outstanding_amount
        
        # Add penalty
        penalty = PenaltyCharge.objects.create(
            loan=loan,
            amount=penalty_amount,
            penalty_rate=Decimal('5.00'),
            days_overdue=1,
            outstanding_amount=loan.outstanding_amount,
            penalty_date=timezone.now().date()
        )
        
        # Reload loan to get updated values
        loan.refresh_from_db()
        
        # Verify outstanding_amount increased by penalty amount
        expected_outstanding = initial_outstanding + penalty_amount
        actual_outstanding = loan.outstanding_amount
        
        # Allow for small rounding differences (within 0.01)
        difference = abs(actual_outstanding - expected_outstanding)
        self.assertLessEqual(difference, Decimal('0.01'),
                            f"Outstanding amount should increase by penalty amount. "
                            f"Expected: {expected_outstanding}, Actual: {actual_outstanding}, "
                            f"Difference: {difference}")
        
        # Clean up
        penalty.delete()
        loan.delete()
        application.delete()
    
    @given(
        principal=decimals(min_value='1000.00', max_value='100000.00', places=2),
        payment_amount=decimals(min_value='100.00', max_value='50000.00', places=2)
    )
    @settings(max_examples=20, deadline=None)
    def test_property_28_recalculation_consistency_on_payment(self, principal, payment_amount):
        """
        Feature: comprehensive-reports-and-fixes, Property 28: Recalculation Consistency (Payment)
        
        When a payment is recorded, amount_paid and outstanding_amount should be recalculated.
        
        Validates: Requirements 9.6, 9.7, 9.8
        """
        # Create loan application
        interest = principal * Decimal('0.10')
        processing_fee = principal * Decimal('0.05')
        
        application = LoanApplication.objects.create(
            borrower=self.user,
            loan_product=self.loan_product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test loan',
            interest_amount=interest,
            processing_fee_amount=processing_fee,
            total_amount=principal + interest + processing_fee
        )
        
        # Create loan
        loan = Loan.objects.create(
            application=application,
            borrower=self.user,
            principal_amount=principal,
            interest_amount=interest,
            processing_fee=processing_fee,
            total_amount=principal + interest + processing_fee,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timezone.timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Record initial values
        initial_amount_paid = loan.amount_paid
        initial_outstanding = loan.outstanding_amount
        
        # Ensure payment amount doesn't exceed total
        safe_payment_amount = min(payment_amount, loan.total_amount)
        
        # Record payment
        repayment = Repayment.objects.create(
            loan=loan,
            amount=safe_payment_amount,
            payment_method='mpesa',
            payment_date=timezone.now()
        )
        
        # Reload loan to get updated values
        loan.refresh_from_db()
        
        # Verify amount_paid increased by payment amount
        expected_amount_paid = initial_amount_paid + safe_payment_amount
        actual_amount_paid = loan.amount_paid
        
        # Allow for small rounding differences (within 0.01)
        difference = abs(actual_amount_paid - expected_amount_paid)
        self.assertLessEqual(difference, Decimal('0.01'),
                            f"Amount paid should increase by payment amount. "
                            f"Expected: {expected_amount_paid}, Actual: {actual_amount_paid}, "
                            f"Difference: {difference}")
        
        # Verify outstanding_amount decreased by payment amount
        expected_outstanding = initial_outstanding - safe_payment_amount
        actual_outstanding = loan.outstanding_amount
        
        difference = abs(actual_outstanding - expected_outstanding)
        self.assertLessEqual(difference, Decimal('0.01'),
                            f"Outstanding amount should decrease by payment amount. "
                            f"Expected: {expected_outstanding}, Actual: {actual_outstanding}, "
                            f"Difference: {difference}")
        
        # Clean up
        repayment.delete()
        loan.delete()
        application.delete()


if __name__ == '__main__':
    import unittest
    
    print("="*70)
    print("PROPERTY-BASED TESTS: Loan Recalculation Consistency")
    print("="*70)
    print()
    
    # Create test suite
    suite = unittest.TestLoader().loadTestsFromTestCase(RecalculationPropertyTests)
    
    # Run tests
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite)
    
    # Print summary
    print()
    print("="*70)
    print("TEST SUMMARY")
    print("="*70)
    print(f"Tests run: {result.testsRun}")
    print(f"Failures: {len(result.failures)}")
    print(f"Errors: {len(result.errors)}")
    print(f"Success: {result.wasSuccessful()}")
    print("="*70)
    
    sys.exit(0 if result.wasSuccessful() else 1)

