"""
Property-Based Tests for Disbursed Loans Report Filtering (Task 2.3)

Feature: comprehensive-reports-and-fixes
Tests Properties 1 and 2 from the design document.

These tests use hypothesis to verify filtering logic works correctly across all possible inputs.
"""
import os
import sys
import django
from decimal import Decimal
from datetime import datetime, timedelta, date

# Setup Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'branch_system.settings')
django.setup()

from django.test import TestCase, RequestFactory
from django.contrib.auth import get_user_model
from django.utils import timezone
from hypothesis import given, strategies as st, settings, assume
from hypothesis.extra.django import TestCase as HypothesisTestCase

from loans.models import Loan, LoanApplication, LoanProduct
from users.models import Branch

User = get_user_model()


class DisbursedLoansFilteringPropertiesTest(HypothesisTestCase):
    """
    Property-based tests for disbursed loans report filtering logic.
    
    **Validates: Requirements 1.2, 1.4, 1.5, 1.6**
    """
    
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        # Create test data that will be reused
        cls.factory = RequestFactory()
        
        # Create test user with staff permissions
        cls.user = User.objects.create_user(
            username='test_staff',
            email='staff@test.com',
            password='testpass123',
            role='admin',
            is_staff=True,
            is_superuser=True
        )
        
        # Create test branch
        cls.branch = Branch.objects.create(
            name='Test Branch',
            code='TB001',
            is_active=True
        )
        
        # Create test loan product
        cls.loan_product = LoanProduct.objects.create(
            name='Test Product',
            product_type='boost',
            description='Test loan product',
            min_amount=Decimal('1000.00'),
            max_amount=Decimal('100000.00'),
            interest_rate=Decimal('10.00'),
            processing_fee=Decimal('5.00'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly'],
            is_active=True
        )
        
        # Create test borrower
        cls.borrower = User.objects.create_user(
            username='test_borrower',
            email='borrower@test.com',
            password='testpass123',
            role='borrower',
            first_name='Test',
            last_name='Borrower',
            phone_number='+254700000000',
            branch=cls.branch
        )
    
    def tearDown(self):
        """Clean up test data after each test"""
        # Delete loans and applications created during tests
        Loan.objects.filter(borrower=self.borrower).delete()
        LoanApplication.objects.filter(borrower=self.borrower).delete()
    
    def create_test_loan(self, status, disbursement_date):
        """Helper method to create a test loan with specific status and disbursement date"""
        # Create loan application
        application = LoanApplication.objects.create(
            borrower=self.borrower,
            loan_product=self.loan_product,
            requested_amount=Decimal('10000.00'),
            requested_duration=30,
            purpose='Test loan',
            repayment_method='monthly',
            status='approved'
        )
        
        # Create loan
        loan = Loan.objects.create(
            application=application,
            borrower=self.borrower,
            principal_amount=Decimal('10000.00'),
            interest_amount=Decimal('1000.00'),
            processing_fee=Decimal('500.00'),
            total_amount=Decimal('11500.00'),
            disbursement_date=disbursement_date,
            due_date=disbursement_date + timedelta(days=30),
            duration_days=30,
            status=status,
            is_deleted=False
        )
        
        return loan
    
    @settings(max_examples=50, deadline=None)
    @given(
        status=st.sampled_from(['active', 'paid', 'defaulted', 'rolled_over', 'written_off', 'pending'])
    )
    def test_property_1_disbursed_loans_filter_correctness(self, status):
        """
        **Property 1: Disbursed Loans Filter Correctness**
        
        For any set of loans in the database, when the disbursed loans report is filtered by status,
        the results should only include loans with status in ['active', 'paid', 'defaulted', 
        'rolled_over', 'written_off'].
        
        **Validates: Requirements 1.2**
        """
        # Create a loan with the given status
        disbursement_date = datetime.now()
        loan = self.create_test_loan(status, disbursement_date)
        
        # Define disbursed statuses
        disbursed_statuses = ['active', 'paid', 'defaulted', 'rolled_over', 'written_off']
        
        # Query loans using the same logic as the view
        loans = Loan.objects.filter(
            status__in=disbursed_statuses,
            is_deleted=False
        )
        
        # Property: All returned loans must have status in disbursed_statuses
        for loan_obj in loans:
            assert loan_obj.status in disbursed_statuses, \
                f"Loan {loan_obj.loan_number} has status '{loan_obj.status}' which is not in disbursed_statuses"
        
        # Property: If loan status is in disbursed_statuses, it should be included
        if status in disbursed_statuses:
            assert loans.filter(id=loan.id).exists(), \
                f"Loan with status '{status}' should be included in disbursed loans"
        else:
            # If status is not in disbursed_statuses, it should NOT be included
            assert not loans.filter(id=loan.id).exists(), \
                f"Loan with status '{status}' should NOT be included in disbursed loans"
    
    @settings(max_examples=50, deadline=None)
    @given(
        days_offset=st.integers(min_value=-30, max_value=30),
        filter_period=st.sampled_from(['today', 'this_week', 'total'])
    )
    def test_property_2_date_filtering_accuracy(self, days_offset, filter_period):
        """
        **Property 2: Date Filtering Accuracy**
        
        For any date range (start_date, end_date) and any set of loans, when filtering by date range,
        all returned loans should have disbursement_date >= start_date AND disbursement_date <= end_date,
        and all loans meeting this criteria should be included.
        
        **Validates: Requirements 1.4, 1.5, 1.6**
        """
        # Create a loan with disbursement date offset from today
        today = date.today()
        disbursement_date = datetime.combine(today + timedelta(days=days_offset), datetime.min.time())
        
        loan = self.create_test_loan('active', disbursement_date)
        
        # Apply period filter logic from the view
        if filter_period == 'today':
            # Filter loans disbursed today
            start_date = today
            end_date = today
            loans = Loan.objects.filter(
                status__in=['active', 'paid', 'defaulted', 'rolled_over', 'written_off'],
                is_deleted=False,
                disbursement_date__date=today
            )
        elif filter_period == 'this_week':
            # Filter loans disbursed this week (Monday to Sunday)
            week_start = today - timedelta(days=today.weekday())
            week_end = week_start + timedelta(days=6)
            start_date = week_start
            end_date = week_end
            loans = Loan.objects.filter(
                status__in=['active', 'paid', 'defaulted', 'rolled_over', 'written_off'],
                is_deleted=False,
                disbursement_date__date__gte=week_start,
                disbursement_date__date__lte=week_end
            )
        else:  # 'total' means no date filter
            start_date = None
            end_date = None
            loans = Loan.objects.filter(
                status__in=['active', 'paid', 'defaulted', 'rolled_over', 'written_off'],
                is_deleted=False
            )
        
        # Get the loan's disbursement date as a date object
        loan_disbursement_date = loan.disbursement_date.date() if hasattr(loan.disbursement_date, 'date') else loan.disbursement_date
        
        # Property: All returned loans must have disbursement_date within the filter range
        for loan_obj in loans:
            loan_date = loan_obj.disbursement_date.date() if hasattr(loan_obj.disbursement_date, 'date') else loan_obj.disbursement_date
            
            if filter_period == 'today':
                assert loan_date == today, \
                    f"Loan {loan_obj.loan_number} disbursed on {loan_date} should match today {today}"
            elif filter_period == 'this_week':
                assert start_date <= loan_date <= end_date, \
                    f"Loan {loan_obj.loan_number} disbursed on {loan_date} should be within week range {start_date} to {end_date}"
            # For 'total', no date constraint
        
        # Property: If loan meets the date criteria, it should be included
        if filter_period == 'today':
            if loan_disbursement_date == today:
                assert loans.filter(id=loan.id).exists(), \
                    f"Loan disbursed today ({loan_disbursement_date}) should be included in 'today' filter"
            else:
                assert not loans.filter(id=loan.id).exists(), \
                    f"Loan disbursed on {loan_disbursement_date} should NOT be included in 'today' filter"
        
        elif filter_period == 'this_week':
            if start_date <= loan_disbursement_date <= end_date:
                assert loans.filter(id=loan.id).exists(), \
                    f"Loan disbursed on {loan_disbursement_date} should be included in 'this_week' filter ({start_date} to {end_date})"
            else:
                assert not loans.filter(id=loan.id).exists(), \
                    f"Loan disbursed on {loan_disbursement_date} should NOT be included in 'this_week' filter ({start_date} to {end_date})"
        
        elif filter_period == 'total':
            # All disbursed loans should be included regardless of date
            assert loans.filter(id=loan.id).exists(), \
                f"Loan should be included in 'total' filter regardless of disbursement date"


if __name__ == '__main__':
    import unittest
    
    # Run the tests
    suite = unittest.TestLoader().loadTestsFromTestCase(DisbursedLoansFilteringPropertiesTest)
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite)
    
    # Exit with appropriate code
    sys.exit(0 if result.wasSuccessful() else 1)

