"""
Integration tests for signal handlers.

This test module validates that signal handlers correctly create and post
journal entries for loan and expense transactions, and that error handling
works properly without failing the primary transaction.

Validates: Requirements 11.1, 11.2, 11.6, 12.1, 12.6
"""

from decimal import Decimal
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from unittest.mock import patch, MagicMock

from loans.models import Loan, LoanProduct, Repayment
from expenses.models import Expense
from accounting.models import JournalEntry, Account
from users.models import Branch

User = get_user_model()


class LoanDisbursementSignalTest(TestCase):
    """
    Test suite for loan disbursement signal handler.
    
    Validates: Requirements 11.1, 11.6
    """
    
    def setUp(self):
        """Set up test fixtures"""
        # Create test user
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123',
            first_name='Test',
            last_name='User',
            role='admin'
        )
        
        # Create branch
        self.branch = Branch.objects.create(
            name='Test Branch',
            code='TEST001',
            address='Test Location'
        )
        self.user.branch = self.branch
        self.user.save()
        
        # Create loan product
        self.loan_product = LoanProduct.objects.create(
            name='Test Product',
            product_type='biashara',
            description='Test loan product',
            min_amount=Decimal('1000.00'),
            max_amount=Decimal('100000.00'),
            interest_rate=Decimal('18.00'),
            processing_fee=Decimal('5.00'),
            min_duration=30,
            max_duration=365,
            duration_months=1,
            available_repayment_methods=['monthly', 'weekly', 'daily']
        )
        
        # Create required accounts
        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
        )
        Account.objects.create(
            code='202003',
            name='Cash',
            account_type='asset',
            description='Cash Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        Account.objects.create(
            code='401001',
            name='Interest Income',
            account_type='income',
            description='Interest Income Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        Account.objects.create(
            code='401002',
            name='Fee Income',
            account_type='income',
            description='Fee Income Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        
        # Create fiscal period
        from accounting.models import FiscalPeriod
        FiscalPeriod.objects.create(
            name='Test Period 2024',
            period_type='annual',
            start_date=timezone.now().date().replace(month=1, day=1),
            end_date=timezone.now().date().replace(month=12, day=31),
            status='open'
        )
    
    def test_loan_creation_triggers_journal_entry(self):
        """
        Test that creating an active loan triggers journal entry creation and posting.
        
        Validates: Requirements 11.1, 11.6
        """
        # Create a loan application first
        from loans.models import LoanApplication
        loan_app = LoanApplication.objects.create(
            borrower=self.user,
            loan_product=self.loan_product,
            requested_amount=Decimal('10000.00'),
            requested_duration=30,
            purpose='Test loan',
            repayment_method='monthly'
        )
        
        # Create a loan with active status
        loan = Loan.objects.create(
            application=loan_app,
            loan_number='TEST-001',
            borrower=self.user,
            principal_amount=Decimal('10000.00'),
            interest_amount=Decimal('1800.00'),
            processing_fee=Decimal('500.00'),
            total_amount=Decimal('10000.00'),
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timezone.timedelta(days=30),
            duration_days=30,
            status='active',
            disbursed_by=self.user,
            created_by=self.user
        )
        
        # Verify journal entry was created
        journal_entries = JournalEntry.objects.filter(loan=loan)
        self.assertEqual(journal_entries.count(), 1)
        
        # Verify journal entry details
        journal_entry = journal_entries.first()
        self.assertEqual(journal_entry.reference_number, f'LOAN-DISB-{loan.loan_number}')
        self.assertEqual(journal_entry.status, 'posted')
        self.assertEqual(journal_entry.branch, self.branch)
        
        # Verify journal entry lines
        lines = journal_entry.lines.all()
        self.assertEqual(lines.count(), 2)
        
        # Line 1: Debit Loan Portfolio
        debit_line = lines.get(line_number=1)
        self.assertEqual(debit_line.account.code, '202001')
        self.assertEqual(debit_line.debit_amount, Decimal('10000.00'))
        self.assertEqual(debit_line.credit_amount, Decimal('0.00'))
        
        # Line 2: Credit Cash
        credit_line = lines.get(line_number=2)
        self.assertEqual(credit_line.account.code, '202003')
        self.assertEqual(credit_line.debit_amount, Decimal('0.00'))
        self.assertEqual(credit_line.credit_amount, Decimal('10000.00'))
    
    def test_inactive_loan_does_not_trigger_journal_entry(self):
        """
        Test that creating a loan with status other than 'active' does not trigger
        journal entry creation.
        
        Validates: Requirements 11.1
        """
        # Create a loan application
        from loans.models import LoanApplication
        loan_app = LoanApplication.objects.create(
            borrower=self.user,
            loan_product=self.loan_product,
            requested_amount=Decimal('5000.00'),
            requested_duration=30,
            purpose='Test loan',
            repayment_method='monthly'
        )
        
        # Create a loan with pending status
        loan = Loan.objects.create(
            application=loan_app,
            loan_number='TEST-002',
            borrower=self.user,
            principal_amount=Decimal('5000.00'),
            interest_amount=Decimal('900.00'),
            processing_fee=Decimal('250.00'),
            total_amount=Decimal('5000.00'),
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timezone.timedelta(days=30),
            duration_days=30,
            status='pending',
            created_by=self.user
        )
        
        # Verify no journal entry was created
        journal_entries = JournalEntry.objects.filter(loan=loan)
        self.assertEqual(journal_entries.count(), 0)
    
    def test_loan_with_existing_journal_entry_skipped(self):
        """
        Test that if a loan already has journal entries, signal handler skips it.
        
        Validates: Requirements 11.1, 11.7
        """
        # Create a loan application
        from loans.models import LoanApplication
        loan_app = LoanApplication.objects.create(
            borrower=self.user,
            loan_product=self.loan_product,
            requested_amount=Decimal('8000.00'),
            requested_duration=30,
            purpose='Test loan',
            repayment_method='monthly'
        )
        
        # Create a loan
        loan = Loan.objects.create(
            application=loan_app,
            loan_number='TEST-003',
            borrower=self.user,
            principal_amount=Decimal('8000.00'),
            interest_amount=Decimal('1440.00'),
            processing_fee=Decimal('400.00'),
            total_amount=Decimal('8000.00'),
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timezone.timedelta(days=30),
            duration_days=30,
            status='active',
            disbursed_by=self.user,
            created_by=self.user
        )
        
        # Get the initial journal entry count
        initial_count = JournalEntry.objects.filter(loan=loan).count()
        self.assertEqual(initial_count, 1)
        
        # Update the loan (simulating another save)
        loan.save()
        
        # Verify no additional journal entry was created
        final_count = JournalEntry.objects.filter(loan=loan).count()
        self.assertEqual(final_count, initial_count)
    
    @patch('accounting.signals.logger')
    def test_loan_journal_entry_failure_does_not_fail_transaction(self, mock_logger):
        """
        Test that if journal entry creation fails, the loan transaction still succeeds.
        
        Validates: Requirements 11.6, 11.7
        """
        # Delete the required cash account to force a failure
        Account.objects.filter(code='202003').delete()
        
        # Create a loan application
        from loans.models import LoanApplication
        loan_app = LoanApplication.objects.create(
            borrower=self.user,
            loan_product=self.loan_product,
            requested_amount=Decimal('12000.00'),
            requested_duration=30,
            purpose='Test loan',
            repayment_method='monthly'
        )
        
        # Create a loan - this should succeed even though journal entry creation fails
        loan = Loan.objects.create(
            application=loan_app,
            loan_number='TEST-004',
            borrower=self.user,
            principal_amount=Decimal('12000.00'),
            interest_amount=Decimal('2160.00'),
            processing_fee=Decimal('600.00'),
            total_amount=Decimal('12000.00'),
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timezone.timedelta(days=30),
            duration_days=30,
            status='active',
            disbursed_by=self.user,
            created_by=self.user
        )
        
        # Verify the loan was created successfully
        self.assertIsNotNone(loan.id)
        self.assertEqual(loan.status, 'active')
        
        # Verify no journal entry was created
        journal_entries = JournalEntry.objects.filter(loan=loan)
        self.assertEqual(journal_entries.count(), 0)
        
        # Verify error was logged
        self.assertTrue(mock_logger.error.called)


class LoanRepaymentSignalTest(TestCase):
    """
    Test suite for loan repayment signal handler.
    
    Validates: Requirements 11.2, 11.6
    """
    
    def setUp(self):
        """Set up test fixtures"""
        # Create test user
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123',
            first_name='Test',
            last_name='User',
            role='admin'
        )
        
        # Create branch
        self.branch = Branch.objects.create(
            name='Test Branch',
            code='TEST001',
            address='Test Location'
        )
        self.user.branch = self.branch
        self.user.save()
        
        # Create loan product
        self.loan_product = LoanProduct.objects.create(
            name='Test Product',
            product_type='biashara',
            description='Test loan product',
            min_amount=Decimal('1000.00'),
            max_amount=Decimal('100000.00'),
            interest_rate=Decimal('18.00'),
            processing_fee=Decimal('5.00'),
            min_duration=30,
            max_duration=365,
            duration_months=1,
            available_repayment_methods=['monthly']
        )
        
        # Create required accounts
        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
        )
        Account.objects.create(
            code='202003',
            name='Cash',
            account_type='asset',
            description='Cash Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        Account.objects.create(
            code='401001',
            name='Interest Income',
            account_type='income',
            description='Interest Income Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        Account.objects.create(
            code='401002',
            name='Fee Income',
            account_type='income',
            description='Fee Income Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        
        # Create fiscal period
        from accounting.models import FiscalPeriod
        FiscalPeriod.objects.create(
            name='Test Period 2024',
            period_type='annual',
            start_date=timezone.now().date().replace(month=1, day=1),
            end_date=timezone.now().date().replace(month=12, day=31),
            status='open'
        )
        
        # Create loan application
        from loans.models import LoanApplication
        loan_app = LoanApplication.objects.create(
            application_number='APP-TEST-REP-001',
            borrower=self.user,
            loan_product=self.loan_product,
            requested_amount=Decimal('10000.00'),
            requested_duration=30,
            purpose='Test loan for repayment',
            repayment_method='monthly',
            interest_amount=Decimal('1800.00'),
            processing_fee_amount=Decimal('500.00'),
            total_amount=Decimal('12300.00'),
            status='approved'
        )
        
        # Create a loan
        self.loan = Loan.objects.create(
            loan_number='TEST-LOAN-001',
            application=loan_app,
            borrower=self.user,
            principal_amount=Decimal('10000.00'),
            interest_amount=Decimal('1800.00'),
            processing_fee=Decimal('500.00'),
            total_amount=Decimal('10000.00'),
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timezone.timedelta(days=30),
            duration_days=30,
            status='active',
            disbursed_by=self.user,
            created_by=self.user
        )
    
    def test_repayment_triggers_journal_entry(self):
        """
        Test that creating a repayment triggers journal entry creation and posting.
        
        Validates: Requirements 11.2, 11.6
        """
        # Create a repayment
        repayment = Repayment.objects.create(
            loan=self.loan,
            amount=Decimal('2000.00'),
            payment_date=timezone.now(),
            payment_method='mpesa',
            mpesa_transaction_id='TEST123456',
            receipt_number=f'RCP-TEST-{self.loan.id}'
        )
        
        # Verify journal entry was created
        journal_entries = JournalEntry.objects.filter(
            loan=self.loan,
            reference_number=f'LOAN-REPAY-{repayment.id}'
        )
        self.assertEqual(journal_entries.count(), 1)
        
        # Verify journal entry details
        journal_entry = journal_entries.first()
        self.assertEqual(journal_entry.status, 'posted')
        self.assertEqual(journal_entry.branch, self.branch)
        
        # Verify journal entry has at least 2 lines (debit cash, credit principal)
        lines = journal_entry.lines.all()
        self.assertGreaterEqual(lines.count(), 2)
        
        # Verify debit line (cash receipt)
        debit_line = lines.get(line_number=1, account__code='202003')
        self.assertEqual(debit_line.debit_amount, Decimal('2000.00'))
        self.assertEqual(debit_line.credit_amount, Decimal('0.00'))
    
    @patch('accounting.signals.logger')
    def test_repayment_journal_entry_failure_does_not_fail_transaction(self, mock_logger):
        """
        Test that if journal entry creation fails, the repayment transaction still succeeds.
        
        Validates: Requirements 11.6, 11.7
        """
        # Make the cash account inactive instead of deleting it (to avoid ProtectedError)
        cash_account = Account.objects.get(code='202003')
        cash_account.is_active = False
        cash_account.save()
        
        # Create a repayment - this should succeed even though journal entry creation fails
        repayment = Repayment.objects.create(
            loan=self.loan,
            amount=Decimal('1500.00'),
            payment_date=timezone.now(),
            payment_method='mpesa',
            mpesa_transaction_id='TEST789012',
            receipt_number=f'RCP-FAIL-{self.loan.id}'
        )
        
        # Verify the repayment was created successfully
        self.assertIsNotNone(repayment.id)
        self.assertEqual(repayment.amount, Decimal('1500.00'))
        
        # Verify no journal entry was created
        journal_entries = JournalEntry.objects.filter(
            reference_number=f'LOAN-REPAY-{repayment.id}'
        )
        self.assertEqual(journal_entries.count(), 0)
        
        # Verify error was logged
        self.assertTrue(mock_logger.error.called)


class ExpenseApprovalIntegrationTest(TestCase):
    """
    Test suite for expense approval integration with accounting.
    
    Validates: Requirements 12.1, 12.6
    """
    
    def setUp(self):
        """Set up test fixtures"""
        # Create test user
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123',
            first_name='Test',
            last_name='User',
            role='admin'
        )
        
        # Create branch
        self.branch = Branch.objects.create(
            name='Test Branch',
            code='TEST001',
            address='Test Location'
        )
        self.user.branch = self.branch
        self.user.save()
        
        # Create required accounts
        Account.objects.create(
            code='502001',
            name='Operating Expenses',
            account_type='expense',
            description='Operating Expenses Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        Account.objects.create(
            code='502002',
            name='Staff Costs',
            account_type='expense',
            description='Staff Costs Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        Account.objects.create(
            code='202003',
            name='Cash',
            account_type='asset',
            description='Cash Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        Account.objects.create(
            code='202004',
            name='Bank',
            account_type='asset',
            description='Bank Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        Account.objects.create(
            code='202005',
            name='M-Pesa',
            account_type='asset',
            description='M-Pesa Account',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        
        # Create fiscal period
        from accounting.models import FiscalPeriod
        FiscalPeriod.objects.create(
            name='Test Period 2024',
            period_type='annual',
            start_date=timezone.now().date().replace(month=1, day=1),
            end_date=timezone.now().date().replace(month=12, day=31),
            status='open'
        )
    
    def test_expense_approval_triggers_journal_entry(self):
        """
        Test that approving an expense triggers journal entry creation and posting.
        
        Validates: Requirements 12.1, 12.6
        """
        # Create a pending expense
        expense = Expense.objects.create(
            title='Office Supplies',
            description='Purchase of office supplies',
            category='operational',
            amount=Decimal('500.00'),
            payment_method='cash',
            paid_to='Office Mart',
            branch=self.branch,
            staff=self.user,
            expense_date=timezone.now().date(),
            status='pending'
        )
        
        # Approve the expense using the view logic
        from accounting.services.integration_service import IntegrationService
        from accounting.services.accounting_service import AccountingService
        
        expense.approve(self.user)
        
        integration_service = IntegrationService()
        journal_entry = integration_service.create_expense_entry(expense)
        
        accounting_service = AccountingService()
        accounting_service.post_journal_entry(journal_entry, self.user)
        
        # Verify journal entry was created
        journal_entries = JournalEntry.objects.filter(expense=expense)
        self.assertEqual(journal_entries.count(), 1)
        
        # Verify journal entry details
        journal_entry = journal_entries.first()
        self.assertEqual(journal_entry.reference_number, f'EXP-{expense.id}')
        self.assertEqual(journal_entry.status, 'posted')
        self.assertEqual(journal_entry.branch, self.branch)
        
        # Verify journal entry lines
        lines = journal_entry.lines.all()
        self.assertEqual(lines.count(), 2)
        
        # Line 1: Debit Operating Expenses
        debit_line = lines.get(line_number=1)
        self.assertEqual(debit_line.account.code, '502001')
        self.assertEqual(debit_line.debit_amount, Decimal('500.00'))
        self.assertEqual(debit_line.credit_amount, Decimal('0.00'))
        
        # Line 2: Credit Cash
        credit_line = lines.get(line_number=2)
        self.assertEqual(credit_line.account.code, '202003')
        self.assertEqual(credit_line.debit_amount, Decimal('0.00'))
        self.assertEqual(credit_line.credit_amount, Decimal('500.00'))
    
    def test_expense_approval_with_different_payment_methods(self):
        """
        Test that expense approval creates correct journal entries for different payment methods.
        
        Validates: Requirements 12.1, 12.6, 12.7
        """
        from accounting.services.integration_service import IntegrationService
        from accounting.services.accounting_service import AccountingService
        
        integration_service = IntegrationService()
        accounting_service = AccountingService()
        
        # Test cash payment
        expense_cash = Expense.objects.create(
            title='Cash Expense',
            category='operational',
            amount=Decimal('100.00'),
            payment_method='cash',
            paid_to='Vendor 1',
            branch=self.branch,
            staff=self.user,
            expense_date=timezone.now().date(),
            status='pending'
        )
        expense_cash.approve(self.user)
        entry_cash = integration_service.create_expense_entry(expense_cash)
        accounting_service.post_journal_entry(entry_cash, self.user)
        
        self.assertEqual(
            entry_cash.lines.get(line_number=2).account.code,
            '202003'
        )
        
        # Test bank payment
        expense_bank = Expense.objects.create(
            title='Bank Expense',
            category='operational',
            amount=Decimal('200.00'),
            payment_method='bank',
            paid_to='Vendor 2',
            branch=self.branch,
            staff=self.user,
            expense_date=timezone.now().date(),
            status='pending'
        )
        expense_bank.approve(self.user)
        entry_bank = integration_service.create_expense_entry(expense_bank)
        accounting_service.post_journal_entry(entry_bank, self.user)
        
        self.assertEqual(
            entry_bank.lines.get(line_number=2).account.code,
            '202004'
        )
        
        # Test M-Pesa payment
        expense_mpesa = Expense.objects.create(
            title='M-Pesa Expense',
            category='operational',
            amount=Decimal('300.00'),
            payment_method='mpesa',
            paid_to='Vendor 3',
            branch=self.branch,
            staff=self.user,
            expense_date=timezone.now().date(),
            status='pending'
        )
        expense_mpesa.approve(self.user)
        entry_mpesa = integration_service.create_expense_entry(expense_mpesa)
        accounting_service.post_journal_entry(entry_mpesa, self.user)
        
        self.assertEqual(
            entry_mpesa.lines.get(line_number=2).account.code,
            '202005'
        )
    
    def test_expense_approval_failure_does_not_prevent_approval(self):
        """
        Test that if journal entry creation fails, the expense approval still succeeds.
        
        Validates: Requirements 12.6, 12.7
        """
        # Delete the required cash account to force a failure
        Account.objects.filter(code='202003').delete()
        
        # Create and approve an expense
        expense = Expense.objects.create(
            title='Test Expense',
            category='operational',
            amount=Decimal('750.00'),
            payment_method='cash',
            paid_to='Test Vendor',
            branch=self.branch,
            staff=self.user,
            expense_date=timezone.now().date(),
            status='pending'
        )
        
        # Approve the expense - this should succeed even if journal entry fails
        expense.approve(self.user)
        
        # Verify the expense was approved
        expense.refresh_from_db()
        self.assertEqual(expense.status, 'approved')
        self.assertEqual(expense.approved_by, self.user)
        
        # Try to create journal entry (will fail due to missing account)
        from accounting.services.integration_service import IntegrationService
        from django.core.exceptions import ValidationError
        
        integration_service = IntegrationService()
        
        with self.assertRaises(ValidationError):
            integration_service.create_expense_entry(expense)
