"""
Unit tests for payments app models

This module tests all models in the payments app:
- MpesaConfiguration
- MpesaCallback
- MpesaAccessToken
- PaymentAllocation
- UnconfirmedPayment
"""

import pytest
from decimal import Decimal
from datetime import timedelta
from django.utils import timezone
from django.contrib.auth import get_user_model
from payments.models import (
    MpesaConfiguration, MpesaCallback, MpesaAccessToken,
    PaymentAllocation, UnconfirmedPayment
)
from users.models import Branch
from loans.models import LoanProduct, LoanApplication, Loan

User = get_user_model()


@pytest.mark.django_db
class TestMpesaConfiguration:
    """Test cases for MpesaConfiguration model"""
    
    def test_mpesa_configuration_creation(self):
        """Test M-Pesa configuration is created successfully"""
        config = MpesaConfiguration.objects.create(
            environment='sandbox',
            consumer_key='test_consumer_key',
            consumer_secret='test_consumer_secret',
            business_short_code='123456',
            passkey='test_passkey',
            validation_url='https://example.com/validate',
            confirmation_url='https://example.com/confirm',
            is_active=True
        )
        assert config.id is not None
        assert config.environment == 'sandbox'
        assert config.business_short_code == '123456'
    
    def test_mpesa_configuration_str_representation(self):
        """Test string representation"""
        config = MpesaConfiguration.objects.create(
            environment='production',
            consumer_key='test_key',
            consumer_secret='test_secret',
            business_short_code='789012',
            validation_url='https://example.com/validate',
            confirmation_url='https://example.com/confirm'
        )
        expected = 'M-Pesa Config (production) - 789012'
        assert str(config) == expected
    
    def test_get_active_config(self):
        """Test get_active_config class method"""
        # Create inactive config
        MpesaConfiguration.objects.create(
            environment='sandbox',
            consumer_key='inactive_key',
            consumer_secret='inactive_secret',
            business_short_code='000000',
            validation_url='https://example.com/validate',
            confirmation_url='https://example.com/confirm',
            is_active=False
        )
        
        # Create active config
        active_config = MpesaConfiguration.objects.create(
            environment='production',
            consumer_key='active_key',
            consumer_secret='active_secret',
            business_short_code='111111',
            validation_url='https://example.com/validate',
            confirmation_url='https://example.com/confirm',
            is_active=True
        )
        
        # Get active config should return the active one
        config = MpesaConfiguration.get_active_config()
        assert config.id == active_config.id
        assert config.business_short_code == '111111'


@pytest.mark.django_db
class TestMpesaCallback:
    """Test cases for MpesaCallback model"""
    
    def test_mpesa_callback_creation(self):
        """Test M-Pesa callback is created successfully"""
        callback = MpesaCallback.objects.create(
            callback_type='validation',
            raw_data={'test': 'data'},
            ip_address='127.0.0.1',
            user_agent='Mozilla/5.0'
        )
        assert callback.id is not None
        assert callback.callback_type == 'validation'
        assert callback.processed is False
    
    def test_mpesa_callback_str_representation(self):
        """Test string representation"""
        callback = MpesaCallback.objects.create(
            callback_type='confirmation',
            raw_data={'test': 'data'}
        )
        expected_start = 'M-Pesa confirmation callback -'
        assert str(callback).startswith(expected_start)


@pytest.mark.django_db
class TestMpesaAccessToken:
    """Test cases for MpesaAccessToken model"""
    
    @pytest.fixture
    def mpesa_config(self):
        """Create M-Pesa configuration"""
        return MpesaConfiguration.objects.create(
            environment='sandbox',
            consumer_key='test_key',
            consumer_secret='test_secret',
            business_short_code='123456',
            validation_url='https://example.com/validate',
            confirmation_url='https://example.com/confirm'
        )
    
    def test_access_token_creation(self, mpesa_config):
        """Test access token is created successfully"""
        expires_at = timezone.now() + timedelta(hours=1)
        token = MpesaAccessToken.objects.create(
            configuration=mpesa_config,
            access_token='test_token_123',
            expires_at=expires_at
        )
        assert token.id is not None
        assert token.access_token == 'test_token_123'
    
    def test_access_token_str_representation(self, mpesa_config):
        """Test string representation"""
        expires_at = timezone.now() + timedelta(hours=1)
        token = MpesaAccessToken.objects.create(
            configuration=mpesa_config,
            access_token='test_token',
            expires_at=expires_at
        )
        expected = f'Access Token for {mpesa_config.business_short_code}'
        assert str(token) == expected
    
    def test_is_expired_valid_token(self, mpesa_config):
        """Test is_expired method for valid token"""
        expires_at = timezone.now() + timedelta(hours=1)
        token = MpesaAccessToken.objects.create(
            configuration=mpesa_config,
            access_token='test_token',
            expires_at=expires_at
        )
        assert token.is_expired() is False
    
    def test_is_expired_expired_token(self, mpesa_config):
        """Test is_expired method for expired token"""
        expires_at = timezone.now() - timedelta(hours=1)
        token = MpesaAccessToken.objects.create(
            configuration=mpesa_config,
            access_token='test_token',
            expires_at=expires_at
        )
        assert token.is_expired() is True


