"""
Property-based tests for reports system enhancement.

These tests use Hypothesis to verify universal properties that should hold
across all valid inputs, ensuring correctness of filtering and calculation logic.
"""

from hypothesis import given, strategies as st, settings
from django.test import TestCase, RequestFactory
from django.utils import timezone
from datetime import date, datetime, timedelta
from decimal import Decimal
import uuid

from loans.models import Loan, LoanProduct, LoanApplication
from users.models import CustomUser
from reports.filter_service import ReportFilterService, FilterParams


# Custom strategies for generating test data
@st.composite
def date_range_strategy(draw):
    """Generate a valid date range (start_date <= end_date)"""
    start_date = draw(st.dates(
        min_value=date(2020, 1, 1),
        max_value=date(2025, 12, 31)
    ))
    # End date must be >= start date
    end_date = draw(st.dates(
        min_value=start_date,
        max_value=date(2025, 12, 31)
    ))
    return start_date, end_date


@st.composite
def loan_with_dates_strategy(draw, start_date, end_date, date_field='disbursement_date'):
    """Generate a loan with a date within the specified range"""
    # Generate a date within the range
    days_diff = (end_date - start_date).days
    if days_diff == 0:
        loan_date = start_date
    else:
        offset = draw(st.integers(min_value=0, max_value=days_diff))
        loan_date = start_date + timedelta(days=offset)
    
    return loan_date


