"""
Unit tests for loans app models

This module tests all models in the loans app:
- LoanProduct
- LoanApplication
- Loan
- Repayment
- MpesaTransaction
- PenaltyCharge
- Guarantor
- Collateral
- LoanComment
- RolloverRequest
"""

import pytest
from decimal import Decimal
from datetime import timedelta
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.contrib.auth import get_user_model
from loans.models import (
    LoanProduct, LoanApplication, Loan, Repayment,
    MpesaTransaction, PenaltyCharge
)
from users.models import Branch

User = get_user_model()


@pytest.mark.django_db
class TestLoanProduct:
    """Test cases for LoanProduct model"""
    
    @pytest.fixture
    def branch(self):
        """Create a test branch"""
        return Branch.objects.create(
            name='Main Branch',
            code='MAIN',
            is_main_branch=True
        )
    
    @pytest.fixture
    def biashara_product(self):
        """Create a Biashara loan product"""
        return LoanProduct.objects.create(
            name='Biashara Loan',
            product_type='biashara',
            description='Business loan for entrepreneurs',
            gl_code='11002',
            grazuri_account_type='B',
            min_amount=Decimal('10000.00'),
            max_amount=Decimal('500000.00'),
            interest_rate=Decimal('18.00'),
            processing_fee=Decimal('5.00'),
            late_payment_penalty=Decimal('5.00'),
            duration_months=6,
            min_duration=30,
            max_duration=180,
            available_repayment_methods=['monthly', 'weekly', 'daily'],
            is_active=True
        )
    
    @pytest.fixture
    def logbook_product(self):
        """Create a Log Book loan product"""
        return LoanProduct.objects.create(
            name='Log Book Loan',
            product_type='logbook',
            description='Secured loan against vehicle logbook',
            gl_code='11003',
            grazuri_account_type='P',
            min_amount=Decimal('20000.00'),
            max_amount=Decimal('1000000.00'),
            interest_rate=Decimal('15.00'),
            processing_fee=Decimal('3.00'),
            late_payment_penalty=Decimal('5.00'),
            duration_months=12,
            min_duration=30,
            max_duration=365,
            available_repayment_methods=['monthly'],
            requires_collateral=True,
            is_active=True
        )
    
    def test_loan_product_creation(self, biashara_product):
        """Test loan product is created successfully"""
        assert biashara_product.id is not None
        assert biashara_product.name == 'Biashara Loan'
        assert biashara_product.product_type == 'biashara'
        assert biashara_product.interest_rate == Decimal('18.00')
    
    def test_loan_product_str_representation(self, biashara_product):
        """Test string representation of loan product"""
        expected = 'Biashara Loan (Business (Biashara))'
        assert str(biashara_product) == expected
    
    def test_calculate_interest_grazuri_product(self, biashara_product):
        """Test interest calculation for Grazuri products (flat rate)"""
        principal = Decimal('100000.00')
        # For Grazuri products, interest is flat on principal
        interest = biashara_product.calculate_interest(principal, 6)
        expected = principal * Decimal('0.18')  # 18% flat
        assert interest == expected
    
    def test_calculate_interest_from_days_grazuri(self, biashara_product):
        """Test interest calculation from days for Grazuri products"""
        principal = Decimal('50000.00')
        duration_days = 90
        # For Grazuri products, duration doesn't affect interest
        interest = biashara_product.calculate_interest_from_days(principal, duration_days)
        expected = principal * Decimal('0.18')
        assert interest == expected
    
    def test_calculate_processing_fee_grazuri(self, biashara_product):
        """Test processing fee calculation for Grazuri products (one-time)"""
        principal = Decimal('100000.00')
        # For Grazuri products, processing fee is one-time
        fee = biashara_product.calculate_processing_fee(principal, 6)
        expected = principal * Decimal('0.05')  # 5% one-time
        assert fee == expected
    
    def test_calculate_total_loan_amount_grazuri(self, biashara_product):
        """Test total loan amount calculation for Grazuri products"""
        principal = Decimal('100000.00')
        duration_days = 180
        total = biashara_product.calculate_total_loan_amount(principal, duration_days)
        # Total = principal + flat interest + one-time fee
        expected = principal + (principal * Decimal('0.18')) + (principal * Decimal('0.05'))
        assert total == expected
    
    def test_validate_amount_grazuri_no_limits(self, biashara_product):
        """Test that Grazuri products don't enforce amount limits"""
        # Any positive amount should be valid for Grazuri products
        assert biashara_product.validate_amount(Decimal('5000.00')) is True
        assert biashara_product.validate_amount(Decimal('1000000.00')) is True
    
    def test_validate_amount_zero_fails(self, biashara_product):
        """Test that zero amount fails validation"""
        with pytest.raises(ValueError, match="Amount must be greater than zero"):
            biashara_product.validate_amount(Decimal('0.00'))
    
    def test_validate_duration_grazuri_no_limits(self, biashara_product):
        """Test that Grazuri products don't enforce duration limits"""
        # Any positive duration should be valid for Grazuri products
        assert biashara_product.validate_duration(10) is True
        assert biashara_product.validate_duration(500) is True
    
    def test_validate_duration_zero_fails(self, biashara_product):
        """Test that zero duration fails validation"""
        with pytest.raises(ValueError, match="Duration must be at least 1 day"):
            biashara_product.validate_duration(0)
    
    def test_validate_repayment_method(self, biashara_product):
        """Test repayment method validation"""
        assert biashara_product.validate_repayment_method('monthly') is True
        assert biashara_product.validate_repayment_method('weekly') is True
        assert biashara_product.validate_repayment_method('daily') is True
    
    def test_validate_repayment_method_invalid(self, biashara_product):
        """Test invalid repayment method fails"""
        with pytest.raises(ValueError):
            biashara_product.validate_repayment_method('invalid')
    
    def test_is_grazuri_product(self, biashara_product, logbook_product):
        """Test identification of Grazuri products"""
        assert biashara_product.is_grazuri_product() is True
        assert logbook_product.is_grazuri_product() is True
    
    def test_is_biashara_product(self, biashara_product, logbook_product):
        """Test identification of Biashara products"""
        assert biashara_product.is_biashara_product() is True
        assert logbook_product.is_biashara_product() is False
    
    def test_is_logbook_product(self, biashara_product, logbook_product):
        """Test identification of Log Book products"""
        assert biashara_product.is_logbook_product() is False
        assert logbook_product.is_logbook_product() is True
    
    def test_to_dict_method(self, biashara_product):
        """Test to_dict method returns correct structure"""
        product_dict = biashara_product.to_dict()
        assert 'id' in product_dict
        assert 'name' in product_dict
        assert product_dict['product_type'] == 'biashara'
        assert product_dict['is_grazuri_product'] is True
        assert float(product_dict['interest_rate']) == 18.00


