"""
Property-based tests for penalty addition functionality.

These tests use Hypothesis to verify universal properties that should hold
across all valid inputs, ensuring correctness of penalty logic.

Feature: comprehensive-reports-and-fixes
"""

from hypothesis import given, strategies as st, settings, assume
from hypothesis.extra.django import TestCase
from django.utils import timezone
from django.test import Client
from datetime import date, timedelta
from decimal import Decimal
import uuid

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


# Custom strategies for generating test data
@st.composite
def decimal_amount_strategy(draw, min_value=1, max_value=10000):
    """Generate a valid decimal amount for penalty calculations"""
    return draw(st.decimals(
        min_value=Decimal(str(min_value)),
        max_value=Decimal(str(max_value)),
        places=2,
        allow_nan=False,
        allow_infinity=False
    ))


@st.composite
def penalty_date_strategy(draw):
    """Generate a valid penalty date (not in the future)"""
    days_ago = draw(st.integers(min_value=0, max_value=365))
    return date.today() - timedelta(days=days_ago)


@st.composite
def invalid_penalty_amount_strategy(draw):
    """Generate invalid penalty amounts (zero or negative)"""
    return draw(st.one_of(
        st.just(Decimal('0')),
        st.decimals(
            min_value=Decimal('-1000'),
            max_value=Decimal('-0.01'),
            places=2,
            allow_nan=False,
            allow_infinity=False
        )
    ))


@st.composite
def future_date_strategy(draw):
    """Generate a date in the future"""
    days_ahead = draw(st.integers(min_value=1, max_value=365))
    return date.today() + timedelta(days=days_ahead)