class TestDateRangeFiltering(TestCase):
    """
    Test Property 1: Date range filtering completeness
    
    Feature: reports-system-enhancement, Property 1: Date range filtering completeness
    Validates: Requirements 1.2, 6.2, 7.2, 9.4, 11.2
    
    For any report with date range filters, when a user specifies start and end dates,
    all returned records should have their relevant date field falling within the
    specified range (inclusive), and no records outside the range should be returned.
    """
    
    def setUp(self):
        """Set up test data"""
        self.factory = RequestFactory()
        
        # Create test user
        self.user = CustomUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            phone_number='+254700000000',
            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('50000'),
            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(
        date_range=date_range_strategy(),
        num_loans_in_range=st.integers(min_value=1, max_value=10),
        num_loans_outside_range=st.integers(min_value=1, max_value=10)
    )
    def test_date_range_filter_includes_only_records_in_range(
        self, date_range, num_loans_in_range, num_loans_outside_range
    ):
        """
        Property: All returned records should have dates within the specified range,
        and no records outside the range should be returned.
        """
        start_date, end_date = date_range
        
        # Create loans within the date range
        loans_in_range = []
        for i in range(num_loans_in_range):
            # Generate a date within the range
            days_diff = (end_date - start_date).days
            if days_diff == 0:
                loan_date = start_date
            else:
                offset = i % (days_diff + 1)
                loan_date = start_date + timedelta(days=offset)
            
            # Create loan application
            app = LoanApplication.objects.create(
                application_number=f'APP-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            # Create loan with date in range
            loan = Loan.objects.create(
                loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=datetime.combine(loan_date, datetime.min.time()),
                due_date=datetime.combine(loan_date + timedelta(days=30), datetime.min.time()),
                duration_days=30,
                status='active'
            )
            loans_in_range.append(loan)
        
        # Create loans outside the date range
        loans_outside_range = []
        for i in range(num_loans_outside_range):
            # Generate a date before start_date
            days_before = i + 1
            loan_date = start_date - timedelta(days=days_before)
            
            # Create loan application
            app = LoanApplication.objects.create(
                application_number=f'APP-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            # Create loan with date outside range
            loan = Loan.objects.create(
                loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=datetime.combine(loan_date, datetime.min.time()),
                due_date=datetime.combine(loan_date + timedelta(days=30), datetime.min.time()),
                duration_days=30,
                status='active'
            )
            loans_outside_range.append(loan)
        
        # Apply date range filter
        queryset = Loan.objects.all()
        filtered_queryset = ReportFilterService.apply_date_range_filter(
            queryset, start_date, end_date, 'disbursement_date'
        )
        
        # Verify all returned loans are within the date range
        for loan in filtered_queryset:
            loan_date = loan.disbursement_date.date()
            assert start_date <= loan_date <= end_date, \
                f"Loan {loan.loan_number} has date {loan_date} outside range [{start_date}, {end_date}]"
        
        # Verify no loans outside the range are included
        filtered_ids = set(filtered_queryset.values_list('id', flat=True))
        for loan in loans_outside_range:
            assert loan.id not in filtered_ids, \
                f"Loan {loan.loan_number} outside date range was incorrectly included"
        
        # Verify all loans in range are included
        for loan in loans_in_range:
            assert loan.id in filtered_ids, \
                f"Loan {loan.loan_number} within date range was incorrectly excluded"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()


class TestProductFiltering(TestCase):
    """
    Test Property 2: Product filtering accuracy
    
    Feature: reports-system-enhancement, Property 2: Product filtering accuracy
    Validates: Requirements 6.3, 11.3
    
    For any report with loan product filters, when a user selects a specific product,
    all returned loans should belong to that product type, and no loans of other
    product types should be returned.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            phone_number='+254700000000',
            first_name='Test',
            last_name='User'
        )
        
        # Create multiple loan products
        self.product1 = LoanProduct.objects.create(
            name='Boost',
            product_type='boost',
            description='Boost product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        self.product2 = LoanProduct.objects.create(
            name='Boost Plus',
            product_type='boost_plus',
            description='Boost Plus product',
            min_amount=Decimal('5000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('12.0'),
            processing_fee=Decimal('6.0'),
            min_duration=30,
            max_duration=180,
            available_repayment_methods=['monthly']
        )
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_loans_product1=st.integers(min_value=1, max_value=10),
        num_loans_product2=st.integers(min_value=1, max_value=10)
    )
    def test_product_filter_includes_only_selected_product(
        self, num_loans_product1, num_loans_product2
    ):
        """
        Property: All returned loans should belong to the selected product type,
        and no loans of other product types should be returned.
        """
        # Create loans for product1
        loans_product1 = []
        for i in range(num_loans_product1):
            app = LoanApplication.objects.create(
                application_number=f'APP-P1-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product1,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-P1-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=timezone.now(),
                due_date=timezone.now() + timedelta(days=30),
                duration_days=30,
                status='active'
            )
            loans_product1.append(loan)
        
        # Create loans for product2
        loans_product2 = []
        for i in range(num_loans_product2):
            app = LoanApplication.objects.create(
                application_number=f'APP-P2-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product2,
                requested_amount=Decimal('20000'),
                requested_duration=60,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-P2-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('20000'),
                interest_amount=Decimal('2400'),
                processing_fee=Decimal('1200'),
                total_amount=Decimal('23600'),
                disbursement_date=timezone.now(),
                due_date=timezone.now() + timedelta(days=60),
                duration_days=60,
                status='active'
            )
            loans_product2.append(loan)
        
        # Apply product filter for product1
        queryset = Loan.objects.all()
        filtered_queryset = ReportFilterService.apply_loan_product_filter(
            queryset, str(self.product1.id)
        )
        
        # Verify all returned loans belong to product1
        for loan in filtered_queryset:
            assert loan.application.loan_product_id == self.product1.id, \
                f"Loan {loan.loan_number} has wrong product {loan.application.loan_product_id}"
        
        # Verify no loans from product2 are included
        filtered_ids = set(filtered_queryset.values_list('id', flat=True))
        for loan in loans_product2:
            assert loan.id not in filtered_ids, \
                f"Loan {loan.loan_number} from product2 was incorrectly included"
        
        # Verify all loans from product1 are included
        for loan in loans_product1:
            assert loan.id in filtered_ids, \
                f"Loan {loan.loan_number} from product1 was incorrectly excluded"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()


class TestMultipleFilterComposition(TestCase):
    """
    Test Property 8: Multiple filter composition
    
    Feature: reports-system-enhancement, Property 8: Multiple filter composition
    Validates: Requirements 1.3
    
    For any report with multiple active filters, all returned records should
    satisfy every filter criterion simultaneously (AND logic).
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            phone_number='+254700000000',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        date_range=date_range_strategy(),
        num_matching_loans=st.integers(min_value=1, max_value=5),
        num_non_matching_loans=st.integers(min_value=1, max_value=5)
    )
    def test_multiple_filters_apply_and_logic(
        self, date_range, num_matching_loans, num_non_matching_loans
    ):
        """
        Property: When multiple filters are applied, all returned records should
        satisfy ALL filter criteria (AND logic).
        """
        start_date, end_date = date_range
        
        # Create loans that match ALL filters (date range + not deleted + not rolled over)
        matching_loans = []
        for i in range(num_matching_loans):
            days_diff = (end_date - start_date).days
            if days_diff == 0:
                loan_date = start_date
            else:
                offset = i % (days_diff + 1)
                loan_date = start_date + timedelta(days=offset)
            
            app = LoanApplication.objects.create(
                application_number=f'APP-MATCH-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-MATCH-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=datetime.combine(loan_date, datetime.min.time()),
                due_date=datetime.combine(loan_date + timedelta(days=30), datetime.min.time()),
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            matching_loans.append(loan)
        
        # Create loans that DON'T match all filters
        non_matching_loans = []
        for i in range(num_non_matching_loans):
            # Some are deleted, some are rolled over, some are outside date range
            is_deleted = i % 3 == 0
            is_rolled_over = i % 3 == 1
            outside_range = i % 3 == 2
            
            if outside_range:
                loan_date = start_date - timedelta(days=i + 1)
            else:
                days_diff = (end_date - start_date).days
                if days_diff == 0:
                    loan_date = start_date
                else:
                    offset = i % (days_diff + 1)
                    loan_date = start_date + timedelta(days=offset)
            
            app = LoanApplication.objects.create(
                application_number=f'APP-NOMATCH-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-NOMATCH-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=datetime.combine(loan_date, datetime.min.time()),
                due_date=datetime.combine(loan_date + timedelta(days=30), datetime.min.time()),
                duration_days=30,
                status='rolled_over' if is_rolled_over else 'active',
                is_deleted=is_deleted,
                is_rolled_over=is_rolled_over
            )
            non_matching_loans.append(loan)
        
        # Apply multiple filters
        queryset = Loan.objects.all()
        queryset = ReportFilterService.apply_date_range_filter(
            queryset, start_date, end_date, 'disbursement_date'
        )
        queryset = ReportFilterService.apply_loan_status_filter(
            queryset, exclude_rolled_over=True, exclude_deleted=True
        )
        
        # Verify all returned loans match ALL criteria
        for loan in queryset:
            loan_date = loan.disbursement_date.date()
            assert start_date <= loan_date <= end_date, \
                f"Loan {loan.loan_number} has date {loan_date} outside range"
            assert not loan.is_deleted, \
                f"Loan {loan.loan_number} is deleted but was included"
            assert not loan.is_rolled_over and loan.status != 'rolled_over', \
                f"Loan {loan.loan_number} is rolled over but was included"
        
        # Verify non-matching loans are excluded
        filtered_ids = set(queryset.values_list('id', flat=True))
        for loan in non_matching_loans:
            assert loan.id not in filtered_ids, \
                f"Non-matching loan {loan.loan_number} was incorrectly included"
        
        # Verify matching loans are included
        for loan in matching_loans:
            assert loan.id in filtered_ids, \
                f"Matching loan {loan.loan_number} was incorrectly excluded"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()


class TestFilterClearing(TestCase):
    """
    Test Property 9: Filter clearing restoration
    
    Feature: reports-system-enhancement, Property 9: Filter clearing restoration
    Validates: Requirements 1.4
    
    For any report, applying filters then clearing all filters should return
    the same record count as the initial unfiltered state.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            phone_number='+254700000000',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        num_loans=st.integers(min_value=5, max_value=20),
        date_range=date_range_strategy()
    )
    def test_clearing_filters_restores_original_count(self, num_loans, date_range):
        """
        Property: Applying filters then clearing them should return the same
        record count as the initial unfiltered state.
        """
        start_date, end_date = date_range
        
        # Create loans with various dates and statuses
        created_loans = []
        for i in range(num_loans):
            # Mix of dates (some in range, some out of range)
            if i % 2 == 0:
                days_diff = (end_date - start_date).days
                if days_diff == 0:
                    loan_date = start_date
                else:
                    offset = i % (days_diff + 1)
                    loan_date = start_date + timedelta(days=offset)
            else:
                loan_date = start_date - timedelta(days=i + 1)
            
            app = LoanApplication.objects.create(
                application_number=f'APP-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=datetime.combine(loan_date, datetime.min.time()),
                due_date=datetime.combine(loan_date + timedelta(days=30), datetime.min.time()),
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            created_loans.append(loan)
        
        # Get initial count (unfiltered)
        initial_queryset = Loan.objects.all()
        initial_count = initial_queryset.count()
        
        # Apply filters
        filtered_queryset = ReportFilterService.apply_date_range_filter(
            initial_queryset, start_date, end_date, 'disbursement_date'
        )
        filtered_count = filtered_queryset.count()
        
        # Verify filtering changed the count (unless all loans happen to be in range)
        # This is expected behavior
        
        # Clear filters by getting fresh queryset
        cleared_queryset = Loan.objects.all()
        cleared_count = cleared_queryset.count()
        
        # Verify cleared count equals initial count
        assert cleared_count == initial_count, \
            f"Cleared count {cleared_count} != initial count {initial_count}"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()



# Import calculation service
from reports.calculation_service import LoanCalculationService


class TestAmountPaidCalculation(TestCase):
    """
    Test Property 3: Amount paid calculation correctness
    
    Feature: reports-system-enhancement, Property 3: Amount paid calculation correctness
    Validates: Requirements 3.1, 3.3, 13.3
    
    For any loan with associated repayments, the displayed amount_paid should equal
    the sum of all repayment amounts for that loan, and for loans with no repayments,
    amount_paid should equal zero.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser_calc',
            email='test_calc@example.com',
            phone_number='+254700000001',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product Calc',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        num_repayments=st.integers(min_value=0, max_value=10),
        repayment_amounts=st.lists(
            st.decimals(min_value=Decimal('100'), max_value=Decimal('5000'), places=2),
            min_size=0,
            max_size=10
        )
    )
    def test_amount_paid_equals_sum_of_repayments(self, num_repayments, repayment_amounts):
        """
        Property: For any loan, amount_paid should equal the sum of all repayment amounts.
        For loans with no repayments, amount_paid should equal zero.
        """
        # Ensure we have the right number of repayment amounts
        if len(repayment_amounts) < num_repayments:
            repayment_amounts.extend([Decimal('1000')] * (num_repayments - len(repayment_amounts)))
        repayment_amounts = repayment_amounts[:num_repayments]
        
        # Create loan
        app = LoanApplication.objects.create(
            application_number=f'APP-CALC-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=Decimal('10000'),
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        loan = Loan.objects.create(
            loan_number=f'LOAN-CALC-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=self.user,
            principal_amount=Decimal('10000'),
            interest_amount=Decimal('1000'),
            processing_fee=Decimal('500'),
            total_amount=Decimal('11500'),
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            duration_days=30,
            status='active'
        )
        
        # Create repayments
        from loans.models import Repayment
        expected_total = Decimal('0.00')
        
        for i, amount in enumerate(repayment_amounts):
            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_total += amount
        
        # Calculate amount paid using service
        calculated_amount_paid = LoanCalculationService.calculate_amount_paid(loan)
        
        # Verify amount paid equals sum of repayments
        assert calculated_amount_paid == expected_total, \
            f"Amount paid {calculated_amount_paid} != expected {expected_total}"
        
        # Special case: if no repayments, amount paid should be zero
        if num_repayments == 0:
            assert calculated_amount_paid == Decimal('0.00'), \
                f"Loan with no repayments should have amount_paid = 0, got {calculated_amount_paid}"
        
        # Cleanup
        loan.delete()
        app.delete()


class TestOutstandingAmountCalculation(TestCase):
    """
    Test Property 6: Outstanding amount calculation correctness
    
    Feature: reports-system-enhancement, Property 6: Outstanding amount calculation correctness
    Validates: Requirements 3.4, 13.2
    
    For any loan, the outstanding amount should equal
    (total_amount + total_penalties - amount_paid), where total_amount includes
    principal, interest, and processing fees.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser_outstanding',
            email='test_outstanding@example.com',
            phone_number='+254700000002',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product Outstanding',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        principal=st.decimals(min_value=Decimal('1000'), max_value=Decimal('50000'), places=2),
        interest=st.decimals(min_value=Decimal('100'), max_value=Decimal('5000'), places=2),
        processing_fee=st.decimals(min_value=Decimal('50'), max_value=Decimal('2500'), places=2),
        num_repayments=st.integers(min_value=0, max_value=5),
        num_penalties=st.integers(min_value=0, max_value=3)
    )
    def test_outstanding_equals_total_plus_penalties_minus_paid(
        self, principal, interest, processing_fee, num_repayments, num_penalties
    ):
        """
        Property: Outstanding amount = total_amount + total_penalties - amount_paid
        """
        # Calculate total amount
        total_amount = principal + interest + processing_fee
        
        # Create loan
        app = LoanApplication.objects.create(
            application_number=f'APP-OUT-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=principal,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        loan = Loan.objects.create(
            loan_number=f'LOAN-OUT-{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() - timedelta(days=35),
            due_date=timezone.now() - timedelta(days=5),  # Overdue
            duration_days=30,
            status='active'
        )
        
        # Create repayments
        from loans.models import Repayment
        total_paid = Decimal('0.00')
        
        for i in range(num_repayments):
            amount = Decimal('500.00')
            Repayment.objects.create(
                loan=loan,
                amount=amount,
                payment_method='mpesa',
                receipt_number=f'RCP-OUT-{uuid.uuid4().hex[:8]}',
                payment_date=timezone.now() - timedelta(days=i)
            )
            total_paid += amount
        
        # Create penalties
        from loans.models import PenaltyCharge
        total_penalties = Decimal('0.00')
        
        for i in range(num_penalties):
            penalty_amount = Decimal('100.00')
            PenaltyCharge.objects.create(
                loan=loan,
                amount=penalty_amount,
                penalty_rate=Decimal('5.0'),
                days_overdue=i + 1,
                outstanding_amount=total_amount - total_paid,
                applied_date=timezone.now() - timedelta(days=i)
            )
            total_penalties += penalty_amount
        
        # Calculate expected outstanding
        expected_outstanding = total_amount + total_penalties - total_paid
        expected_outstanding = max(expected_outstanding, Decimal('0.00'))  # Never negative
        
        # Calculate outstanding using service
        calculated_outstanding = LoanCalculationService.calculate_outstanding_amount(loan)
        
        # Verify outstanding amount is correct
        assert calculated_outstanding == expected_outstanding, \
            f"Outstanding {calculated_outstanding} != expected {expected_outstanding}"
        
        # Verify outstanding is never negative
        assert calculated_outstanding >= Decimal('0.00'), \
            f"Outstanding amount should never be negative, got {calculated_outstanding}"
        
        # Cleanup
        loan.delete()
        app.delete()


class TestDailyPaymentCalculation(TestCase):
    """
    Test Property 7: Daily payment calculation correctness
    
    Feature: reports-system-enhancement, Property 7: Daily payment calculation correctness
    Validates: Requirements 3.5
    
    For any active loan with remaining days until due date greater than zero,
    the daily payment required should equal outstanding_amount divided by remaining_days.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser_daily',
            email='test_daily@example.com',
            phone_number='+254700000003',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product Daily',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        total_amount=st.decimals(min_value=Decimal('1000'), max_value=Decimal('50000'), places=2),
        days_until_due=st.integers(min_value=1, max_value=90),
        amount_paid=st.decimals(min_value=Decimal('0'), max_value=Decimal('5000'), places=2)
    )
    def test_daily_payment_equals_outstanding_divided_by_remaining_days(
        self, total_amount, days_until_due, amount_paid
    ):
        """
        Property: Daily payment = outstanding_amount / remaining_days (when remaining_days > 0)
        """
        # Create loan with future due date
        app = LoanApplication.objects.create(
            application_number=f'APP-DAILY-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=total_amount,
            requested_duration=days_until_due + 5,
            purpose='Test',
            status='approved'
        )
        
        loan = Loan.objects.create(
            loan_number=f'LOAN-DAILY-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=self.user,
            principal_amount=total_amount,
            interest_amount=Decimal('0'),
            processing_fee=Decimal('0'),
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=days_until_due),
            duration_days=days_until_due + 5,
            status='active'
        )
        
        # Create a repayment if amount_paid > 0
        if amount_paid > Decimal('0'):
            from loans.models import Repayment
            Repayment.objects.create(
                loan=loan,
                amount=amount_paid,
                payment_method='mpesa',
                receipt_number=f'RCP-DAILY-{uuid.uuid4().hex[:8]}',
                payment_date=timezone.now()
            )
        
        # Calculate outstanding
        outstanding = total_amount - amount_paid
        outstanding = max(outstanding, Decimal('0.00'))
        
        # Calculate expected daily payment
        if outstanding > Decimal('0') and days_until_due > 0:
            expected_daily_payment = outstanding / Decimal(str(days_until_due))
        else:
            expected_daily_payment = Decimal('0.00')
        
        # Calculate daily payment using service
        calculated_daily_payment = LoanCalculationService.calculate_daily_payment_required(loan)
        
        # Verify daily payment is correct (allow small rounding differences)
        difference = abs(calculated_daily_payment - expected_daily_payment)
        assert difference < Decimal('0.01'), \
            f"Daily payment {calculated_daily_payment} != expected {expected_daily_payment}"
        
        # Cleanup
        loan.delete()
        app.delete()