@pytest.mark.django_db
class TestLoanApplication:
    """Test cases for LoanApplication model"""
    
    @pytest.fixture
    def branch(self):
        """Create a test branch"""
        return Branch.objects.create(
            name='Main Branch',
            code='MAIN',
            is_main_branch=True
        )
    
    @pytest.fixture
    def loan_product(self):
        """Create a test loan product"""
        return LoanProduct.objects.create(
            name='Biashara Loan',
            product_type='biashara',
            description='Business loan',
            min_amount=Decimal('10000.00'),
            max_amount=Decimal('500000.00'),
            interest_rate=Decimal('18.00'),
            processing_fee=Decimal('5.00'),
            late_payment_penalty=Decimal('5.00'),
            duration_months=6,
            min_duration=30,
            max_duration=180,
            available_repayment_methods=['monthly', 'weekly', 'daily']
        )
    
    @pytest.fixture
    def borrower(self, branch):
        """Create a test borrower"""
        return User.objects.create_user(
            username='testborrower',
            phone_number='+254700000001',
            role='borrower',
            status='active',
            branch=branch
        )
    
    @pytest.fixture
    def loan_officer(self, branch):
        """Create a test loan officer"""
        return User.objects.create_user(
            username='testofficer',
            phone_number='+254700000002',
            role='loan_officer',
            is_staff=True,
            branch=branch
        )
    
    @pytest.fixture
    def loan_application(self, borrower, loan_product):
        """Create a test loan application"""
        return LoanApplication.objects.create(
            borrower=borrower,
            loan_product=loan_product,
            requested_amount=Decimal('100000.00'),
            requested_duration=90,
            purpose='Business expansion',
            repayment_method='monthly'
        )
    
    def test_loan_application_creation(self, loan_application):
        """Test loan application is created successfully"""
        assert loan_application.id is not None
        assert loan_application.application_number is not None
        assert loan_application.status == 'pending'
    
    def test_application_number_generation(self, loan_application):
        """Test application number is auto-generated"""
        assert loan_application.application_number.startswith('APP-')
    
    def test_str_representation(self, loan_application):
        """Test string representation"""
        expected = f"Application {loan_application.application_number} - {loan_application.borrower.get_full_name()}"
        assert str(loan_application) == expected
    
    def test_interest_amount_auto_calculated(self, loan_application):
        """Test interest amount is calculated on save"""
        assert loan_application.interest_amount is not None
        assert loan_application.interest_amount > 0
    
    def test_processing_fee_auto_calculated(self, loan_application):
        """Test processing fee is calculated on save"""
        assert loan_application.processing_fee_amount is not None
        assert loan_application.processing_fee_amount > 0
    
    def test_total_amount_auto_calculated(self, loan_application):
        """Test total amount is calculated on save"""
        assert loan_application.total_amount is not None
        expected = (loan_application.requested_amount +
                   loan_application.interest_amount +
                   loan_application.processing_fee_amount)
        assert loan_application.total_amount == expected
    
    def test_approve_creates_loan(self, loan_application, loan_officer):
        """Test approving application creates a loan"""
        loan = loan_application.approve(
            approved_by=loan_officer,
            notes="Approved for disbursement"
        )
        assert loan is not None
        assert loan_application.status == 'approved'
        assert loan.status == 'active'
        assert loan.borrower == loan_application.borrower
    
    def test_approve_blacklisted_borrower_fails(self, loan_application, loan_officer):
        """Test approving application for blacklisted borrower fails"""
        loan_application.borrower.status = 'blacklisted'
        loan_application.borrower.save()
        
        with pytest.raises(ValueError, match="Cannot approve loan application for blacklisted client"):
            loan_application.approve(approved_by=loan_officer)
    
    def test_reject_application(self, loan_application, loan_officer):
        """Test rejecting an application"""
        loan_application.reject(
            rejected_by=loan_officer,
            notes="Insufficient credit history"
        )
        assert loan_application.status == 'rejected'
        assert loan_application.reviewed_by == loan_officer
        assert loan_application.reviewed_at is not None
    
    def test_calculate_risk_score(self, loan_application):
        """Test risk score calculation"""
        score = loan_application.calculate_risk_score()
        assert score >= Decimal('0')
        assert score <= Decimal('100')