@pytest.mark.django_db
class TestPaymentAllocation:
    """Test cases for PaymentAllocation 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 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(self, borrower):
        """Create a test loan"""
        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_payment_allocation_str_representation(self, loan):
        """Test string representation"""
        from loans.models import MpesaTransaction, Repayment
        
        # Create M-Pesa transaction
        transaction = MpesaTransaction.objects.create(
            borrower=loan.borrower,
            transaction_id='TEST123',
            amount=Decimal('10000.00'),
            phone_number='+254700000001',
            status='completed'
        )
        
        # Create repayment
        repayment = Repayment.objects.create(
            loan=loan,
            amount=Decimal('10000.00'),
            payment_date=timezone.now()
        )
        
        # Create payment allocation
        allocation = PaymentAllocation.objects.create(
            mpesa_transaction=transaction,
            loan=loan,
            allocated_amount=Decimal('10000.00'),
            repayment=repayment
        )
        
        expected = f'Allocation: KES 10000.00 to {loan.loan_number}'
        assert str(allocation) == expected


@pytest.mark.django_db
class TestUnconfirmedPayment:
    """Test cases for UnconfirmedPayment 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 borrower(self, branch):
        """Create a test borrower"""
        return User.objects.create_user(
            username='testborrower',
            phone_number='+254700000001',
            id_number='12345678',
            role='borrower',
            status='active',
            branch=branch
        )
    
    @pytest.fixture
    def admin(self, branch):
        """Create an admin user"""
        return User.objects.create_user(
            username='admin',
            phone_number='+254700000002',
            role='admin',
            is_staff=True,
            branch=branch
        )
    
    @pytest.fixture
    def mpesa_transaction(self, borrower):
        """Create M-Pesa transaction"""
        from loans.models import MpesaTransaction
        return MpesaTransaction.objects.create(
            borrower=borrower,
            transaction_id='TEST123',
            amount=Decimal('5000.00'),
            phone_number='+254799999999',  # Different phone
            status='pending'
        )
    
    def test_unconfirmed_payment_creation(self, mpesa_transaction, borrower):
        """Test unconfirmed payment is created successfully"""
        unconfirmed = UnconfirmedPayment.objects.create(
            mpesa_transaction=mpesa_transaction,
            payment_phone='+254799999999',
            payment_id_number='12345678',
            suggested_borrower=borrower,
            match_type='id_only',
            notes='Phone number mismatch'
        )
        assert unconfirmed.id is not None
        assert unconfirmed.status == 'pending'
        assert unconfirmed.match_type == 'id_only'
    
    def test_unconfirmed_payment_str_representation(self, mpesa_transaction, borrower):
        """Test string representation"""
        unconfirmed = UnconfirmedPayment.objects.create(
            mpesa_transaction=mpesa_transaction,
            suggested_borrower=borrower,
            match_type='phone_only'
        )
        expected = f'Unconfirmed Payment: KES {mpesa_transaction.amount} - Phone Matches (ID Doesn\'t)'
        assert str(unconfirmed) == expected
    
    def test_approve_unconfirmed_payment(self, mpesa_transaction, borrower, admin):
        """Test approving an unconfirmed payment"""
        from loans.models import Loan, LoanProduct, LoanApplication
        
        # Create a loan for the borrower
        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'
        )
        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'
        )
        
        unconfirmed = UnconfirmedPayment.objects.create(
            mpesa_transaction=mpesa_transaction,
            suggested_borrower=borrower,
            match_type='id_only'
        )
        
        # Approve the payment
        success = unconfirmed.approve(admin, 'Verified by admin')
        
        assert success is True
        unconfirmed.refresh_from_db()
        assert unconfirmed.status == 'processed'
        assert unconfirmed.approved_by == admin
    
    def test_reject_unconfirmed_payment(self, mpesa_transaction, borrower, admin):
        """Test rejecting an unconfirmed payment"""
        unconfirmed = UnconfirmedPayment.objects.create(
            mpesa_transaction=mpesa_transaction,
            suggested_borrower=borrower,
            match_type='none'
        )
        
        # Reject the payment
        unconfirmed.reject(admin, 'Cannot match to any borrower')
        
        unconfirmed.refresh_from_db()
        assert unconfirmed.status == 'rejected'
        assert unconfirmed.approved_by == admin
        assert unconfirmed.rejection_reason == 'Cannot match to any borrower'
        
        # Check transaction status
        mpesa_transaction.refresh_from_db()
        assert mpesa_transaction.status == 'rejected'