class TestTotalLoanAmountCalculation(TestCase):
    """
    Test Property 31: Total loan amount calculation
    
    Feature: reports-system-enhancement, Property 31: Total loan amount calculation
    Validates: Requirements 13.1
    
    For any loan, the total_amount should equal principal_amount + interest_amount + processing_fee.
    """
    
    @settings(max_examples=50, deadline=None)
    @given(
        principal=st.decimals(min_value=Decimal('1000'), max_value=Decimal('100000'), places=2),
        interest=st.decimals(min_value=Decimal('0'), max_value=Decimal('10000'), places=2),
        processing_fee=st.decimals(min_value=Decimal('0'), max_value=Decimal('5000'), places=2)
    )
    def test_total_amount_equals_sum_of_components(self, principal, interest, processing_fee):
        """
        Property: Total amount = principal + interest + processing_fee
        """
        # Calculate expected total
        expected_total = principal + interest + processing_fee
        
        # Calculate using service
        calculated_total = LoanCalculationService.calculate_total_loan_amount(
            principal, interest, processing_fee
        )
        
        # Verify total is correct
        assert calculated_total == expected_total, \
            f"Total {calculated_total} != expected {expected_total}"


class TestPenaltyInclusion(TestCase):
    """
    Test Property 32: Penalty inclusion in outstanding
    
    Feature: reports-system-enhancement, Property 32: Penalty inclusion in outstanding
    Validates: Requirements 13.4
    
    For any loan with penalty charges, the outstanding_amount should include
    the sum of all penalty amounts.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser_penalty',
            email='test_penalty@example.com',
            phone_number='+254700000004',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product Penalty',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        total_amount=st.decimals(min_value=Decimal('1000'), max_value=Decimal('50000'), places=2),
        num_penalties=st.integers(min_value=1, max_value=5),
        penalty_amounts=st.lists(
            st.decimals(min_value=Decimal('50'), max_value=Decimal('500'), places=2),
            min_size=1,
            max_size=5
        )
    )
    def test_outstanding_includes_all_penalties(self, total_amount, num_penalties, penalty_amounts):
        """
        Property: Outstanding amount must include the sum of all penalty charges.
        """
        # Ensure we have the right number of penalty amounts
        if len(penalty_amounts) < num_penalties:
            penalty_amounts.extend([Decimal('100')] * (num_penalties - len(penalty_amounts)))
        penalty_amounts = penalty_amounts[:num_penalties]
        
        # Create loan
        app = LoanApplication.objects.create(
            application_number=f'APP-PEN-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=total_amount,
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        loan = Loan.objects.create(
            loan_number=f'LOAN-PEN-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=self.user,
            principal_amount=total_amount,
            interest_amount=Decimal('0'),
            processing_fee=Decimal('0'),
            total_amount=total_amount,
            disbursement_date=timezone.now() - timedelta(days=35),
            due_date=timezone.now() - timedelta(days=5),
            duration_days=30,
            status='active'
        )
        
        # Create penalties
        from loans.models import PenaltyCharge
        total_penalties = Decimal('0.00')
        
        for i, penalty_amount in enumerate(penalty_amounts):
            PenaltyCharge.objects.create(
                loan=loan,
                amount=penalty_amount,
                penalty_rate=Decimal('5.0'),
                days_overdue=i + 1,
                outstanding_amount=total_amount,
                applied_date=timezone.now() - timedelta(days=i)
            )
            total_penalties += penalty_amount
        
        # Calculate expected outstanding (no repayments, so just total + penalties)
        expected_outstanding = total_amount + total_penalties
        
        # Calculate outstanding using service
        calculated_outstanding = LoanCalculationService.calculate_outstanding_amount(loan)
        
        # Verify outstanding includes all penalties
        assert calculated_outstanding == expected_outstanding, \
            f"Outstanding {calculated_outstanding} != expected {expected_outstanding}"
        
        # Verify penalties are included
        assert calculated_outstanding >= total_amount, \
            f"Outstanding {calculated_outstanding} should be >= total_amount {total_amount}"
        
        # Cleanup
        loan.delete()
        app.delete()


class TestCurrencyFormatting(TestCase):
    """
    Test Property 33: Currency formatting precision
    
    Feature: reports-system-enhancement, Property 33: Currency formatting precision
    Validates: Requirements 13.5
    
    For any displayed currency value, the formatted string should contain
    exactly two digits after the decimal point.
    """
    
    @settings(max_examples=50, deadline=None)
    @given(
        amount=st.decimals(
            min_value=Decimal('0'),
            max_value=Decimal('1000000'),
            allow_nan=False,
            allow_infinity=False
        )
    )
    def test_currency_format_has_two_decimal_places(self, amount):
        """
        Property: All currency values must be formatted with exactly 2 decimal places.
        """
        # Format using service
        formatted = LoanCalculationService.format_currency(amount)
        
        # Verify format is a string
        assert isinstance(formatted, str), f"Formatted value should be string, got {type(formatted)}"
        
        # Verify it has exactly 2 decimal places
        if '.' in formatted:
            decimal_part = formatted.split('.')[1]
            assert len(decimal_part) == 2, \
                f"Formatted value {formatted} should have exactly 2 decimal places, got {len(decimal_part)}"
        else:
            # Should not happen with our format, but check anyway
            assert False, f"Formatted value {formatted} should contain decimal point"
        
        # Verify the formatted value can be parsed back to a number
        try:
            parsed = Decimal(formatted)
            # Allow small rounding differences
            difference = abs(parsed - amount)
            assert difference < Decimal('0.01'), \
                f"Parsed value {parsed} differs too much from original {amount}"
        except Exception as e:
            assert False, f"Formatted value {formatted} could not be parsed: {e}"



class TestRolloverStateTransition(TestCase):
    """
    Test Property 13: Rollover state transition
    
    Feature: reports-system-enhancement, Property 13: Rollover state transition
    Validates: Requirements 4.1
    
    For any loan that undergoes rollover, the original loan should have status
    set to 'rolled_over' AND is_rolled_over flag set to true.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser_rollover',
            email='test_rollover@example.com',
            phone_number='+254700000010',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product Rollover',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly'],
            rollover_fee_percentage=Decimal('5.0'),
            max_rollover_count=3,
            max_rollover_days=30
        )
    
    @settings(max_examples=50, deadline=None)
    @given(
        principal_amount=st.decimals(min_value=Decimal('1000'), max_value=Decimal('50000'), places=2),
        duration_days=st.integers(min_value=7, max_value=90)
    )
    def test_rollover_sets_both_status_and_flag(self, principal_amount, duration_days):
        """
        Property: For any loan that undergoes rollover, the original loan should have
        status set to 'rolled_over' AND is_rolled_over flag set to true.
        """
        # Create original loan
        app = LoanApplication.objects.create(
            application_number=f'APP-ROLL-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=principal_amount,
            requested_duration=duration_days,
            purpose='Test',
            status='approved'
        )
        
        interest_amount = principal_amount * Decimal('0.10')
        processing_fee = principal_amount * Decimal('0.05')
        total_amount = principal_amount + interest_amount + processing_fee
        
        original_loan = Loan.objects.create(
            loan_number=f'LOAN-ROLL-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=self.user,
            principal_amount=principal_amount,
            interest_amount=interest_amount,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=duration_days),
            duration_days=duration_days,
            status='active',
            is_rolled_over=False
        )
        
        # Simulate rollover by setting both status and flag
        # This is what the rollover process should do
        original_loan.status = 'rolled_over'
        original_loan.is_rolled_over = True
        original_loan.save()
        
        # Refresh from database
        original_loan.refresh_from_db()
        
        # Verify both status and flag are set
        assert original_loan.status == 'rolled_over', \
            f"Rolled over loan should have status='rolled_over', got '{original_loan.status}'"
        assert original_loan.is_rolled_over is True, \
            f"Rolled over loan should have is_rolled_over=True, got {original_loan.is_rolled_over}"
        
        # Cleanup
        original_loan.delete()
        app.delete()
    
    @settings(max_examples=50, deadline=None)
    @given(
        principal_amount=st.decimals(min_value=Decimal('1000'), max_value=Decimal('50000'), places=2),
        duration_days=st.integers(min_value=7, max_value=90)
    )
    def test_rollover_prevents_active_status_with_flag(self, principal_amount, duration_days):
        """
        Property: A loan with is_rolled_over=True cannot have status='active'.
        This ensures consistency in rollover state.
        """
        # Create loan
        app = LoanApplication.objects.create(
            application_number=f'APP-ROLL2-{uuid.uuid4().hex[:6]}',
            borrower=self.user,
            loan_product=self.product,
            requested_amount=principal_amount,
            requested_duration=duration_days,
            purpose='Test',
            status='approved'
        )
        
        interest_amount = principal_amount * Decimal('0.10')
        processing_fee = principal_amount * Decimal('0.05')
        total_amount = principal_amount + interest_amount + processing_fee
        
        loan = Loan.objects.create(
            loan_number=f'LOAN-ROLL2-{uuid.uuid4().hex[:6]}',
            application=app,
            borrower=self.user,
            principal_amount=principal_amount,
            interest_amount=interest_amount,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=duration_days),
            duration_days=duration_days,
            status='rolled_over',
            is_rolled_over=True
        )
        
        # Try to set status back to active (should fail validation)
        loan.status = 'active'
        
        # This should raise ValueError due to validation
        with self.assertRaises(ValueError) as context:
            loan.save()
        
        assert 'rolled over loan cannot be set back to active' in str(context.exception).lower(), \
            f"Expected validation error for setting rolled over loan to active"
        
        # Cleanup
        loan.refresh_from_db()  # Reset to saved state
        loan.delete()
        app.delete()



# Import client report service
from reports.client_report_service import ClientReportService