class TestPenaltyValidation(TestCase):
    """
    Test Property 14: Penalty Validation
    
    Feature: comprehensive-reports-and-fixes, Property 14: Penalty Validation
    Validates: Requirements 4.4, 4.5
    
    For any penalty submission, the system should reject it if 
    amount <= 0 or penalty_date > current_date.
    """
    
    def setUp(self):
        """Set up test data"""
        # Create test user (admin)
        self.admin_user = CustomUser.objects.create_user(
            username=f'admin_penalty_{uuid.uuid4().hex[:6]}',
            email='admin_penalty@example.com',
            phone_number=f'+2547000{uuid.uuid4().hex[:5]}',
            first_name='Admin',
            last_name='User',
            is_staff=True,
            is_superuser=True
        )
        
        # Create branch
        self.branch = Branch.objects.create(
            name='Test Branch Penalty',
            code=f'TBP{uuid.uuid4().hex[:6]}'
        )
        
        # Create loan product
        self.loan_product = LoanProduct.objects.create(
            name='Test Product Penalty',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            late_payment_penalty=Decimal('2.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        # Create borrower
        self.borrower = CustomUser.objects.create_user(
            username=f'borrower_penalty_{uuid.uuid4().hex[:6]}',
            email='borrower_penalty@example.com',
            phone_number=f'+2547001{uuid.uuid4().hex[:5]}',
            first_name='Borrower',
            last_name='User'
        )
        
        # Create loan application
        self.application = LoanApplication.objects.create(
            application_number=f'APP-PENALTY-{uuid.uuid4().hex[:6]}',
            borrower=self.borrower,
            loan_product=self.loan_product,
            requested_amount=Decimal('10000'),
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan
        self.loan = Loan.objects.create(
            loan_number=f'LOAN-PENALTY-{uuid.uuid4().hex[:6]}',
            borrower=self.borrower,
            application=self.application,
            principal_amount=Decimal('10000'),
            interest_amount=Decimal('1000'),
            processing_fee=Decimal('500'),
            total_amount=Decimal('11500'),
            disbursement_date=timezone.now() - timedelta(days=30),
            due_date=timezone.now() - timedelta(days=5),
            duration_days=25,
            status='active'
        )
        
        self.client = Client()
        self.client.force_login(self.admin_user)
    
    @given(amount=invalid_penalty_amount_strategy())
    @settings(max_examples=20, deadline=None)
    def test_invalid_penalty_amount_rejected(self, amount):
        """Test that penalties with zero or negative amounts are rejected"""
        response = self.client.post(
            f'/loans/{self.loan.id}/penalty/apply/',
            {
                'penalty_amount': str(amount),
                'penalty_date': date.today().isoformat(),
                'reason': 'Test penalty'
            },
            HTTP_X_REQUESTED_WITH='XMLHttpRequest'
        )
        
        # Should return error response
        self.assertEqual(response.status_code, 400)
        data = response.json()
        self.assertFalse(data['success'])
        self.assertIn('error', data)
    
    @given(future_date=future_date_strategy())
    @settings(max_examples=20, deadline=None)
    def test_future_penalty_date_rejected(self, future_date):
        """Test that penalties with future dates are rejected"""
        response = self.client.post(
            f'/loans/{self.loan.id}/penalty/apply/',
            {
                'penalty_amount': '100.00',
                'penalty_date': future_date.isoformat(),
                'reason': 'Test penalty'
            },
            HTTP_X_REQUESTED_WITH='XMLHttpRequest'
        )
        
        # Should return error response
        self.assertEqual(response.status_code, 400)
        data = response.json()
        self.assertFalse(data['success'])
        self.assertIn('future', data['error'].lower())


class TestPenaltyPersistence(TestCase):
    """
    Test Property 15: Penalty Persistence
    
    Feature: comprehensive-reports-and-fixes, Property 15: Penalty Persistence
    Validates: Requirements 4.3
    
    For any valid penalty submitted by an admin user, the penalty should be 
    saved with amount, penalty_date, associated loan, and creation timestamp.
    """
    
    def setUp(self):
        """Set up test data"""
        # Create test user (admin)
        self.admin_user = CustomUser.objects.create_user(
            username=f'admin_persist_{uuid.uuid4().hex[:6]}',
            email='admin_persist@example.com',
            phone_number=f'+2547002{uuid.uuid4().hex[:5]}',
            first_name='Admin',
            last_name='Persist',
            is_staff=True,
            is_superuser=True
        )
        
        # Create branch
        self.branch = Branch.objects.create(
            name='Test Branch Persist',
            code=f'TBP{uuid.uuid4().hex[:6]}'
        )
        
        # Create loan product
        self.loan_product = LoanProduct.objects.create(
            name='Test Product Persist',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            late_payment_penalty=Decimal('2.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        # Create borrower
        self.borrower = CustomUser.objects.create_user(
            username=f'borrower_persist_{uuid.uuid4().hex[:6]}',
            email='borrower_persist@example.com',
            phone_number=f'+2547003{uuid.uuid4().hex[:5]}',
            first_name='Borrower',
            last_name='Persist'
        )
        
        # Create loan application
        self.application = LoanApplication.objects.create(
            application_number=f'APP-PERSIST-{uuid.uuid4().hex[:6]}',
            borrower=self.borrower,
            loan_product=self.loan_product,
            requested_amount=Decimal('10000'),
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan
        self.loan = Loan.objects.create(
            loan_number=f'LOAN-PERSIST-{uuid.uuid4().hex[:6]}',
            borrower=self.borrower,
            application=self.application,
            principal_amount=Decimal('10000'),
            interest_amount=Decimal('1000'),
            processing_fee=Decimal('500'),
            total_amount=Decimal('11500'),
            disbursement_date=timezone.now() - timedelta(days=30),
            due_date=timezone.now() - timedelta(days=5),
            duration_days=25,
            status='active'
        )
        
        self.client = Client()
        self.client.force_login(self.admin_user)
    
    @given(
        amount=decimal_amount_strategy(min_value=1, max_value=5000),
        penalty_date=penalty_date_strategy()
    )
    @settings(max_examples=20, deadline=None)
    def test_valid_penalty_persisted_with_all_fields(self, amount, penalty_date):
        """Test that valid penalties are saved with all required fields"""
        initial_count = PenaltyCharge.objects.filter(loan=self.loan).count()
        
        response = self.client.post(
            f'/loans/{self.loan.id}/penalty/apply/',
            {
                'penalty_amount': str(amount),
                'penalty_date': penalty_date.isoformat(),
                'reason': f'Test penalty for {amount}'
            },
            HTTP_X_REQUESTED_WITH='XMLHttpRequest'
        )
        
        # Should succeed
        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertTrue(data['success'])
        
        # Verify penalty was created
        new_count = PenaltyCharge.objects.filter(loan=self.loan).count()
        self.assertEqual(new_count, initial_count + 1)
        
        # Verify all required fields are present
        penalty = PenaltyCharge.objects.filter(loan=self.loan).latest('created_at')
        self.assertEqual(penalty.amount, amount)
        self.assertEqual(penalty.penalty_date, penalty_date)
        self.assertEqual(penalty.loan, self.loan)
        self.assertEqual(penalty.applied_by, self.admin_user)
        self.assertIsNotNone(penalty.created_at)
        self.assertIsNotNone(penalty.applied_date)
        self.assertFalse(penalty.is_automatic)


class TestPenaltyImpactOnOutstanding(TestCase):
    """
    Test Property 16: Penalty Impact on Outstanding Amount
    
    Feature: comprehensive-reports-and-fixes, Property 16: Penalty Impact on Outstanding Amount
    Validates: Requirements 4.8, 4.9
    
    For any loan, when a penalty is added, the outstanding_amount should 
    increase by the penalty amount.
    """
    
    def setUp(self):
        """Set up test data"""
        # Create test user (admin)
        self.admin_user = CustomUser.objects.create_user(
            username=f'admin_impact_{uuid.uuid4().hex[:6]}',
            email='admin_impact@example.com',
            phone_number=f'+2547004{uuid.uuid4().hex[:5]}',
            first_name='Admin',
            last_name='Impact',
            is_staff=True,
            is_superuser=True
        )
        
        # Create branch
        self.branch = Branch.objects.create(
            name='Test Branch Impact',
            code=f'TBI{uuid.uuid4().hex[:6]}'
        )
        
        # Create loan product
        self.loan_product = LoanProduct.objects.create(
            name='Test Product Impact',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            late_payment_penalty=Decimal('2.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        # Create borrower
        self.borrower = CustomUser.objects.create_user(
            username=f'borrower_impact_{uuid.uuid4().hex[:6]}',
            email='borrower_impact@example.com',
            phone_number=f'+2547005{uuid.uuid4().hex[:5]}',
            first_name='Borrower',
            last_name='Impact'
        )
        
        # Create loan application
        self.application = LoanApplication.objects.create(
            application_number=f'APP-IMPACT-{uuid.uuid4().hex[:6]}',
            borrower=self.borrower,
            loan_product=self.loan_product,
            requested_amount=Decimal('10000'),
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan
        self.loan = Loan.objects.create(
            loan_number=f'LOAN-IMPACT-{uuid.uuid4().hex[:6]}',
            borrower=self.borrower,
            application=self.application,
            principal_amount=Decimal('10000'),
            interest_amount=Decimal('1000'),
            processing_fee=Decimal('500'),
            total_amount=Decimal('11500'),
            disbursement_date=timezone.now() - timedelta(days=30),
            due_date=timezone.now() - timedelta(days=5),
            duration_days=25,
            status='active'
        )
    
    @given(penalty_amount=decimal_amount_strategy(min_value=1, max_value=5000))
    @settings(max_examples=20, deadline=None)
    def test_penalty_increases_outstanding_amount(self, penalty_amount):
        """Test that adding a penalty increases outstanding amount by penalty amount"""
        # Get initial outstanding amount
        initial_outstanding = self.loan.outstanding_amount
        
        # Add penalty
        PenaltyCharge.objects.create(
            loan=self.loan,
            amount=penalty_amount,
            penalty_rate=Decimal('2.00'),
            days_overdue=5,
            outstanding_amount=initial_outstanding,
            is_automatic=False,
            applied_by=self.admin_user,
            reason='Test penalty',
            penalty_date=date.today()
        )
        
        # Refresh loan from database
        self.loan.refresh_from_db()
        
        # Get new outstanding amount
        new_outstanding = self.loan.outstanding_amount
        
        # Verify outstanding increased by penalty amount
        expected_outstanding = initial_outstanding + penalty_amount
        self.assertEqual(new_outstanding, expected_outstanding)


class TestMultiplePenaltiesSupport(TestCase):
    """
    Test Property 17: Multiple Penalties Support
    
    Feature: comprehensive-reports-and-fixes, Property 17: Multiple Penalties Support
    Validates: Requirements 4.12
    
    For any loan, the system should allow multiple penalties to be added, 
    and total_penalties should equal the sum of all penalty amounts for that loan.
    """
    
    def setUp(self):
        """Set up test data"""
        # Create test user (admin)
        self.admin_user = CustomUser.objects.create_user(
            username=f'admin_multiple_{uuid.uuid4().hex[:6]}',
            email='admin_multiple@example.com',
            phone_number=f'+2547006{uuid.uuid4().hex[:5]}',
            first_name='Admin',
            last_name='Multiple',
            is_staff=True,
            is_superuser=True
        )
        
        # Create branch
        self.branch = Branch.objects.create(
            name='Test Branch Multiple',
            code=f'TBM{uuid.uuid4().hex[:6]}'
        )
        
        # Create loan product
        self.loan_product = LoanProduct.objects.create(
            name='Test Product Multiple',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000'),
            max_amount=Decimal('100000'),
            interest_rate=Decimal('10.0'),
            processing_fee=Decimal('5.0'),
            late_payment_penalty=Decimal('2.0'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        # Create borrower
        self.borrower = CustomUser.objects.create_user(
            username=f'borrower_multiple_{uuid.uuid4().hex[:6]}',
            email='borrower_multiple@example.com',
            phone_number=f'+2547007{uuid.uuid4().hex[:5]}',
            first_name='Borrower',
            last_name='Multiple'
        )
        
        # Create loan application
        self.application = LoanApplication.objects.create(
            application_number=f'APP-MULTIPLE-{uuid.uuid4().hex[:6]}',
            borrower=self.borrower,
            loan_product=self.loan_product,
            requested_amount=Decimal('10000'),
            requested_duration=30,
            purpose='Test',
            status='approved'
        )
        
        # Create loan
        self.loan = Loan.objects.create(
            loan_number=f'LOAN-MULTIPLE-{uuid.uuid4().hex[:6]}',
            borrower=self.borrower,
            application=self.application,
            principal_amount=Decimal('10000'),
            interest_amount=Decimal('1000'),
            processing_fee=Decimal('500'),
            total_amount=Decimal('11500'),
            disbursement_date=timezone.now() - timedelta(days=30),
            due_date=timezone.now() - timedelta(days=5),
            duration_days=25,
            status='active'
        )
    
    @given(penalty_amounts=st.lists(
        decimal_amount_strategy(min_value=1, max_value=1000),
        min_size=1,
        max_size=5
    ))
    @settings(max_examples=20, deadline=None)
    def test_multiple_penalties_sum_correctly(self, penalty_amounts):
        """Test that multiple penalties can be added and sum correctly"""
        # Add multiple penalties
        for i, amount in enumerate(penalty_amounts):
            PenaltyCharge.objects.create(
                loan=self.loan,
                amount=amount,
                penalty_rate=Decimal('2.00'),
                days_overdue=5 + i,
                outstanding_amount=self.loan.outstanding_amount,
                is_automatic=False,
                applied_by=self.admin_user,
                reason=f'Test penalty {i+1}',
                penalty_date=date.today() - timedelta(days=i)
            )
        
        # Refresh loan from database
        self.loan.refresh_from_db()
        
        # Calculate expected total
        expected_total = sum(penalty_amounts)
        
        # Verify total_penalties equals sum of all penalties
        actual_total = self.loan.total_penalties
        self.assertEqual(actual_total, expected_total)
        
        # Verify count of penalties
        penalty_count = PenaltyCharge.objects.filter(loan=self.loan).count()
        self.assertEqual(penalty_count, len(penalty_amounts))

