"""
Property-based tests for missed payments service.

These tests verify the correctness properties for missed payment tracking and reporting.
"""

from hypothesis import given, strategies as st, settings, assume
from django.test import TestCase
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.missed_payments_service import MissedPaymentsService


# Custom strategies for generating test data
@st.composite
def client_with_loans_strategy(draw):
    """Generate a client with multiple loans, some with missed payments"""
    # Create client
    username = f"test_user_{uuid.uuid4().hex[:8]}"
    client = CustomUser.objects.create_user(
        username=username,
        email=f"{username}@example.com",
        phone_number=f"+2547{draw(st.integers(min_value=10000000, max_value=99999999))}",
        first_name='Test',
        last_name='User'
    )
    
    # Create loan product
    product = LoanProduct.objects.create(
        name=f'Test Product {uuid.uuid4().hex[:8]}',
        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']
    )
    
    return client, product


class TestMissedPaymentDetailRetrieval(TestCase):
    """
    Test Property 26: Missed payment detail retrieval
    
    Feature: reports-system-enhancement, Property 26: Missed payment detail retrieval
    Validates: Requirements 10.2
    
    For any client in the missed payments report, clicking "View All Missed Payments"
    should display all loans for that client where due_date < current_date AND
    outstanding_amount > 0.
    """
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_overdue_loans=st.integers(min_value=1, max_value=5),
        num_current_loans=st.integers(min_value=0, max_value=3),
        days_overdue=st.integers(min_value=1, max_value=90)
    )
    def test_missed_payment_detail_includes_all_overdue_loans(
        self, num_overdue_loans, num_current_loans, days_overdue
    ):
        """
        Property: For any client, get_missed_payments_for_client should return
        all loans where due_date < today AND outstanding_amount > 0.
        """
        # Create client and product
        username = f"test_user_{uuid.uuid4().hex[:8]}"
        client = CustomUser.objects.create_user(
            username=username,
            email=f"{username}@example.com",
            phone_number=f"+254700000000",
            first_name='Test',
            last_name='User'
        )
        
        product = LoanProduct.objects.create(
            name=f'Test Product {uuid.uuid4().hex[:8]}',
            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']
        )
        
        today = timezone.now().date()
        
        # Create overdue loans (should be included)
        overdue_loan_ids = []
        for i in range(num_overdue_loans):
            application = LoanApplication.objects.create(
                borrower=client,
                loan_product=product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            due_date = today - timedelta(days=days_overdue)
            loan = Loan.objects.create(
                application=application,
                borrower=client,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=due_date - timedelta(days=30),
                due_date=due_date,
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
            overdue_loan_ids.append(str(loan.id))
        
        # Create current loans (should NOT be included)
        for i in range(num_current_loans):
            application = LoanApplication.objects.create(
                borrower=client,
                loan_product=product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            due_date = today + timedelta(days=30)
            Loan.objects.create(
                application=application,
                borrower=client,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=today,
                due_date=due_date,
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
        
        # Get missed payments for client
        missed_payments = MissedPaymentsService.get_missed_payments_for_client(str(client.id))
        
        # Verify: Should return exactly the overdue loans
        self.assertEqual(len(missed_payments), num_overdue_loans)
        
        # Verify: All returned loans should be overdue
        for payment in missed_payments:
            self.assertLess(payment['due_date'], today)
            self.assertGreater(payment['amount_due'], 0)
        
        # Verify: All overdue loans should be in the results
        returned_loan_ids = [str(payment['loan'].id) for payment in missed_payments]
        for loan_id in overdue_loan_ids:
            self.assertIn(loan_id, returned_loan_ids)


class TestMissedPaymentDefinition(TestCase):
    """
    Test Property 27: Missed payment definition
    
    Feature: reports-system-enhancement, Property 27: Missed payment definition
    Validates: Requirements 10.4
    
    For any loan, it should be classified as having a missed payment if and only if
    due_date < current_date AND outstanding_amount > 0.
    """
    
    @settings(max_examples=50, deadline=None)
    @given(
        days_offset=st.integers(min_value=-90, max_value=90),
        has_outstanding=st.booleans()
    )
    def test_missed_payment_definition_correctness(self, days_offset, has_outstanding):
        """
        Property: A loan has a missed payment IFF due_date < today AND outstanding > 0.
        """
        # Create client and product
        username = f"test_user_{uuid.uuid4().hex[:8]}"
        client = CustomUser.objects.create_user(
            username=username,
            email=f"{username}@example.com",
            phone_number=f"+254700000000",
            first_name='Test',
            last_name='User'
        )
        
        product = LoanProduct.objects.create(
            name=f'Test Product {uuid.uuid4().hex[:8]}',
            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']
        )
        
        today = timezone.now().date()
        due_date = today + timedelta(days=days_offset)
        
        # Create loan application
        application = LoanApplication.objects.create(
            borrower=client,
            loan_product=product,
            requested_amount=Decimal('10000'),
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan with specified due date
        loan = Loan.objects.create(
            application=application,
            borrower=client,
            principal_amount=Decimal('10000'),
            interest_amount=Decimal('1000'),
            processing_fee=Decimal('500'),
            total_amount=Decimal('11500'),
            disbursement_date=due_date - timedelta(days=30),
            due_date=due_date,
            duration_days=30,
            status='active',
            is_deleted=False,
            is_rolled_over=False
        )
        
        # Set outstanding amount based on has_outstanding
        if not has_outstanding:
            # Simulate full payment by setting _amount_paid_cache
            loan._amount_paid_cache = loan.total_amount
            loan.save()
        
        # Get missed payments for client
        missed_payments = MissedPaymentsService.get_missed_payments_for_client(str(client.id))
        
        # Determine expected result
        is_overdue = due_date < today
        has_outstanding_balance = loan.outstanding_amount > 0
        should_be_missed = is_overdue and has_outstanding_balance
        
        # Verify: Loan should be in missed payments IFF it meets both conditions
        loan_in_results = any(str(p['loan'].id) == str(loan.id) for p in missed_payments)
        self.assertEqual(loan_in_results, should_be_missed)


class TestMissedPaymentsSortOrder(TestCase):
    """
    Test Property 28: Missed payments sort order
    
    Feature: reports-system-enhancement, Property 28: Missed payments sort order
    Validates: Requirements 10.5
    
    For any list of missed payments, the records should be ordered by due_date
    in descending order (most recent first).
    """
    
    @settings(max_examples=50, deadline=None)
    @given(
        num_loans=st.integers(min_value=2, max_value=10)
    )
    def test_missed_payments_sorted_by_due_date_descending(self, num_loans):
        """
        Property: For any client with multiple missed payments, the results
        should be sorted by due_date in descending order.
        """
        # Create client and product
        username = f"test_user_{uuid.uuid4().hex[:8]}"
        client = CustomUser.objects.create_user(
            username=username,
            email=f"{username}@example.com",
            phone_number=f"+254700000000",
            first_name='Test',
            last_name='User'
        )
        
        product = LoanProduct.objects.create(
            name=f'Test Product {uuid.uuid4().hex[:8]}',
            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']
        )
        
        today = timezone.now().date()
        
        # Create loans with different due dates (all overdue)
        due_dates = []
        for i in range(num_loans):
            days_overdue = (i + 1) * 10  # 10, 20, 30, ... days overdue
            due_date = today - timedelta(days=days_overdue)
            due_dates.append(due_date)
            
            application = LoanApplication.objects.create(
                borrower=client,
                loan_product=product,
                requested_amount=Decimal('10000'),
                requested_duration=30,
                purpose='Test',
                status='approved'
            )
            
            Loan.objects.create(
                application=application,
                borrower=client,
                principal_amount=Decimal('10000'),
                interest_amount=Decimal('1000'),
                processing_fee=Decimal('500'),
                total_amount=Decimal('11500'),
                disbursement_date=due_date - timedelta(days=30),
                due_date=due_date,
                duration_days=30,
                status='active',
                is_deleted=False,
                is_rolled_over=False
            )
        
        # Get missed payments for client
        missed_payments = MissedPaymentsService.get_missed_payments_for_client(str(client.id))
        
        # Verify: Should return all loans
        self.assertEqual(len(missed_payments), num_loans)
        
        # Verify: Results should be sorted by due_date descending (most recent first)
        for i in range(len(missed_payments) - 1):
            current_due_date = missed_payments[i]['due_date']
            next_due_date = missed_payments[i + 1]['due_date']
            self.assertGreaterEqual(current_due_date, next_due_date,
                f"Missed payments not sorted correctly: {current_due_date} should be >= {next_due_date}")