class TestClientMetricExclusion(TestCase):
    """
    Test Property 10: Client metric exclusion of soft-deleted
    
    Feature: reports-system-enhancement, Property 10: Client metric exclusion of soft-deleted
    Validates: Requirements 2.2
    
    For any client metric calculation (total count, average score, repayment rate),
    clients with is_deleted flag set to true should not contribute to the calculation.
    """
    
    def setUp(self):
        """Set up test data"""
        self.product = LoanProduct.objects.create(
            name='Test Product Client',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        num_active_clients=st.integers(min_value=1, max_value=10),
        num_deleted_clients=st.integers(min_value=1, max_value=10)
    )
    def test_deleted_clients_excluded_from_metrics(self, num_active_clients, num_deleted_clients):
        """
        Property: Clients with is_active=False or status='inactive' should not
        contribute to client metric calculations.
        """
        # Create active clients
        active_clients = []
        for i in range(num_active_clients):
            client = CustomUser.objects.create_user(
                username=f'active_client_{i}_{uuid.uuid4().hex[:6]}',
                email=f'active{i}_{uuid.uuid4().hex[:6]}@example.com',
                phone_number=f'+25470{1000000 + i}',
                first_name=f'Active{i}',
                last_name='Client',
                role='borrower',
                is_active=True,
                status='active'
            )
            active_clients.append(client)
        
        # Create deleted/inactive clients
        deleted_clients = []
        for i in range(num_deleted_clients):
            client = CustomUser.objects.create_user(
                username=f'deleted_client_{i}_{uuid.uuid4().hex[:6]}',
                email=f'deleted{i}_{uuid.uuid4().hex[:6]}@example.com',
                phone_number=f'+25470{2000000 + i}',
                first_name=f'Deleted{i}',
                last_name='Client',
                role='borrower',
                is_active=False,  # Soft-deleted
                status='inactive'
            )
            deleted_clients.append(client)
        
        # Get client metrics
        metrics = ClientReportService.get_client_metrics()
        
        # Verify total_clients equals only active clients
        assert metrics['total_clients'] == num_active_clients, \
            f"Total clients {metrics['total_clients']} should equal active clients {num_active_clients}"
        
        # Verify deleted clients are not counted
        assert metrics['total_clients'] != (num_active_clients + num_deleted_clients), \
            f"Deleted clients should not be included in total count"
        
        # Cleanup
        for client in active_clients + deleted_clients:
            client.delete()
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_active_clients=st.integers(min_value=2, max_value=5),
        num_deleted_clients=st.integers(min_value=1, max_value=5)
    )
    def test_deleted_clients_excluded_from_average_calculations(
        self, num_active_clients, num_deleted_clients
    ):
        """
        Property: Average score and repayment rate calculations should exclude
        soft-deleted clients.
        """
        # Create active clients with loans
        active_clients = []
        for i in range(num_active_clients):
            client = CustomUser.objects.create_user(
                username=f'active_avg_{i}_{uuid.uuid4().hex[:6]}',
                email=f'activeavg{i}_{uuid.uuid4().hex[:6]}@example.com',
                phone_number=f'+25470{3000000 + i}',
                first_name=f'Active{i}',
                last_name='Client',
                role='borrower',
                is_active=True,
                status='active'
            )
            
            # Create a loan for this client
            app = LoanApplication.objects.create(
                application_number=f'APP-AVG-{uuid.uuid4().hex[:6]}',
                borrower=client,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-AVG-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=client,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                amount_paid=Decimal('5000'),  # 50% repayment rate
                disbursement_date=timezone.now(),
                due_date=timezone.now() + timedelta(days=30),
                duration_days=30,
                status='active'
            )
            
            active_clients.append(client)
        
        # Create deleted clients with loans (should be excluded)
        deleted_clients = []
        for i in range(num_deleted_clients):
            client = CustomUser.objects.create_user(
                username=f'deleted_avg_{i}_{uuid.uuid4().hex[:6]}',
                email=f'deletedavg{i}_{uuid.uuid4().hex[:6]}@example.com',
                phone_number=f'+25470{4000000 + i}',
                first_name=f'Deleted{i}',
                last_name='Client',
                role='borrower',
                is_active=False,
                status='inactive'
            )
            
            # Create a loan for this deleted client
            app = LoanApplication.objects.create(
                application_number=f'APP-DEL-{uuid.uuid4().hex[:6]}',
                borrower=client,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-DEL-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=client,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                amount_paid=Decimal('0'),  # 0% repayment rate
                disbursement_date=timezone.now(),
                due_date=timezone.now() + timedelta(days=30),
                duration_days=30,
                status='active'
            )
            
            deleted_clients.append(client)
        
        # Get client metrics
        metrics = ClientReportService.get_client_metrics()
        
        # Calculate expected average repayment rate (only from active clients)
        # All active clients have 50% repayment rate
        expected_avg_repayment_rate = 50.0
        
        # Verify average calculations exclude deleted clients
        # If deleted clients were included, the average would be lower
        assert abs(metrics['avg_repayment_rate'] - expected_avg_repayment_rate) < 1.0, \
            f"Average repayment rate {metrics['avg_repayment_rate']} should be close to {expected_avg_repayment_rate}"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()
        for client in active_clients + deleted_clients:
            client.delete()


class TestPerformanceCategorization(TestCase):
    """
    Test Property 11: Client performance categorization
    
    Feature: reports-system-enhancement, Property 11: Client performance categorization
    Validates: Requirements 2.3
    
    For any client with a calculated score, the performance category should be
    Excellent if score >= 85, Good if 70 <= score < 85, Average if 50 <= score < 70,
    and Poor if score < 50.
    """
    
    def setUp(self):
        """Set up test data"""
        self.product = LoanProduct.objects.create(
            name='Test Product Perf',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        score=st.floats(min_value=0.0, max_value=100.0, allow_nan=False, allow_infinity=False)
    )
    def test_performance_category_matches_score_thresholds(self, score):
        """
        Property: Performance category should match the score thresholds:
        - Excellent: score >= 85
        - Good: 70 <= score < 85
        - Average: 50 <= score < 70
        - Poor: score < 50
        """
        # Determine expected category based on score
        if score >= ClientReportService.EXCELLENT_THRESHOLD:
            expected_category = 'Excellent'
        elif score >= ClientReportService.GOOD_THRESHOLD:
            expected_category = 'Good'
        elif score >= ClientReportService.AVERAGE_THRESHOLD:
            expected_category = 'Average'
        else:
            expected_category = 'Poor'
        
        # Verify the categorization logic
        if score >= 85:
            assert expected_category == 'Excellent', \
                f"Score {score} should be categorized as Excellent"
        elif score >= 70:
            assert expected_category == 'Good', \
                f"Score {score} should be categorized as Good"
        elif score >= 50:
            assert expected_category == 'Average', \
                f"Score {score} should be categorized as Average"
        else:
            assert expected_category == 'Poor', \
                f"Score {score} should be categorized as Poor"
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_excellent=st.integers(min_value=0, max_value=3),
        num_good=st.integers(min_value=0, max_value=3),
        num_average=st.integers(min_value=0, max_value=3),
        num_poor=st.integers(min_value=0, max_value=3)
    )
    def test_performance_distribution_counts_correctly(
        self, num_excellent, num_good, num_average, num_poor
    ):
        """
        Property: Performance distribution should correctly count clients in each category.
        """
        # Skip if no clients
        if num_excellent + num_good + num_average + num_poor == 0:
            return
        
        created_clients = []
        
        # Create clients with scores in Excellent range (85-100)
        for i in range(num_excellent):
            client = CustomUser.objects.create_user(
                username=f'excellent_{i}_{uuid.uuid4().hex[:6]}',
                email=f'excellent{i}_{uuid.uuid4().hex[:6]}@example.com',
                phone_number=f'+25470{5000000 + i}',
                first_name=f'Excellent{i}',
                last_name='Client',
                role='borrower',
                is_active=True,
                status='active'
            )
            
            # Create loans to achieve high score (90% repayment rate)
            for j in range(3):
                app = LoanApplication.objects.create(
                    application_number=f'APP-EXC-{uuid.uuid4().hex[:6]}',
                    borrower=client,
                    loan_product=self.product,
                    requested_amount=Decimal('10000'),
                    requested_duration=30,
                    purpose='Test',
                    status='approved'
                )
                
                loan = Loan.objects.create(
                    loan_number=f'LOAN-EXC-{uuid.uuid4().hex[:6]}',
                    application=app,
                    borrower=client,
                    principal_amount=Decimal('10000'),
                    interest_amount=Decimal('1000'),
                    processing_fee=Decimal('500'),
                    total_amount=Decimal('11500'),
                    amount_paid=Decimal('10350'),  # 90% paid
                    disbursement_date=timezone.now() - timedelta(days=60),
                    due_date=timezone.now() - timedelta(days=30),
                    duration_days=30,
                    status='paid'
                )
            
            created_clients.append(client)
        
        # Create clients with scores in Good range (70-84)
        for i in range(num_good):
            client = CustomUser.objects.create_user(
                username=f'good_{i}_{uuid.uuid4().hex[:6]}',
                email=f'good{i}_{uuid.uuid4().hex[:6]}@example.com',
                phone_number=f'+25470{6000000 + i}',
                first_name=f'Good{i}',
                last_name='Client',
                role='borrower',
                is_active=True,
                status='active'
            )
            
            # Create loans to achieve good score (75% repayment rate)
            for j in range(2):
                app = LoanApplication.objects.create(
                    application_number=f'APP-GOOD-{uuid.uuid4().hex[:6]}',
                    borrower=client,
                    loan_product=self.product,
                    requested_amount=Decimal('10000'),
                    requested_duration=30,
                    purpose='Test',
                    status='approved'
                )
                
                loan = Loan.objects.create(
                    loan_number=f'LOAN-GOOD-{uuid.uuid4().hex[:6]}',
                    application=app,
                    borrower=client,
                    principal_amount=Decimal('10000'),
                    interest_amount=Decimal('1000'),
                    processing_fee=Decimal('500'),
                    total_amount=Decimal('11500'),
                    amount_paid=Decimal('8625'),  # 75% paid
                    disbursement_date=timezone.now() - timedelta(days=60),
                    due_date=timezone.now() + timedelta(days=30),
                    duration_days=30,
                    status='active'
                )
            
            created_clients.append(client)
        
        # Create clients with scores in Average range (50-69)
        for i in range(num_average):
            client = CustomUser.objects.create_user(
                username=f'average_{i}_{uuid.uuid4().hex[:6]}',
                email=f'average{i}_{uuid.uuid4().hex[:6]}@example.com',
                phone_number=f'+25470{7000000 + i}',
                first_name=f'Average{i}',
                last_name='Client',
                role='borrower',
                is_active=True,
                status='active'
            )
            
            # Create loan to achieve average score (60% repayment rate)
            app = LoanApplication.objects.create(
                application_number=f'APP-AVG-{uuid.uuid4().hex[:6]}',
                borrower=client,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-AVG-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=client,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                amount_paid=Decimal('6900'),  # 60% paid
                disbursement_date=timezone.now() - timedelta(days=60),
                due_date=timezone.now() + timedelta(days=30),
                duration_days=30,
                status='active'
            )
            
            created_clients.append(client)
        
        # Create clients with scores in Poor range (<50)
        for i in range(num_poor):
            client = CustomUser.objects.create_user(
                username=f'poor_{i}_{uuid.uuid4().hex[:6]}',
                email=f'poor{i}_{uuid.uuid4().hex[:6]}@example.com',
                phone_number=f'+25470{8000000 + i}',
                first_name=f'Poor{i}',
                last_name='Client',
                role='borrower',
                is_active=True,
                status='active'
            )
            
            # Create loan to achieve poor score (20% repayment rate)
            app = LoanApplication.objects.create(
                application_number=f'APP-POOR-{uuid.uuid4().hex[:6]}',
                borrower=client,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-POOR-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=client,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                amount_paid=Decimal('2300'),  # 20% paid
                disbursement_date=timezone.now() - timedelta(days=60),
                due_date=timezone.now() - timedelta(days=30),
                duration_days=30,
                status='active'
            )
            
            created_clients.append(client)
        
        # Get performance distribution
        distribution = ClientReportService.get_performance_distribution()
        
        # Verify counts (allow some tolerance due to scoring algorithm complexity)
        # The exact counts may vary slightly due to the scoring algorithm
        total_clients = num_excellent + num_good + num_average + num_poor
        total_distributed = sum(distribution.values())
        
        assert total_distributed == total_clients, \
            f"Total distributed {total_distributed} should equal total clients {total_clients}"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()
        for client in created_clients:
            client.delete()