@pytest.mark.django_db
class TestLoan:
    """Test cases for Loan model"""
    
    @pytest.fixture
    def branch(self):
        """Create a test branch"""
        return Branch.objects.create(
            name='Main Branch',
            code='MAIN',
            is_main_branch=True
        )
    
    @pytest.fixture
    def loan_product(self):
        """Create a test loan product"""
        return LoanProduct.objects.create(
            name='Biashara Loan',
            product_type='biashara',
            description='Business loan',
            min_amount=Decimal('10000.00'),
            max_amount=Decimal('500000.00'),
            interest_rate=Decimal('18.00'),
            processing_fee=Decimal('5.00'),
            late_payment_penalty=Decimal('5.00'),
            duration_months=6,
            min_duration=30,
            max_duration=180,
            available_repayment_methods=['monthly']
        )
    
    @pytest.fixture
    def borrower(self, branch):
        """Create a test borrower"""
        return User.objects.create_user(
            username='testborrower',
            phone_number='+254700000001',
            role='borrower',
            status='active',
            branch=branch
        )
    
    @pytest.fixture
    def loan_officer(self, branch):
        """Create a test loan officer"""
        return User.objects.create_user(
            username='testofficer',
            phone_number='+254700000002',
            role='loan_officer',
            is_staff=True,
            branch=branch
        )
    
    @pytest.fixture
    def loan_application(self, borrower, loan_product):
        """Create and approve a loan application"""
        return LoanApplication.objects.create(
            borrower=borrower,
            loan_product=loan_product,
            requested_amount=Decimal('100000.00'),
            requested_duration=90,
            purpose='Business expansion',
            repayment_method='monthly',
            status='approved'
        )
    
    @pytest.fixture
    def loan(self, loan_application, borrower):
        """Create a test loan"""
        return Loan.objects.create(
            application=loan_application,
            borrower=borrower,
            principal_amount=Decimal('100000.00'),
            interest_amount=Decimal('18000.00'),
            processing_fee=Decimal('5000.00'),
            total_amount=Decimal('100000.00'),  # Under company model
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=90),
            duration_days=90,
            status='active'
        )
    
    def test_loan_creation(self, loan):
        """Test loan is created successfully"""
        assert loan.id is not None
        assert loan.loan_number is not None
        assert loan.status == 'active'
    
    def test_loan_number_generation(self, loan):
        """Test loan number is auto-generated"""
        assert loan.loan_number.startswith('LOAN-')
    
    def test_str_representation(self, loan):
        """Test string representation"""
        expected = f"Loan {loan.loan_number} - {loan.borrower.get_full_name()}"
        assert str(loan) == expected
    
    def test_calculated_total_amount(self, loan):
        """Test calculated_total_amount property"""
        # Under company model, total = principal
        assert loan.calculated_total_amount == loan.principal_amount
    
    def test_amount_paid_property(self, loan):
        """Test amount_paid property calculates from repayments"""
        # Create some repayments
        Repayment.objects.create(
            loan=loan,
            amount=Decimal('10000.00'),
            payment_date=timezone.now()
        )
        Repayment.objects.create(
            loan=loan,
            amount=Decimal('5000.00'),
            payment_date=timezone.now()
        )
        # Amount paid should sum repayments
        assert loan.amount_paid == Decimal('15000.00')
    
    def test_outstanding_amount_property(self, loan):
        """Test outstanding_amount property"""
        # Create a repayment
        Repayment.objects.create(
            loan=loan,
            amount=Decimal('10000.00'),
            payment_date=timezone.now()
        )
        # Outstanding = total - amount_paid
        expected = loan.total_amount - Decimal('10000.00')
        assert loan.outstanding_amount == expected
    
    def test_is_in_arrears_not_overdue(self, loan):
        """Test loan not in arrears when not overdue"""
        # Loan due in future
        assert loan.is_in_arrears() is False
    
    def test_is_in_arrears_overdue_with_balance(self, loan):
        """Test loan in arrears when overdue with balance"""
        # Set due date in past
        loan.due_date = timezone.now() - timedelta(days=10)
        loan.save()
        assert loan.is_in_arrears() is True
    
    def test_is_in_arrears_overdue_but_paid(self, loan):
        """Test loan not in arrears when paid even if overdue"""
        # Set due date in past
        loan.due_date = timezone.now() - timedelta(days=10)
        loan.save()
        # Pay full amount
        Repayment.objects.create(
            loan=loan,
            amount=loan.total_amount,
            payment_date=timezone.now()
        )
        assert loan.is_in_arrears() is False
    
    def test_soft_delete(self, loan, loan_officer):
        """Test soft delete functionality"""
        loan.soft_delete(deleted_by=loan_officer)
        assert loan.is_deleted is True
        assert loan.deleted_at is not None
        assert loan.deleted_by == loan_officer
    
    def test_restore_soft_deleted_loan(self, loan, loan_officer):
        """Test restoring a soft-deleted loan"""
        loan.soft_delete(deleted_by=loan_officer)
        loan.restore()
        assert loan.is_deleted is False
        assert loan.deleted_at is None
        assert loan.deleted_by is None
    
    def test_validate_status_transition_valid(self, loan):
        """Test valid status transitions"""
        # Active to paid is valid
        assert loan.validate_status_transition('paid') is True
        # Active to defaulted is valid
        assert loan.validate_status_transition('defaulted') is True
    
    def test_validate_status_transition_invalid(self, loan):
        """Test invalid status transitions"""
        loan.status = 'rolled_over'
        loan.save()
        # Rolled over cannot transition to paid
        with pytest.raises(ValueError, match="Invalid status transition"):
            loan.validate_status_transition('paid')


