"""
Unit tests for accounting Celery tasks.

Tests cover:
- Interest accrual task functionality
- Account balance caching task functionality
- Error handling and resilience
"""

from decimal import Decimal
from datetime import date, timedelta
from django.test import TestCase
from django.utils import timezone
from django.contrib.auth import get_user_model
from unittest.mock import patch, MagicMock

from accounting.models import (
    Account,
    JournalEntry,
    AccountBalance,
    FiscalPeriod,
)
from accounting.tasks import (
    accrue_loan_interest,
    cache_account_balances,
    calculate_daily_interest,
)
from loans.models import Loan, LoanApplication, LoanProduct

User = get_user_model()


class AccrueLoanInterestTaskTestCase(TestCase):
    """Test cases for the accrue_loan_interest Celery task."""
    
    def setUp(self):
        """Set up test data."""
        # Create a branch
        from users.models import Branch
        self.branch = Branch.objects.create(
            name='Test Branch',
            code='TB001'
        )
        
        # Create a test user
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123',
            email='test@example.com',
            phone_number='0700123456',
            first_name='Test',
            last_name='User',
            branch=self.branch
        )
        
        # Create system user for auto-posting
        self.system_user = User.objects.create_user(
            username='system',
            password='systempass123',
            email='system@example.com',
            phone_number='0700000001',
            first_name='System',
            last_name='User'
        )
        
        # Create a loan product
        self.loan_product = LoanProduct.objects.create(
            name='Test Product',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000.00'),
            max_amount=Decimal('50000.00'),
            interest_rate=Decimal('10.00'),  # 10% monthly
            processing_fee=Decimal('5.00'),
            late_payment_penalty=Decimal('2.00'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly']
        )
        
        # Create required accounting accounts
        self.loan_portfolio = Account.objects.create(
            code='202001',
            name='Loan Portfolio',
            account_type='asset',
            description='Loan Portfolio Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        
        self.accrued_interest = Account.objects.create(
            code='202002',
            name='Accrued Interest Receivable',
            account_type='asset',
            description='Accrued Interest Receivable',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        
        self.interest_income = Account.objects.create(
            code='401001',
            name='Interest Income',
            account_type='income',
            description='Interest Income',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        
        # Create an open fiscal period
        today = timezone.now().date()
        self.fiscal_period = FiscalPeriod.objects.create(
            name=f'{today.strftime("%B %Y")}',
            period_type='monthly',
            start_date=today.replace(day=1),
            end_date=(today.replace(day=1) + timedelta(days=32)).replace(day=1) - timedelta(days=1),
            status='open'
        )
    
    def create_active_loan(self, principal=10000, interest_rate=10):
        """Helper method to create an active loan."""
        application = LoanApplication.objects.create(
            borrower=self.user,
            loan_product=self.loan_product,
            requested_amount=Decimal(str(principal)),
            requested_duration=30,
            purpose='Test loan',
            repayment_method='monthly',
            status='approved'
        )
        
        loan = Loan.objects.create(
            application=application,
            borrower=self.user,
            principal_amount=Decimal(str(principal)),
            interest_amount=Decimal(str(principal * interest_rate / 100)),
            processing_fee=Decimal('500.00'),
            total_amount=Decimal(str(principal)),
            disbursement_date=timezone.now() - timedelta(days=5),
            due_date=timezone.now() + timedelta(days=25),
            duration_days=30,
            status='active',
            is_deleted=False
        )
        
        return loan
    
    def test_accrue_loan_interest_creates_entries_for_active_loans(self):
        """Test that accrual entries are created for active loans."""
        # Create an active loan
        loan = self.create_active_loan(principal=10000)
        
        # Run the task
        result = accrue_loan_interest()
        
        # Verify results
        self.assertEqual(result['processed'], 1)
        self.assertEqual(result['accrued'], 1)
        self.assertEqual(result['skipped'], 0)
        self.assertEqual(result['errors'], 0)
        
        # Verify journal entry was created
        today = timezone.now().date()
        journal_entries = JournalEntry.objects.filter(
            loan=loan,
            transaction_date=today,
            reference_number__startswith='INT-ACCR'
        )
        self.assertEqual(journal_entries.count(), 1)
        
        # Verify journal entry was posted
        entry = journal_entries.first()
        self.assertEqual(entry.status, 'posted')
        
        # Verify total interest accrued is positive
        total_interest = Decimal(result['total_interest_accrued'])
        self.assertGreater(total_interest, Decimal('0.00'))
    
    def test_accrue_loan_interest_skips_zero_interest_loans(self):
        """Test that loans with zero interest are skipped."""
        # Create a loan with zero outstanding balance
        loan = self.create_active_loan(principal=10000)
        
        # Mock outstanding_amount to return 0
        with patch.object(Loan, 'outstanding_amount', new_callable=lambda: Decimal('0.00')):
            # Run the task
            result = accrue_loan_interest()
            
            # Verify results
            self.assertEqual(result['processed'], 1)
            self.assertEqual(result['accrued'], 0)
            self.assertEqual(result['skipped'], 1)
            self.assertEqual(result['errors'], 0)
            
            # Verify no journal entry was created
            today = timezone.now().date()
            journal_entries = JournalEntry.objects.filter(
                loan=loan,
                transaction_date=today
            )
            self.assertEqual(journal_entries.count(), 0)
    
    def test_accrue_loan_interest_handles_errors_gracefully(self):
        """Test that task continues processing when individual entries fail."""
        # Create multiple active loans
        loan1 = self.create_active_loan(principal=10000)
        loan2 = self.create_active_loan(principal=20000)
        loan3 = self.create_active_loan(principal=15000)
        
        # Mock create_interest_accrual_entry to fail for loan2
        from accounting.services.integration_service import IntegrationService
        original_method = IntegrationService.create_interest_accrual_entry
        
        def side_effect(*args, **kwargs):
            loan = kwargs.get('loan') or args[0]
            if loan == loan2:
                raise Exception("Test error for loan2")
            return original_method(IntegrationService(), *args, **kwargs)
        
        with patch.object(
            IntegrationService,
            'create_interest_accrual_entry',
            side_effect=side_effect
        ):
            # Run the task
            result = accrue_loan_interest()
            
            # Verify results - should process all 3, accrue 2, error 1
            self.assertEqual(result['processed'], 3)
            self.assertEqual(result['accrued'], 2)
            self.assertEqual(result['errors'], 1)
            
            # Verify journal entries were created for loan1 and loan3
            today = timezone.now().date()
            for loan in [loan1, loan3]:
                journal_entries = JournalEntry.objects.filter(
                    loan=loan,
                    transaction_date=today,
                    status='posted'
                )
                self.assertEqual(journal_entries.count(), 1)
            
            # Verify no entry for loan2
            journal_entries = JournalEntry.objects.filter(
                loan=loan2,
                transaction_date=today
            )
            self.assertEqual(journal_entries.count(), 0)
    
    def test_accrue_loan_interest_skips_inactive_loans(self):
        """Test that inactive loans are not processed."""
        # Create an active loan
        active_loan = self.create_active_loan(principal=10000)
        
        # Create an inactive loan (paid)
        paid_loan = self.create_active_loan(principal=15000)
        paid_loan.status = 'paid'
        paid_loan.save()
        
        # Create a deleted loan
        deleted_loan = self.create_active_loan(principal=20000)
        deleted_loan.is_deleted = True
        deleted_loan.save()
        
        # Run the task
        result = accrue_loan_interest()
        
        # Verify only active loan was processed
        self.assertEqual(result['processed'], 1)
        self.assertEqual(result['accrued'], 1)
        
        # Verify journal entries
        today = timezone.now().date()
        self.assertEqual(
            JournalEntry.objects.filter(
                loan=active_loan,
                transaction_date=today
            ).count(),
            1
        )
        self.assertEqual(
            JournalEntry.objects.filter(
                loan__in=[paid_loan, deleted_loan],
                transaction_date=today
            ).count(),
            0
        )
    
    def test_calculate_daily_interest_function(self):
        """Test the calculate_daily_interest helper function."""
        # Create a loan with known parameters
        loan = self.create_active_loan(principal=10000)
        
        # Mock outstanding_amount
        with patch.object(
            Loan,
            'outstanding_amount',
            new_callable=lambda: Decimal('10000.00')
        ):
            # Calculate daily interest
            daily_interest = calculate_daily_interest(loan)
            
            # Verify calculation
            # Monthly rate: 10%
            # Annual rate: 10% * 12 = 120%
            # Daily rate: 120% / 365 = 0.3288% per day
            # Daily interest: 10000 * 0.003288 = 32.88
            expected = Decimal('32.88')
            self.assertAlmostEqual(
                float(daily_interest),
                float(expected),
                places=2
            )


class CacheAccountBalancesTaskTestCase(TestCase):
    """Test cases for the cache_account_balances Celery task."""
    
    def setUp(self):
        """Set up test data."""
        # Create a test user
        self.user = User.objects.create_user(
            username='testuser2',
            password='testpass123',
            email='test2@example.com',
            phone_number='0700123457',
            first_name='Test',
            last_name='User'
        )
        
        # Create test accounts
        self.asset_account = Account.objects.create(
            code='202001',
            name='Cash',
            account_type='asset',
            description='Cash Account',
            is_active=True,
            created_by=self.user
        )
        
        self.liability_account = Account.objects.create(
            code='302001',
            name='Savings',
            account_type='liability',
            description='Client Savings',
            is_active=True,
            created_by=self.user
        )
        
        self.income_account = Account.objects.create(
            code='401001',
            name='Interest Income',
            account_type='income',
            description='Interest Income',
            is_active=True,
            created_by=self.user
        )
    
    def test_cache_account_balances_creates_records(self):
        """Test that AccountBalance records are created."""
        # Mock calculate_account_balance to return test values
        from accounting.services.accounting_service import AccountingService
        
        def mock_calculate_balance(account, as_of_date, branch):
            # Return different balances for different account types
            if account.account_type == 'asset':
                return Decimal('10000.00')
            elif account.account_type == 'liability':
                return Decimal('5000.00')
            else:  # income
                return Decimal('2000.00')
        
        with patch.object(
            AccountingService,
            'calculate_account_balance',
            side_effect=mock_calculate_balance
        ):
            # Run the task
            result = cache_account_balances()
            
            # Verify results
            self.assertEqual(result['processed'], 3)
            self.assertEqual(result['created'], 3)
            self.assertEqual(result['updated'], 0)
            self.assertEqual(result['errors'], 0)
            
            # Verify AccountBalance records were created
            yesterday = timezone.now().date() - timedelta(days=1)
            
            # Check asset account
            asset_balance = AccountBalance.objects.get(
                account=self.asset_account,
                as_of_date=yesterday
            )
            self.assertEqual(asset_balance.debit_balance, Decimal('10000.00'))
            self.assertEqual(asset_balance.credit_balance, Decimal('0.00'))
            self.assertEqual(asset_balance.net_balance, Decimal('10000.00'))
            
            # Check liability account
            liability_balance = AccountBalance.objects.get(
                account=self.liability_account,
                as_of_date=yesterday
            )
            self.assertEqual(liability_balance.debit_balance, Decimal('0.00'))
            self.assertEqual(liability_balance.credit_balance, Decimal('5000.00'))
            self.assertEqual(liability_balance.net_balance, Decimal('5000.00'))
            
            # Check income account
            income_balance = AccountBalance.objects.get(
                account=self.income_account,
                as_of_date=yesterday
            )
            self.assertEqual(income_balance.debit_balance, Decimal('0.00'))
            self.assertEqual(income_balance.credit_balance, Decimal('2000.00'))
            self.assertEqual(income_balance.net_balance, Decimal('2000.00'))
    
    def test_cache_account_balances_updates_existing_records(self):
        """Test that existing AccountBalance records are updated."""
        yesterday = timezone.now().date() - timedelta(days=1)
        
        # Create an existing AccountBalance record
        existing_balance = AccountBalance.objects.create(
            account=self.asset_account,
            branch=None,
            as_of_date=yesterday,
            debit_balance=Decimal('5000.00'),
            credit_balance=Decimal('0.00'),
            net_balance=Decimal('5000.00')
        )
        
        # Mock calculate_account_balance
        from accounting.services.accounting_service import AccountingService
        
        with patch.object(
            AccountingService,
            'calculate_account_balance',
            return_value=Decimal('15000.00')
        ):
            # Run the task
            result = cache_account_balances()
            
            # Verify results
            self.assertGreater(result['processed'], 0)
            self.assertGreater(result['updated'], 0)
            
            # Verify the record was updated
            existing_balance.refresh_from_db()
            self.assertEqual(existing_balance.debit_balance, Decimal('15000.00'))
            self.assertEqual(existing_balance.net_balance, Decimal('15000.00'))
    
    def test_cache_account_balances_handles_errors_gracefully(self):
        """Test that task continues when individual calculations fail."""
        # Mock calculate_account_balance to fail for liability account
        from accounting.services.accounting_service import AccountingService
        
        def mock_calculate_balance(account, as_of_date, branch):
            if account == self.liability_account:
                raise Exception("Test error for liability account")
            return Decimal('10000.00')
        
        with patch.object(
            AccountingService,
            'calculate_account_balance',
            side_effect=mock_calculate_balance
        ):
            # Run the task
            result = cache_account_balances()
            
            # Verify results - should process all 3, succeed 2, error 1
            self.assertEqual(result['processed'], 3)
            self.assertEqual(result['created'], 2)
            self.assertEqual(result['errors'], 1)
            
            # Verify records for successful accounts
            yesterday = timezone.now().date() - timedelta(days=1)
            self.assertTrue(
                AccountBalance.objects.filter(
                    account=self.asset_account,
                    as_of_date=yesterday
                ).exists()
            )
            self.assertTrue(
                AccountBalance.objects.filter(
                    account=self.income_account,
                    as_of_date=yesterday
                ).exists()
            )
            
            # Verify no record for failed account
            self.assertFalse(
                AccountBalance.objects.filter(
                    account=self.liability_account,
                    as_of_date=yesterday
                ).exists()
            )
    
    def test_cache_account_balances_skips_inactive_accounts(self):
        """Test that inactive accounts are not processed."""
        # Create an inactive account
        inactive_account = Account.objects.create(
            code='999999',
            name='Inactive Account',
            account_type='asset',
            description='Inactive',
            is_active=False,
            created_by=self.user
        )
        
        # Run the task
        with patch('accounting.services.accounting_service.AccountingService.calculate_account_balance') as mock_calc:
            mock_calc.return_value = Decimal('10000.00')
            result = cache_account_balances()
            
            # Verify only active accounts were processed (3 active accounts)
            self.assertEqual(result['processed'], 3)
            
            # Verify no record for inactive account
            yesterday = timezone.now().date() - timedelta(days=1)
            self.assertFalse(
                AccountBalance.objects.filter(
                    account=inactive_account,
                    as_of_date=yesterday
                ).exists()
            )