class TestTopPerformersRanking(TestCase):
    """
    Test Property 12: Top performers ranking
    
    Feature: reports-system-enhancement, Property 12: Top performers ranking
    Validates: Requirements 2.4
    
    For any set of clients, when displaying top performers, the list should be
    ordered by repayment_rate in descending order, and the count should not exceed 10.
    """
    
    def setUp(self):
        """Set up test data"""
        self.product = LoanProduct.objects.create(
            name='Test Product Top',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        num_clients=st.integers(min_value=3, max_value=15)
    )
    def test_top_performers_ordered_by_repayment_rate_descending(self, num_clients):
        """
        Property: Top performers list should be ordered by repayment_rate in descending order.
        """
        created_clients = []
        
        # Create clients with varying repayment rates
        for i in range(num_clients):
            client = CustomUser.objects.create_user(
                username=f'top_client_{i}_{uuid.uuid4().hex[:6]}',
                email=f'top{i}_{uuid.uuid4().hex[:6]}@example.com',
                phone_number=f'+25470{9000000 + i}',
                first_name=f'Top{i}',
                last_name='Client',
                role='borrower',
                is_active=True,
                status='active'
            )
            
            # Create loan with varying repayment rates
            # Repayment rate will be (i+1) * 5% to create variation
            repayment_percentage = min((i + 1) * 5, 100)
            
            app = LoanApplication.objects.create(
                application_number=f'APP-TOP-{uuid.uuid4().hex[:6]}',
                borrower=client,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            total_amount = Decimal('11500')
            amount_paid = total_amount * Decimal(str(repayment_percentage / 100))
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-TOP-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=client,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=total_amount,
                amount_paid=amount_paid,
                disbursement_date=timezone.now(),
                due_date=timezone.now() + timedelta(days=30),
                duration_days=30,
                status='active'
            )
            
            created_clients.append(client)
        
        # Get top performers
        top_performers = ClientReportService.get_top_performers(limit=10)
        
        # Verify list is ordered by repayment_rate descending
        for i in range(len(top_performers) - 1):
            current_rate = top_performers[i]['repayment_rate']
            next_rate = top_performers[i + 1]['repayment_rate']
            assert current_rate >= next_rate, \
                f"Top performers not ordered correctly: {current_rate} < {next_rate} at position {i}"
        
        # Verify count does not exceed 10
        assert len(top_performers) <= 10, \
            f"Top performers count {len(top_performers)} exceeds limit of 10"
        
        # If we have more than 10 clients, verify we only get 10
        if num_clients > 10:
            assert len(top_performers) == 10, \
                f"Should return exactly 10 performers when more than 10 clients exist"
        else:
            assert len(top_performers) == num_clients, \
                f"Should return all {num_clients} clients when less than 10 exist"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()
        for client in created_clients:
            client.delete()
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_clients=st.integers(min_value=5, max_value=20),
        limit=st.integers(min_value=1, max_value=15)
    )
    def test_top_performers_respects_limit_parameter(self, num_clients, limit):
        """
        Property: Top performers should respect the limit parameter and return
        at most 'limit' performers.
        """
        created_clients = []
        
        # Create clients with varying repayment rates
        for i in range(num_clients):
            client = CustomUser.objects.create_user(
                username=f'limit_client_{i}_{uuid.uuid4().hex[:6]}',
                email=f'limit{i}_{uuid.uuid4().hex[:6]}@example.com',
                phone_number=f'+25471{0000000 + i}',
                first_name=f'Limit{i}',
                last_name='Client',
                role='borrower',
                is_active=True,
                status='active'
            )
            
            # Create loan with varying repayment rates
            repayment_percentage = min((i + 1) * 4, 100)
            
            app = LoanApplication.objects.create(
                application_number=f'APP-LIM-{uuid.uuid4().hex[:6]}',
                borrower=client,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            total_amount = Decimal('11500')
            amount_paid = total_amount * Decimal(str(repayment_percentage / 100))
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-LIM-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=client,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=total_amount,
                amount_paid=amount_paid,
                disbursement_date=timezone.now(),
                due_date=timezone.now() + timedelta(days=30),
                duration_days=30,
                status='active'
            )
            
            created_clients.append(client)
        
        # Get top performers with custom limit
        top_performers = ClientReportService.get_top_performers(limit=limit)
        
        # Verify count does not exceed limit
        expected_count = min(limit, num_clients)
        assert len(top_performers) <= limit, \
            f"Top performers count {len(top_performers)} exceeds limit {limit}"
        assert len(top_performers) == expected_count, \
            f"Expected {expected_count} performers, got {len(top_performers)}"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()
        for client in created_clients:
            client.delete()