@pytest.mark.django_db
class TestRepayment:
    """Test cases for Repayment model"""
    
    @pytest.fixture
    def branch(self):
        """Create a test branch"""
        return Branch.objects.create(
            name='Main Branch',
            code='MAIN',
            is_main_branch=True
        )
    
    @pytest.fixture
    def loan(self, branch):
        """Create a test loan"""
        borrower = User.objects.create_user(
            username='testborrower',
            phone_number='+254700000001',
            role='borrower',
            branch=branch
        )
        product = LoanProduct.objects.create(
            name='Test Product',
            product_type='biashara',
            description='Test',
            min_amount=Decimal('10000.00'),
            max_amount=Decimal('500000.00'),
            interest_rate=Decimal('18.00'),
            processing_fee=Decimal('5.00'),
            duration_months=6,
            min_duration=30,
            max_duration=180,
            available_repayment_methods=['monthly']
        )
        application = LoanApplication.objects.create(
            borrower=borrower,
            loan_product=product,
            requested_amount=Decimal('100000.00'),
            requested_duration=90,
            purpose='Test',
            status='approved'
        )
        return Loan.objects.create(
            application=application,
            borrower=borrower,
            principal_amount=Decimal('100000.00'),
            interest_amount=Decimal('18000.00'),
            processing_fee=Decimal('5000.00'),
            total_amount=Decimal('100000.00'),
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=90),
            duration_days=90,
            status='active'
        )
    
    def test_repayment_creation(self, loan):
        """Test repayment is created successfully"""
        repayment = Repayment.objects.create(
            loan=loan,
            amount=Decimal('10000.00'),
            payment_date=timezone.now(),
            payment_method='mpesa'
        )
        assert repayment.id is not None
        assert repayment.amount == Decimal('10000.00')
    
    def test_repayment_updates_loan_cache(self, loan):
        """Test repayment updates loan's amount paid cache"""
        initial_cache = loan._amount_paid_cache
        Repayment.objects.create(
            loan=loan,
            amount=Decimal('10000.00'),
            payment_date=timezone.now()
        )
        loan.refresh_from_db()
        # Cache should be updated
        assert loan._amount_paid_cache > initial_cache