class TestRolledOverLoanExclusion(TestCase):
    """
    Test Property 4: Rolled-over loan exclusion from active reports
    
    Feature: reports-system-enhancement, Property 4: Rolled-over loan exclusion from active reports
    Validates: Requirements 4.2, 4.3, 4.4
    
    For any report displaying active loans (loans due, delinquent, arrears),
    no loans with status 'rolled_over' or is_rolled_over flag set to true
    should appear in the results.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser_rollover',
            email='test_rollover@example.com',
            phone_number='+254700000010',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product Rollover',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        num_active_loans=st.integers(min_value=1, max_value=10),
        num_rolled_over_loans=st.integers(min_value=1, max_value=10)
    )
    def test_rolled_over_loans_excluded_from_active_reports(
        self, num_active_loans, num_rolled_over_loans
    ):
        """
        Property: No loans with status='rolled_over' or is_rolled_over=True
        should appear in active loan reports.
        """
        # Create active loans (not rolled over)
        active_loans = []
        for i in range(num_active_loans):
            app = LoanApplication.objects.create(
                application_number=f'APP-ACTIVE-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-ACTIVE-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=timezone.now(),
                due_date=timezone.now() + timedelta(days=30),
                duration_days=30,
                status='active',
                is_rolled_over=False,
                is_deleted=False
            )
            active_loans.append(loan)
        
        # Create rolled-over loans (mix of status='rolled_over' and is_rolled_over=True)
        rolled_over_loans = []
        for i in range(num_rolled_over_loans):
            app = LoanApplication.objects.create(
                application_number=f'APP-ROLLED-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            # Alternate between status='rolled_over' and is_rolled_over=True
            if i % 2 == 0:
                status = 'rolled_over'
                is_rolled_over = True
            else:
                status = 'active'  # Status might still be active but flag is set
                is_rolled_over = True
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-ROLLED-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=timezone.now(),
                due_date=timezone.now() + timedelta(days=30),
                duration_days=30,
                status=status,
                is_rolled_over=is_rolled_over,
                is_deleted=False
            )
            rolled_over_loans.append(loan)
        
        # Apply filter to exclude rolled-over loans
        queryset = Loan.objects.all()
        filtered_queryset = ReportFilterService.apply_loan_status_filter(
            queryset, exclude_rolled_over=True, exclude_deleted=False
        )
        
        # Verify no rolled-over loans are in the results
        filtered_ids = set(filtered_queryset.values_list('id', flat=True))
        for loan in rolled_over_loans:
            assert loan.id not in filtered_ids, \
                f"Rolled-over loan {loan.loan_number} (status={loan.status}, " \
                f"is_rolled_over={loan.is_rolled_over}) was incorrectly included"
        
        # Verify all active loans are in the results
        for loan in active_loans:
            assert loan.id in filtered_ids, \
                f"Active loan {loan.loan_number} was incorrectly excluded"
        
        # Verify the filtered queryset contains only active loans
        for loan in filtered_queryset:
            assert loan.status != 'rolled_over', \
                f"Loan {loan.loan_number} has status='rolled_over' but was included"
            assert not loan.is_rolled_over, \
                f"Loan {loan.loan_number} has is_rolled_over=True but was included"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()


class TestSoftDeletedLoanExclusion(TestCase):
    """
    Test Property 5: Soft-deleted loan exclusion from active reports
    
    Feature: reports-system-enhancement, Property 5: Soft-deleted loan exclusion from active reports
    Validates: Requirements 5.1, 5.2, 5.3, 5.4
    
    For any report displaying active loans, no loans with is_deleted flag
    set to true should appear in the results.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser_deleted',
            email='test_deleted@example.com',
            phone_number='+254700000011',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product Deleted',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        num_active_loans=st.integers(min_value=1, max_value=10),
        num_deleted_loans=st.integers(min_value=1, max_value=10)
    )
    def test_soft_deleted_loans_excluded_from_active_reports(
        self, num_active_loans, num_deleted_loans
    ):
        """
        Property: No loans with is_deleted=True should appear in active loan reports.
        """
        # Create active loans (not deleted)
        active_loans = []
        for i in range(num_active_loans):
            app = LoanApplication.objects.create(
                application_number=f'APP-NOTDEL-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-NOTDEL-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=timezone.now(),
                due_date=timezone.now() + timedelta(days=30),
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            active_loans.append(loan)
        
        # Create soft-deleted loans
        deleted_loans = []
        for i in range(num_deleted_loans):
            app = LoanApplication.objects.create(
                application_number=f'APP-DEL-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-DEL-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=timezone.now(),
                due_date=timezone.now() + timedelta(days=30),
                duration_days=30,
                status='active',
                is_deleted=True,
                deleted_at=timezone.now(),
                is_rolled_over=False
            )
            deleted_loans.append(loan)
        
        # Apply filter to exclude deleted loans
        queryset = Loan.objects.all()
        filtered_queryset = ReportFilterService.apply_loan_status_filter(
            queryset, exclude_rolled_over=False, exclude_deleted=True
        )
        
        # Verify no deleted loans are in the results
        filtered_ids = set(filtered_queryset.values_list('id', flat=True))
        for loan in deleted_loans:
            assert loan.id not in filtered_ids, \
                f"Deleted loan {loan.loan_number} (is_deleted={loan.is_deleted}) " \
                f"was incorrectly included"
        
        # Verify all active loans are in the results
        for loan in active_loans:
            assert loan.id in filtered_ids, \
                f"Active loan {loan.loan_number} was incorrectly excluded"
        
        # Verify the filtered queryset contains only non-deleted loans
        for loan in filtered_queryset:
            assert not loan.is_deleted, \
                f"Loan {loan.loan_number} has is_deleted=True but was included"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()



class TestDailyFilterPrecision(TestCase):
    """
    Test Property 23: Daily filter precision
    
    Feature: reports-system-enhancement, Property 23: Daily filter precision
    Validates: Requirements 9.2
    
    For any loans due report with daily filter selected, all returned loans
    should have due_date equal to the current date.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser_daily',
            email='test_daily@example.com',
            phone_number='+254700000010',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product Daily',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['daily']
        )
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_loans_today=st.integers(min_value=1, max_value=10),
        num_loans_other_days=st.integers(min_value=1, max_value=10)
    )
    def test_daily_filter_returns_only_loans_due_today(
        self, num_loans_today, num_loans_other_days
    ):
        """
        Property: When daily filter is selected, all returned loans should have
        due_date equal to the current date.
        """
        today = timezone.now().date()
        
        # Create loans due today
        loans_due_today = []
        for i in range(num_loans_today):
            app = LoanApplication.objects.create(
                application_number=f'APP-TODAY-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-TODAY-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=timezone.now() - timedelta(days=15),
                due_date=datetime.combine(today, datetime.min.time()),
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            loans_due_today.append(loan)
        
        # Create loans due on other days
        loans_due_other_days = []
        for i in range(num_loans_other_days):
            # Some before today, some after today
            if i % 2 == 0:
                due_date = today - timedelta(days=i + 1)
            else:
                due_date = today + timedelta(days=i + 1)
            
            app = LoanApplication.objects.create(
                application_number=f'APP-OTHER-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-OTHER-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=timezone.now() - timedelta(days=20),
                due_date=datetime.combine(due_date, datetime.min.time()),
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            loans_due_other_days.append(loan)
        
        # Apply daily filter (due_date = today)
        queryset = Loan.objects.filter(status='active')
        queryset = ReportFilterService.apply_loan_status_filter(
            queryset, exclude_rolled_over=True, exclude_deleted=True
        )
        # Filter for loans due today
        queryset = queryset.filter(due_date__date=today)
        
        # Verify all returned loans have due_date = today
        for loan in queryset:
            loan_due_date = loan.due_date.date() if hasattr(loan.due_date, 'date') else loan.due_date
            assert loan_due_date == today, \
                f"Loan {loan.loan_number} has due_date {loan_due_date} != today {today}"
        
        # Verify no loans due on other days are included
        filtered_ids = set(queryset.values_list('id', flat=True))
        for loan in loans_due_other_days:
            assert loan.id not in filtered_ids, \
                f"Loan {loan.loan_number} due on other day was incorrectly included"
        
        # Verify all loans due today are included
        for loan in loans_due_today:
            assert loan.id in filtered_ids, \
                f"Loan {loan.loan_number} due today was incorrectly excluded"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()


class TestWeeklyFilterRange(TestCase):
    """
    Test Property 24: Weekly filter range
    
    Feature: reports-system-enhancement, Property 24: Weekly filter range
    Validates: Requirements 9.3
    
    For any loans due report with weekly filter selected, all returned loans
    should have due_date within 7 days from the current date (inclusive).
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser_weekly',
            email='test_weekly@example.com',
            phone_number='+254700000011',
            first_name='Test',
            last_name='User'
        )
        
        self.product = LoanProduct.objects.create(
            name='Test Product Weekly',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['weekly']
        )
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_loans_within_week=st.integers(min_value=1, max_value=10),
        num_loans_outside_week=st.integers(min_value=1, max_value=10)
    )
    def test_weekly_filter_returns_loans_due_within_7_days(
        self, num_loans_within_week, num_loans_outside_week
    ):
        """
        Property: When weekly filter is selected, all returned loans should have
        due_date within 7 days from the current date (inclusive).
        """
        today = timezone.now().date()
        week_end = today + timedelta(days=7)
        
        # Create loans due within the next 7 days
        loans_within_week = []
        for i in range(num_loans_within_week):
            # Due date between today and today + 7 days
            days_offset = i % 8  # 0 to 7 days
            due_date = today + timedelta(days=days_offset)
            
            app = LoanApplication.objects.create(
                application_number=f'APP-WEEK-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-WEEK-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=timezone.now() - timedelta(days=15),
                due_date=datetime.combine(due_date, datetime.min.time()),
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            loans_within_week.append(loan)
        
        # Create loans due outside the next 7 days
        loans_outside_week = []
        for i in range(num_loans_outside_week):
            # Due date either before today or after today + 7 days
            if i % 2 == 0:
                due_date = today - timedelta(days=i + 1)
            else:
                due_date = today + timedelta(days=8 + i)
            
            app = LoanApplication.objects.create(
                application_number=f'APP-NOWEEK-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-NOWEEK-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=timezone.now() - timedelta(days=20),
                due_date=datetime.combine(due_date, datetime.min.time()),
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            loans_outside_week.append(loan)
        
        # Apply weekly filter (due_date within next 7 days)
        queryset = Loan.objects.filter(status='active')
        queryset = ReportFilterService.apply_loan_status_filter(
            queryset, exclude_rolled_over=True, exclude_deleted=True
        )
        # Filter for loans due within next 7 days
        queryset = queryset.filter(
            due_date__date__gte=today,
            due_date__date__lte=week_end
        )
        
        # Verify all returned loans have due_date within 7 days
        for loan in queryset:
            loan_due_date = loan.due_date.date() if hasattr(loan.due_date, 'date') else loan.due_date
            assert today <= loan_due_date <= week_end, \
                f"Loan {loan.loan_number} has due_date {loan_due_date} outside range [{today}, {week_end}]"
        
        # Verify no loans outside the week are included
        filtered_ids = set(queryset.values_list('id', flat=True))
        for loan in loans_outside_week:
            assert loan.id not in filtered_ids, \
                f"Loan {loan.loan_number} due outside week was incorrectly included"
        
        # Verify all loans within the week are included
        for loan in loans_within_week:
            assert loan.id in filtered_ids, \
                f"Loan {loan.loan_number} due within week was incorrectly excluded"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()


class TestFilterCompleteness(TestCase):
    """
    Test Property 25: Filter completeness (no false negatives)
    
    Feature: reports-system-enhancement, Property 25: Filter completeness (no false negatives)
    Validates: Requirements 9.5
    
    For any report with active filters, if a record matches all filter criteria,
    it should appear in the results.
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = CustomUser.objects.create_user(
            username='testuser_complete',
            email='test_complete@example.com',
            phone_number='+254700000012',
            first_name='Test',
            last_name='User'
        )
        
        self.product1 = LoanProduct.objects.create(
            name='Test Product 1',
            product_type='boost',
            description='Test product 1',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        self.product2 = LoanProduct.objects.create(
            name='Test Product 2',
            product_type='boost_plus',
            description='Test product 2',
            min_amount=Decimal('5000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('12.0'),
            processing_fee=Decimal('6.0'),
            min_duration=30,
            max_duration=180,
            available_repayment_methods=['monthly']
        )
    
    @settings(max_examples=50, deadline=None)
    @given(
        date_range=date_range_strategy(),
        num_matching_loans=st.integers(min_value=1, max_value=10)
    )
    def test_all_matching_records_are_included(self, date_range, num_matching_loans):
        """
        Property: If a record matches all filter criteria, it should appear in
        the results (no false negatives).
        """
        start_date, end_date = date_range
        
        # Create loans that match ALL filter criteria
        matching_loans = []
        for i in range(num_matching_loans):
            # Generate a date within the range
            days_diff = (end_date - start_date).days
            if days_diff == 0:
                loan_date = start_date
            else:
                offset = i % (days_diff + 1)
                loan_date = start_date + timedelta(days=offset)
            
            app = LoanApplication.objects.create(
                application_number=f'APP-COMPLETE-{uuid.uuid4().hex[:6]}',
                borrower=self.user,
                loan_product=self.product1,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            loan = Loan.objects.create(
                loan_number=f'LOAN-COMPLETE-{uuid.uuid4().hex[:6]}',
                application=app,
                borrower=self.user,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=datetime.combine(loan_date, datetime.min.time()),
                due_date=datetime.combine(loan_date + timedelta(days=30), datetime.min.time()),
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            matching_loans.append(loan)
        
        # Apply filters that should match all created loans
        queryset = Loan.objects.all()
        queryset = ReportFilterService.apply_date_range_filter(
            queryset, start_date, end_date, 'disbursement_date'
        )
        queryset = ReportFilterService.apply_loan_status_filter(
            queryset, exclude_rolled_over=True, exclude_deleted=True
        )
        queryset = ReportFilterService.apply_loan_product_filter(
            queryset, str(self.product1.id)
        )
        
        # Verify ALL matching loans are included (no false negatives)
        filtered_ids = set(queryset.values_list('id', flat=True))
        for loan in matching_loans:
            assert loan.id in filtered_ids, \
                f"Loan {loan.loan_number} matches all criteria but was excluded (false negative)"
        
        # Verify the count matches
        assert queryset.count() == len(matching_loans), \
            f"Expected {len(matching_loans)} loans, got {queryset.count()}"
        
        # Cleanup
        Loan.objects.all().delete()
        LoanApplication.objects.all().delete()


class TestProcessingFeesAggregation(TestCase):
    """
    Test Property 16: Processing fees aggregation
    
    Feature: reports-system-enhancement, Property 16: Processing fees aggregation
    Validates: Requirements 6.4
    
    For any filtered set of loans in the processing fees report, the total processing
    fees should equal the sum of the processing_fee field from all filtered loans.
    """
    
    def setUp(self):
        """Set up test data"""
        self.factory = RequestFactory()
        
        # Create test user
        self.user = CustomUser.objects.create_user(
            username='testuser_pf',
            email='testpf@example.com',
            phone_number='+254700000001',
            first_name='Test',
            last_name='User'
        )
        
        # Create test loan product
        self.product = LoanProduct.objects.create(
            name='Test Product PF',
            product_type='boost',
            description='Test product for processing fees',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        num_loans=st.integers(min_value=1, max_value=20),
        processing_fees=st.lists(
            st.decimals(
                min_value=Decimal('0.00'),
                max_value=Decimal('10000.00'),
                places=2
            ),
            min_size=1,
            max_size=20
        )
    )
    def test_processing_fees_aggregation_equals_sum(self, num_loans, processing_fees):
        """
        Property: The total processing fees should equal the sum of all individual
        processing_fee fields from filtered loans.
        """
        # Ensure we have the right number of fees
        if len(processing_fees) < num_loans:
            processing_fees = processing_fees + [Decimal('100.00')] * (num_loans - len(processing_fees))
        processing_fees = processing_fees[:num_loans]
        
        # Create loans with specific processing fees
        created_loans = []
        expected_total = Decimal('0.00')
        
        for i, fee in enumerate(processing_fees):
            # Create loan application
            application = LoanApplication.objects.create(
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000.00'),
                loan_purpose='Test purpose',
                status='approved'
            )
            
            # Create loan with specific processing fee
            loan = Loan.objects.create(
                borrower=self.user,
                application=application,
                principal_amount=Decimal('10000.00'),
                interest_amount=Decimal('1000.00'),
                processing_fee=fee,
                total_amount=Decimal('11000.00') + fee,
                disbursement_date=timezone.now().date(),
                due_date=timezone.now().date() + timedelta(days=30),
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            created_loans.append(loan)
            expected_total += fee
        
        # Get all loans (no filtering)
        queryset = Loan.objects.filter(
            id__in=[loan.id for loan in created_loans],
            is_deleted=False,
            is_rolled_over=False
        )
        
        # Calculate total processing fees using aggregation
        from django.db.models import Sum
        calculated_total = queryset.aggregate(
            total=Sum('processing_fee')
        )['total'] or Decimal('0.00')
        
        # Property: Calculated total should equal expected sum
        self.assertEqual(
            calculated_total,
            expected_total,
            f"Processing fees aggregation failed: {calculated_total} != {expected_total}"
        )
        
        # Cleanup
        for loan in created_loans:
            loan.application.delete()
            loan.delete()


class TestPeriodComparisonDuration(TestCase):
    """
    Test Property 17: Period comparison duration equality
    
    Feature: reports-system-enhancement, Property 17: Period comparison duration equality
    Validates: Requirements 6.5, 8.2
    
    For any report with period comparison, the previous period should have the same
    duration (in days) as the current period.
    """
    
    def setUp(self):
        """Set up test data"""
        self.factory = RequestFactory()
    
    @settings(max_examples=50, deadline=None)
    @given(
        start_date=st.dates(
            min_value=date(2020, 1, 1),
            max_value=date(2025, 6, 1)
        ),
        duration_days=st.integers(min_value=1, max_value=365)
    )
    def test_period_comparison_has_equal_duration(self, start_date, duration_days):
        """
        Property: The previous period should have the same duration as the current period.
        """
        # Calculate current period
        current_start = start_date
        current_end = start_date + timedelta(days=duration_days)
        current_duration = (current_end - current_start).days
        
        # Calculate previous period (same duration, ending at current_start)
        previous_end = current_start - timedelta(days=1)
        previous_start = previous_end - timedelta(days=current_duration)
        previous_duration = (previous_end - previous_start).days
        
        # Property: Durations should be equal
        self.assertEqual(
            current_duration,
            previous_duration,
            f"Period durations not equal: current={current_duration} days, previous={previous_duration} days"
        )
    
    @settings(max_examples=50, deadline=None)
    @given(
        date_range=date_range_strategy()
    )
    def test_period_comparison_calculation_logic(self, date_range):
        """
        Property: Given a date range, the previous period calculation should
        maintain the same duration.
        """
        current_start, current_end = date_range
        current_duration = (current_end - current_start).days
        
        # Calculate previous period using the same logic as the report
        previous_end = current_start - timedelta(days=1)
        previous_start = previous_end - timedelta(days=current_duration)
        previous_duration = (previous_end - previous_start).days
        
        # Property: Previous period duration equals current period duration
        self.assertEqual(
            previous_duration,
            current_duration,
            f"Period comparison failed: previous={previous_duration} != current={current_duration}"
        )
        
        # Additional property: Previous period ends exactly one day before current starts
        self.assertEqual(
            previous_end + timedelta(days=1),
            current_start,
            "Previous period should end exactly one day before current period starts"
        )



class TestInterestIncomeAggregation(TestCase):
    """
    Test Property 18: Interest income aggregation
    
    Feature: reports-system-enhancement, Property 18: Interest income aggregation
    Validates: Requirements 7.3
    
    For any filtered set of loans in the interest income report, the total interest
    income should equal the sum of the interest_amount field from all filtered loans.
    """
    
    def setUp(self):
        """Set up test data"""
        self.factory = RequestFactory()
        
        # Create test user
        self.user = CustomUser.objects.create_user(
            username='testuser_ii',
            email='testii@example.com',
            phone_number='+254700000002',
            first_name='Test',
            last_name='User'
        )
        
        # Create test loan product
        self.product = LoanProduct.objects.create(
            name='Test Product II',
            product_type='boost',
            description='Test product for interest income',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        num_loans=st.integers(min_value=1, max_value=20),
        interest_amounts=st.lists(
            st.decimals(
                min_value=Decimal('0.00'),
                max_value=Decimal('50000.00'),
                places=2
            ),
            min_size=1,
            max_size=20
        )
    )
    def test_interest_income_aggregation_equals_sum(self, num_loans, interest_amounts):
        """
        Property: The total interest income should equal the sum of all individual
        interest_amount fields from filtered loans.
        """
        # Ensure we have the right number of interest amounts
        if len(interest_amounts) < num_loans:
            interest_amounts = interest_amounts + [Decimal('1000.00')] * (num_loans - len(interest_amounts))
        interest_amounts = interest_amounts[:num_loans]
        
        # Create loans with specific interest amounts
        created_loans = []
        expected_total = Decimal('0.00')
        
        for i, interest in enumerate(interest_amounts):
            # Create loan application
            application = LoanApplication.objects.create(
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000.00'),
                loan_purpose='Test purpose',
                status='approved'
            )
            
            # Create loan with specific interest amount
            principal = Decimal('10000.00')
            processing_fee = Decimal('500.00')
            total = principal + interest + processing_fee
            
            loan = Loan.objects.create(
                borrower=self.user,
                application=application,
                principal_amount=principal,
                interest_amount=interest,
                processing_fee=processing_fee,
                total_amount=total,
                disbursement_date=timezone.now().date(),
                due_date=timezone.now().date() + timedelta(days=30),
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            created_loans.append(loan)
            expected_total += interest
        
        # Get all loans (no filtering) - exclude rolled-over and deleted
        queryset = Loan.objects.filter(
            id__in=[loan.id for loan in created_loans],
            is_deleted=False,
            is_rolled_over=False,
            status='active'
        )
        
        # Calculate total interest income using aggregation
        from django.db.models import Sum
        calculated_total = queryset.aggregate(
            total=Sum('interest_amount')
        )['total'] or Decimal('0.00')
        
        # Property: Calculated total should equal expected sum
        self.assertEqual(
            calculated_total,
            expected_total,
            f"Interest income aggregation failed: {calculated_total} != {expected_total}"
        )
        
        # Additional property: Each loan's interest should be non-negative
        for loan in queryset:
            self.assertGreaterEqual(
                loan.interest_amount,
                Decimal('0.00'),
                f"Loan {loan.id} has negative interest amount"
            )
        
        # Cleanup
        for loan in created_loans:
            loan.application.delete()
            loan.delete()
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_loans=st.integers(min_value=5, max_value=15),
        filter_ratio=st.floats(min_value=0.1, max_value=0.9)
    )
    def test_interest_income_aggregation_with_filtering(self, num_loans, filter_ratio):
        """
        Property: When filtering loans, the aggregated interest should equal
        the sum of interest from only the filtered loans.
        """
        # Create loans with varying interest amounts
        created_loans = []
        all_interest = []
        
        for i in range(num_loans):
            interest = Decimal(str(1000 + (i * 500)))
            all_interest.append(interest)
            
            # Create loan application
            application = LoanApplication.objects.create(
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000.00'),
                loan_purpose='Test purpose',
                status='approved'
            )
            
            # Create loan
            loan = Loan.objects.create(
                borrower=self.user,
                application=application,
                principal_amount=Decimal('10000.00'),
                interest_amount=interest,
                processing_fee=Decimal('500.00'),
                total_amount=Decimal('10000.00') + interest + Decimal('500.00'),
                disbursement_date=timezone.now().date() - timedelta(days=i),
                due_date=timezone.now().date() + timedelta(days=30),
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            created_loans.append(loan)
        
        # Filter a subset of loans (by date range)
        filter_count = max(1, int(num_loans * filter_ratio))
        cutoff_date = timezone.now().date() - timedelta(days=filter_count)
        
        filtered_queryset = Loan.objects.filter(
            id__in=[loan.id for loan in created_loans],
            disbursement_date__gte=cutoff_date,
            is_deleted=False,
            is_rolled_over=False,
            status='active'
        )
        
        # Calculate expected total from filtered loans
        expected_filtered_total = sum([
            loan.interest_amount for loan in created_loans
            if loan.disbursement_date >= cutoff_date
        ])
        
        # Calculate total using aggregation
        from django.db.models import Sum
        calculated_filtered_total = filtered_queryset.aggregate(
            total=Sum('interest_amount')
        )['total'] or Decimal('0.00')
        
        # Property: Filtered aggregation should equal sum of filtered loans
        self.assertEqual(
            calculated_filtered_total,
            expected_filtered_total,
            f"Filtered interest aggregation failed: {calculated_filtered_total} != {expected_filtered_total}"
        )
        
        # Property: Filtered total should be <= total of all loans
        total_all = sum(all_interest)
        self.assertLessEqual(
            calculated_filtered_total,
            total_all,
            "Filtered total should not exceed total of all loans"
        )
        
        # Cleanup
        for loan in created_loans:
            loan.application.delete()
            loan.delete()



class TestRegistrationFeesProperties(TestCase):
    """
    Property tests for registration fees report metrics
    
    Tests Properties 20, 21, and 22 related to registration fees calculations
    """
    
    def setUp(self):
        """Set up test data"""
        self.factory = RequestFactory()
        
        # Create test user
        self.user = CustomUser.objects.create_user(
            username='testuser_regfees',
            email='test_regfees@example.com',
            phone_number='+254700000001',
            first_name='Test',
            last_name='User'
        )
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_clients=st.integers(min_value=1, max_value=20),
        fee_amounts=st.lists(
            st.decimals(
                min_value=Decimal('100'),
                max_value=Decimal('5000'),
                places=2
            ),
            min_size=1,
            max_size=20
        )
    )
    def test_property_20_maximum_value_identification(self, num_clients, fee_amounts):
        """
        Feature: reports-system-enhancement, Property 20: Maximum value identification
        Validates: Requirements 8.3
        
        For any dataset in the registration fees report, the highest single fee should be
        the maximum value of the registration_fee field across all filtered records.
        """
        # Ensure we have the right number of fee amounts
        if len(fee_amounts) < num_clients:
            fee_amounts = fee_amounts * ((num_clients // len(fee_amounts)) + 1)
        fee_amounts = fee_amounts[:num_clients]
        
        # Create clients with registration fees
        created_clients = []
        for i in range(num_clients):
            client = CustomUser.objects.create_user(
                username=f'client_max_{i}_{uuid.uuid4().hex[:8]}',
                email=f'client_max_{i}_{uuid.uuid4().hex[:8]}@example.com',
                phone_number=f'+25470{i:07d}',
                first_name=f'Client',
                last_name=f'Max{i}',
                role='borrower',
                registration_fee_amount=fee_amounts[i],
                registration_fee_paid=True,
                registration_fee_payment_date=timezone.now()
            )
            created_clients.append(client)
        
        try:
            # Calculate expected maximum
            expected_max = max(fee_amounts)
            
            # Get all registration fees from created clients
            actual_fees = [
                client.registration_fee_amount
                for client in created_clients
                if client.registration_fee_amount
            ]
            
            # Calculate actual maximum
            actual_max = max(actual_fees) if actual_fees else Decimal('0.00')
            
            # Property: The maximum value should match the expected maximum
            self.assertEqual(
                actual_max,
                expected_max,
                f"Maximum registration fee mismatch: {actual_max} != {expected_max}"
            )
            
            # Property: All other fees should be <= maximum
            for fee in actual_fees:
                self.assertLessEqual(
                    fee,
                    actual_max,
                    f"Fee {fee} exceeds maximum {actual_max}"
                )
        
        finally:
            # Cleanup
            for client in created_clients:
                client.delete()
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_clients=st.integers(min_value=1, max_value=20),
        days_to_pay_list=st.lists(
            st.integers(min_value=0, max_value=90),
            min_size=1,
            max_size=20
        )
    )
    def test_property_21_average_calculation_correctness(self, num_clients, days_to_pay_list):
        """
        Feature: reports-system-enhancement, Property 21: Average calculation correctness
        Validates: Requirements 8.4
        
        For any set of registration fees with payment dates, the average days to pay
        should equal the sum of (payment_date - registration_date) divided by the
        count of paid registrations.
        """
        # Ensure we have the right number of days
        if len(days_to_pay_list) < num_clients:
            days_to_pay_list = days_to_pay_list * ((num_clients // len(days_to_pay_list)) + 1)
        days_to_pay_list = days_to_pay_list[:num_clients]
        
        # Create clients with registration fees and payment dates
        created_clients = []
        base_registration_date = timezone.now() - timedelta(days=100)
        
        for i in range(num_clients):
            registration_date = base_registration_date + timedelta(days=i)
            payment_date = registration_date + timedelta(days=days_to_pay_list[i])
            
            client = CustomUser.objects.create_user(
                username=f'client_avg_{i}_{uuid.uuid4().hex[:8]}',
                email=f'client_avg_{i}_{uuid.uuid4().hex[:8]}@example.com',
                phone_number=f'+25471{i:07d}',
                first_name=f'Client',
                last_name=f'Avg{i}',
                role='borrower',
                registration_fee_amount=Decimal('500.00'),
                registration_fee_paid=True,
                registration_fee_payment_date=payment_date,
                created_at=registration_date
            )
            created_clients.append(client)
        
        try:
            # Calculate expected average
            expected_average = sum(days_to_pay_list) / len(days_to_pay_list)
            
            # Calculate actual average from clients
            actual_days_list = []
            for client in created_clients:
                if client.registration_fee_paid and client.registration_fee_payment_date:
                    days_diff = (client.registration_fee_payment_date.date() - client.created_at.date()).days
                    actual_days_list.append(days_diff)
            
            actual_average = sum(actual_days_list) / len(actual_days_list) if actual_days_list else 0
            
            # Property: The average should match the expected average (within rounding tolerance)
            self.assertAlmostEqual(
                actual_average,
                expected_average,
                places=1,
                msg=f"Average days to pay mismatch: {actual_average} != {expected_average}"
            )
            
            # Property: Average should be >= 0
            self.assertGreaterEqual(
                actual_average,
                0,
                "Average days to pay should be non-negative"
            )
            
            # Property: Average should be <= max days
            if actual_days_list:
                max_days = max(actual_days_list)
                self.assertLessEqual(
                    actual_average,
                    max_days,
                    "Average should not exceed maximum days"
                )
        
        finally:
            # Cleanup
            for client in created_clients:
                client.delete()
    
    @settings(max_examples=50, deadline=None)
    @given(
        current_period_total=st.decimals(
            min_value=Decimal('0'),
            max_value=Decimal('1000000'),
            places=2
        ),
        previous_period_total=st.decimals(
            min_value=Decimal('0'),
            max_value=Decimal('1000000'),
            places=2
        )
    )
    def test_property_22_arithmetic_difference_correctness(
        self, current_period_total, previous_period_total
    ):
        """
        Feature: reports-system-enhancement, Property 22: Arithmetic difference correctness
        Validates: Requirements 8.5
        
        For any period comparison display, the difference should equal
        current_period_total minus previous_period_total.
        """
        # Calculate expected difference
        expected_difference = current_period_total - previous_period_total
        
        # Simulate the calculation that would be done in the report
        calculated_difference = current_period_total - previous_period_total
        
        # Property: The difference should equal current - previous
        self.assertEqual(
            calculated_difference,
            expected_difference,
            f"Period difference mismatch: {calculated_difference} != {expected_difference}"
        )
        
        # Property: If current > previous, difference should be positive
        if current_period_total > previous_period_total:
            self.assertGreater(
                calculated_difference,
                Decimal('0'),
                "Difference should be positive when current > previous"
            )
        
        # Property: If current < previous, difference should be negative
        if current_period_total < previous_period_total:
            self.assertLess(
                calculated_difference,
                Decimal('0'),
                "Difference should be negative when current < previous"
            )
        
        # Property: If current == previous, difference should be zero
        if current_period_total == previous_period_total:
            self.assertEqual(
                calculated_difference,
                Decimal('0'),
                "Difference should be zero when current == previous"
            )
        
        # Property: Difference should satisfy: current = previous + difference
        reconstructed_current = previous_period_total + calculated_difference
        self.assertEqual(
            reconstructed_current,
            current_period_total,
            "Difference should allow reconstruction of current from previous"
        )



class TestExportDataConsistency(TestCase):
    """
    Test Property 19: Export data consistency
    
    Feature: reports-system-enhancement, Property 19: Export data consistency
    Validates: Requirements 7.4, 7.5, 12.2, 12.3, 12.4
    
    For any report export (PDF or Excel), the exported data should contain exactly
    the same records as currently displayed with active filters applied.
    """
    
    def setUp(self):
        """Set up test data"""
        self.factory = RequestFactory()
        
        # Create test user
        self.user = CustomUser.objects.create_user(
            username='exportuser',
            email='export@example.com',
            phone_number='+254700000001',
            first_name='Export',
            last_name='User'
        )
        
        # Create test loan product
        self.product = LoanProduct.objects.create(
            name='Export Test Product',
            product_type='boost',
            description='Test product for exports',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        num_loans=st.integers(min_value=1, max_value=20),
        filter_date_range=st.booleans(),
        filter_product=st.booleans()
    )
    def test_export_contains_same_records_as_filtered_view(
        self, num_loans, filter_date_range, filter_product
    ):
        """
        Property: Exported data should contain exactly the same records as the
        filtered view, with no additions or omissions.
        """
        from reports.export_service import ReportExportService
        from io import BytesIO
        import openpyxl
        
        # Create loans with varying dates
        loans = []
        base_date = date(2024, 1, 1)
        
        for i in range(num_loans):
            # Create loan application
            application = LoanApplication.objects.create(
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                duration=30,
                repayment_method='monthly',
                status='approved'
            )
            
            # Create loan with varying disbursement dates
            loan_date = base_date + timedelta(days=i * 10)
            loan = Loan.objects.create(
                application=application,
                borrower=self.user,
                loan_number=f'LOAN-EXPORT-{i:04d}',
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=loan_date,
                due_date=loan_date + timedelta(days=30),
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            loans.append(loan)
        
        # Apply filters
        queryset = Loan.objects.filter(is_deleted=False, is_rolled_over=False)
        
        if filter_date_range:
            # Filter to middle half of loans
            start_date = base_date + timedelta(days=num_loans * 2)
            end_date = base_date + timedelta(days=num_loans * 7)
            queryset = ReportFilterService.apply_date_range_filter(
                queryset, start_date, end_date, 'disbursement_date'
            )
        
        if filter_product:
            queryset = ReportFilterService.apply_loan_product_filter(
                queryset, str(self.product.id)
            )
        
        # Get filtered loan IDs
        filtered_loan_ids = set(queryset.values_list('id', flat=True))
        filtered_count = len(filtered_loan_ids)
        
        # Prepare report data
        report_data = {
            'loans': list(queryset.values(
                'id', 'loan_number', 'principal_amount', 'disbursement_date'
            ))
        }
        
        filters = {
            'start_date': start_date if filter_date_range else None,
            'end_date': end_date if filter_date_range else None,
            'product_id': str(self.product.id) if filter_product else None
        }
        
        # Export to Excel
        export_service = ReportExportService()
        response = export_service.export_to_excel(report_data, 'test_report', filters)
        
        # Parse Excel file
        excel_buffer = BytesIO(response.content)
        workbook = openpyxl.load_workbook(excel_buffer)
        sheet = workbook.active
        
        # Count data rows (excluding header and metadata rows)
        # Find where data starts (after "Loan Number" header)
        data_start_row = None
        for row_idx, row in enumerate(sheet.iter_rows(min_row=1, max_row=20), start=1):
            if row[0].value == 'Loan Number':
                data_start_row = row_idx + 1
                break
        
        if data_start_row:
            exported_loan_numbers = []
            for row in sheet.iter_rows(min_row=data_start_row, values_only=True):
                if row[0]:  # If first cell has value
                    exported_loan_numbers.append(row[0])
            
            exported_count = len(exported_loan_numbers)
            
            # Property: Exported count should equal filtered count
            self.assertEqual(
                exported_count, filtered_count,
                f"Export should contain {filtered_count} records but contains {exported_count}"
            )
        
        # Clean up
        for loan in loans:
            loan.application.delete()
            loan.delete()


class TestExportDownloadInitiation(TestCase):
    """
    Test Property 30: Export download initiation
    
    Feature: reports-system-enhancement, Property 30: Export download initiation
    Validates: Requirements 12.5
    
    For any completed export operation, the HTTP response should have
    Content-Disposition header set to 'attachment' to trigger browser download.
    """
    
    def setUp(self):
        """Set up test data"""
        self.factory = RequestFactory()
        
        # Create test user
        self.user = CustomUser.objects.create_user(
            username='downloaduser',
            email='download@example.com',
            phone_number='+254700000002',
            first_name='Download',
            last_name='User'
        )
        
        # Create test loan product
        self.product = LoanProduct.objects.create(
            name='Download Test Product',
            product_type='boost',
            description='Test product for downloads',
            min_amount=Decimal('1000'),
            max_amount=Decimal('50000'),
            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(
        export_format=st.sampled_from(['pdf', 'excel']),
        num_records=st.integers(min_value=0, max_value=10)
    )
    def test_export_response_has_attachment_header(self, export_format, num_records):
        """
        Property: All export responses should have Content-Disposition header
        set to 'attachment' to trigger browser download.
        """
        from reports.export_service import ReportExportService
        
        # Create test loans
        loans = []
        for i in range(num_records):
            application = LoanApplication.objects.create(
                borrower=self.user,
                loan_product=self.product,
                requested_amount=Decimal('10000'),
                duration=30,
                repayment_method='monthly',
                status='approved'
            )
            
            loan = Loan.objects.create(
                application=application,
                borrower=self.user,
                loan_number=f'LOAN-DL-{i:04d}',
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=date(2024, 1, 1),
                due_date=date(2024, 1, 31),
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            loans.append(loan)
        
        # Prepare report data
        report_data = {
            'loans': list(Loan.objects.filter(
                is_deleted=False, is_rolled_over=False
            ).values('id', 'loan_number', 'principal_amount'))
        }
        
        filters = {}
        
        # Export based on format
        export_service = ReportExportService()
        
        if export_format == 'pdf':
            response = export_service.export_to_pdf(report_data, 'test_report', filters)
        else:
            response = export_service.export_to_excel(report_data, 'test_report', filters)
        
        # Property: Response must have Content-Disposition header with 'attachment'
        self.assertIn('Content-Disposition', response)
        content_disposition = response['Content-Disposition']
        self.assertIn('attachment', content_disposition.lower(),
                     f"Content-Disposition should contain 'attachment' but got: {content_disposition}")
        
        # Property: Response should have appropriate content type
        if export_format == 'pdf':
            self.assertEqual(response['Content-Type'], 'application/pdf')
        else:
            self.assertIn('spreadsheet', response['Content-Type'].lower())
        
        # Clean up
        for loan in loans:
            loan.application.delete()
            loan.delete()