@pytest.mark.django_db
class TestPenaltyCharge:
    """Test cases for PenaltyCharge model"""
    
    @pytest.fixture
    def branch(self):
        """Create a test branch"""
        return Branch.objects.create(
            name='Main Branch',
            code='MAIN',
            is_main_branch=True
        )
    
    @pytest.fixture
    def loan(self, branch):
        """Create a test loan"""
        borrower = User.objects.create_user(
            username='testborrower',
            phone_number='+254700000001',
            role='borrower',
            branch=branch
        )
        product = LoanProduct.objects.create(
            name='Test Product',
            product_type='biashara',
            description='Test',
            min_amount=Decimal('10000.00'),
            max_amount=Decimal('500000.00'),
            interest_rate=Decimal('18.00'),
            processing_fee=Decimal('5.00'),
            late_payment_penalty=Decimal('5.00'),
            duration_months=6,
            min_duration=30,
            max_duration=180,
            available_repayment_methods=['monthly']
        )
        application = LoanApplication.objects.create(
            borrower=borrower,
            loan_product=product,
            requested_amount=Decimal('100000.00'),
            requested_duration=90,
            purpose='Test',
            status='approved'
        )
        return Loan.objects.create(
            application=application,
            borrower=borrower,
            principal_amount=Decimal('100000.00'),
            interest_amount=Decimal('18000.00'),
            processing_fee=Decimal('5000.00'),
            total_amount=Decimal('100000.00'),
            disbursement_date=timezone.now() - timedelta(days=100),
            due_date=timezone.now() - timedelta(days=10),  # Overdue
            duration_days=90,
            status='active'
        )
    
    def test_penalty_charge_creation(self, loan):
        """Test penalty charge is created successfully"""
        penalty = PenaltyCharge.objects.create(
            loan=loan,
            amount=Decimal('500.00'),
            reason='Late payment penalty',
            charge_date=timezone.now()
        )
        assert penalty.id is not None
        assert penalty.amount == Decimal('500.00')
    
    def test_penalty_affects_outstanding_amount(self, loan):
        """Test penalty charges increase outstanding amount"""
        initial_outstanding = loan.outstanding_amount
        PenaltyCharge.objects.create(
            loan=loan,
            amount=Decimal('500.00'),
            reason='Late payment penalty',
            charge_date=timezone.now()
        )
        # Outstanding should increase by penalty amount
        assert loan.outstanding_amount == initial_outstanding + Decimal('500.00')
