"""
Unit tests for audit logging functionality.

Tests for task 24.5:
- Test audit log created on journal entry posting
- Test audit log created on account modification
- Test audit_log_report view shows correct logs
- Test data_integrity_check finds unbalanced entries

Requirements: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7, 14.9
"""

from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
from django.core.management import call_command
from decimal import Decimal
from datetime import date, timedelta
from io import StringIO

from accounting.models import (
    Account,
    JournalEntry,
    JournalEntryLine,
    GeneralLedger,
    FiscalPeriod,
    AuditLog
)
from accounting.services.accounting_service import AccountingService

User = get_user_model()


class AuditLogModelTest(TestCase):
    """Test cases for AuditLog model"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123',
            email='test@example.com',
            first_name='Test',
            last_name='User'
        )
    
    def test_create_audit_log_entry(self):
        """Test creating an audit log entry with all fields"""
        audit_log = AuditLog.objects.create(
            user=self.user,
            action_type='create',
            model_name='Account',
            object_id='202001',
            changes={
                'after': {
                    'code': '202001',
                    'name': 'Loan Portfolio',
                    'account_type': 'asset'
                }
            },
            description='Created account 202001 - Loan Portfolio'
        )
        
        self.assertEqual(audit_log.user, self.user)
        self.assertEqual(audit_log.action_type, 'create')
        self.assertEqual(audit_log.model_name, 'Account')
        self.assertEqual(audit_log.object_id, '202001')
        self.assertIsNotNone(audit_log.timestamp)
        self.assertIn('after', audit_log.changes)
        self.assertEqual(audit_log.description, 'Created account 202001 - Loan Portfolio')
    
    def test_audit_log_action_types(self):
        """Test all available action types"""
        action_types = [
            'create', 'modify', 'delete', 'post', 'reverse',
            'activate', 'deactivate', 'close', 'reopen'
        ]
        
        for action in action_types:
            audit_log = AuditLog.objects.create(
                user=self.user,
                action_type=action,
                model_name='TestModel',
                object_id='test_id',
                description=f'Test {action} action'
            )
            self.assertEqual(audit_log.action_type, action)
    
    def test_audit_log_ordering(self):
        """Test that audit logs are ordered by timestamp descending"""
        # Create multiple audit logs
        for i in range(5):
            AuditLog.objects.create(
                user=self.user,
                action_type='create',
                model_name='Account',
                object_id=f'20200{i}',
                description=f'Test log {i}'
            )
        
        # Get all logs
        logs = AuditLog.objects.all()
        
        # Verify ordering (most recent first)
        timestamps = [log.timestamp for log in logs]
        self.assertEqual(timestamps, sorted(timestamps, reverse=True))


class JournalEntryAuditLoggingTest(TestCase):
    """Test cases for journal entry audit logging"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123',
            email='test@example.com',
            first_name='Test',
            last_name='User'
        )
        
        # Create accounts
        self.cash_account = Account.objects.create(
            code='202003',
            name='Cash',
            account_type='asset',
            description='Cash on hand',
            created_by=self.user
        )
        
        self.loan_account = Account.objects.create(
            code='202001',
            name='Loan Portfolio',
            account_type='asset',
            description='Outstanding loans',
            created_by=self.user
        )
        
        # Create fiscal period
        self.period = FiscalPeriod.objects.create(
            name='January 2024',
            period_type='monthly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 1, 31),
            status='open'
        )
    
    def test_audit_log_created_on_journal_entry_creation(self):
        """Test that an audit log is created when a journal entry is created"""
        # Clear any existing audit logs
        AuditLog.objects.all().delete()
        
        # Create journal entry
        journal_entry = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date(2024, 1, 15),
            description='Test journal entry',
            status='draft',
            created_by=self.user
        )
        
        # Check that audit log was created
        audit_logs = AuditLog.objects.filter(
            model_name='JournalEntry',
            object_id='JE-2024-001'
        )
        
        self.assertEqual(audit_logs.count(), 1)
        
        log = audit_logs.first()
        self.assertEqual(log.action_type, 'create')
        self.assertEqual(log.user, self.user)
        self.assertIn('after', log.changes)
        self.assertEqual(log.changes['after']['reference_number'], 'JE-2024-001')
        self.assertIn('Created journal entry', log.description)
    
    def test_audit_log_created_on_journal_entry_posting(self):
        """Test that an audit log is created when a journal entry is posted"""
        # Create journal entry with lines
        journal_entry = JournalEntry.objects.create(
            reference_number='JE-2024-002',
            transaction_date=date(2024, 1, 15),
            description='Test posting',
            status='draft',
            created_by=self.user
        )
        
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=self.cash_account,
            description='Test debit',
            debit_amount=Decimal('1000.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=self.loan_account,
            description='Test credit',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('1000.00'),
            line_number=2
        )
        
        # Clear audit logs from creation
        AuditLog.objects.filter(action_type='create').delete()
        
        # Post the journal entry
        service = AccountingService()
        service.post_journal_entry(journal_entry, self.user)
        
        # Check that posting audit log was created
        audit_logs = AuditLog.objects.filter(
            model_name='JournalEntry',
            object_id='JE-2024-002',
            action_type='post'
        )
        
        self.assertEqual(audit_logs.count(), 1)
        
        log = audit_logs.first()
        self.assertEqual(log.action_type, 'post')
        self.assertEqual(log.user, self.user)
        self.assertIn('before', log.changes)
        self.assertIn('after', log.changes)
        self.assertEqual(log.changes['before']['status'], 'draft')
        self.assertEqual(log.changes['after']['status'], 'posted')
        self.assertIn('Posted journal entry', log.description)
    
    def test_audit_log_created_on_journal_entry_reversal(self):
        """Test that an audit log is created when a journal entry is reversed"""
        # Create and post journal entry
        journal_entry = JournalEntry.objects.create(
            reference_number='JE-2024-003',
            transaction_date=date(2024, 1, 15),
            description='Test reversal',
            status='draft',
            created_by=self.user
        )
        
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=self.cash_account,
            description='Test debit',
            debit_amount=Decimal('1000.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=self.loan_account,
            description='Test credit',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('1000.00'),
            line_number=2
        )
        
        service = AccountingService()
        service.post_journal_entry(journal_entry, self.user)
        
        # Clear previous audit logs
        AuditLog.objects.all().delete()
        
        # Reverse the journal entry
        reversal_entry = service.reverse_journal_entry(
            journal_entry, self.user, date(2024, 1, 16), 'Test reversal'
        )
        
        # Check that reversal audit logs were created
        # One for the original entry status change, one for the new reversal entry
        original_logs = AuditLog.objects.filter(
            model_name='JournalEntry',
            object_id='JE-2024-003',
            action_type='reverse'
        )
        
        reversal_logs = AuditLog.objects.filter(
            model_name='JournalEntry',
            object_id='JE-2024-003-REV'
        )
        
        self.assertGreaterEqual(original_logs.count(), 1)
        self.assertGreaterEqual(reversal_logs.count(), 1)


class AccountAuditLoggingTest(TestCase):
    """Test cases for account audit logging"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123',
            email='test@example.com',
            first_name='Test',
            last_name='User'
        )
    
    def test_audit_log_created_on_account_creation(self):
        """Test that an audit log is created when an account is created"""
        # Clear any existing audit logs
        AuditLog.objects.all().delete()
        
        # Create account
        account = Account.objects.create(
            code='202010',
            name='Test Account',
            account_type='asset',
            description='Test account for audit',
            created_by=self.user
        )
        
        # Check that audit log was created
        audit_logs = AuditLog.objects.filter(
            model_name='Account',
            object_id='202010'
        )
        
        self.assertEqual(audit_logs.count(), 1)
        
        log = audit_logs.first()
        self.assertEqual(log.action_type, 'create')
        self.assertEqual(log.user, self.user)
        self.assertIn('after', log.changes)
        self.assertEqual(log.changes['after']['code'], '202010')
        self.assertIn('Created account', log.description)
    
    def test_audit_log_created_on_account_modification(self):
        """Test that an audit log is created when an account is modified"""
        # Create account
        account = Account.objects.create(
            code='202011',
            name='Original Name',
            account_type='asset',
            description='Original description',
            created_by=self.user
        )
        
        # Clear creation audit log
        AuditLog.objects.all().delete()
        
        # Modify account
        account.name = 'Modified Name'
        account.description = 'Modified description'
        account.save()
        
        # Check that modification audit log was created
        audit_logs = AuditLog.objects.filter(
            model_name='Account',
            object_id='202011',
            action_type='modify'
        )
        
        self.assertEqual(audit_logs.count(), 1)
        
        log = audit_logs.first()
        self.assertEqual(log.action_type, 'modify')
        self.assertIn('Modified account', log.description)
    
    def test_audit_log_created_on_account_deactivation(self):
        """Test that an audit log is created when an account is deactivated"""
        # Create active account
        account = Account.objects.create(
            code='202012',
            name='Test Account',
            account_type='asset',
            description='Test',
            is_active=True,
            created_by=self.user
        )
        
        # Clear creation audit log
        AuditLog.objects.all().delete()
        
        # Deactivate account
        account.is_active = False
        account.save()
        
        # Check that deactivation audit log was created
        audit_logs = AuditLog.objects.filter(
            model_name='Account',
            object_id='202012',
            action_type='deactivate'
        )
        
        self.assertEqual(audit_logs.count(), 1)
        
        log = audit_logs.first()
        self.assertEqual(log.action_type, 'deactivate')
        self.assertIn('Deactivated account', log.description)
        self.assertEqual(log.changes['before']['is_active'], True)
        self.assertEqual(log.changes['after']['is_active'], False)
    
    def test_audit_log_created_on_account_activation(self):
        """Test that an audit log is created when an account is activated"""
        # Create inactive account
        account = Account.objects.create(
            code='202013',
            name='Test Account',
            account_type='asset',
            description='Test',
            is_active=False,
            created_by=self.user
        )
        
        # Clear creation audit log
        AuditLog.objects.all().delete()
        
        # Activate account
        account.is_active = True
        account.save()
        
        # Check that activation audit log was created
        audit_logs = AuditLog.objects.filter(
            model_name='Account',
            object_id='202013',
            action_type='activate'
        )
        
        self.assertEqual(audit_logs.count(), 1)
        
        log = audit_logs.first()
        self.assertEqual(log.action_type, 'activate')
        self.assertIn('Activated account', log.description)


class AuditLogReportViewTest(TestCase):
    """Test cases for audit log report view"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123',
            email='test@example.com',
            first_name='Test',
            last_name='User',
            phone_number='0722000000',
            role='admin',
            is_staff=True
        )
        self.client = Client()
        self.client.login(username='testuser', password='testpass123')
        
        # Create some audit log entries
        for i in range(10):
            AuditLog.objects.create(
                user=self.user,
                action_type='create',
                model_name='Account',
                object_id=f'20201{i}',
                description=f'Test log {i}'
            )
    
    def test_audit_log_report_view_accessible(self):
        """Test that audit log report view is accessible"""
        response = self.client.get(reverse('accounting:audit_log_report'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'accounting/reports/audit_log.html')
    
    def test_audit_log_report_shows_correct_logs(self):
        """Test that audit log report shows the correct logs"""
        response = self.client.get(reverse('accounting:audit_log_report'))
        
        self.assertEqual(response.status_code, 200)
        self.assertIn('audit_logs', response.context)
        
        # Should have 10 logs
        audit_logs = response.context['audit_logs']
        self.assertEqual(len(audit_logs), 10)
    
    def test_audit_log_report_filtering_by_user(self):
        """Test filtering audit log by user"""
        # Create another user with logs
        other_user = User.objects.create_user(
            username='otheruser',
            password='testpass123',
            email='other@example.com',
            phone_number='0722000001',
            is_staff=True
        )
        
        AuditLog.objects.create(
            user=other_user,
            action_type='create',
            model_name='Account',
            object_id='202020',
            description='Other user log'
        )
        
        # Filter by first user
        response = self.client.get(
            reverse('accounting:audit_log_report'),
            {'user': self.user.id}
        )
        
        self.assertEqual(response.status_code, 200)
        audit_logs = response.context['audit_logs']
        
        # Should only show logs from first user
        for log in audit_logs:
            self.assertEqual(log.user, self.user)
    
    def test_audit_log_report_filtering_by_action_type(self):
        """Test filtering audit log by action type"""
        # Create logs with different action types
        AuditLog.objects.create(
            user=self.user,
            action_type='modify',
            model_name='Account',
            object_id='202021',
            description='Modify action'
        )
        
        # Filter by modify action
        response = self.client.get(
            reverse('accounting:audit_log_report'),
            {'action_type': 'modify'}
        )
        
        self.assertEqual(response.status_code, 200)
        audit_logs = response.context['audit_logs']
        
        # Should only show modify actions
        for log in audit_logs:
            self.assertEqual(log.action_type, 'modify')
    
    def test_audit_log_report_search(self):
        """Test searching audit log by model name or object ID"""
        response = self.client.get(
            reverse('accounting:audit_log_report'),
            {'search': '202015'}
        )
        
        self.assertEqual(response.status_code, 200)
        audit_logs = response.context['audit_logs']
        
        # Should find logs with object_id containing '202015'
        self.assertGreaterEqual(len(audit_logs), 1)


class DataIntegrityCheckTest(TestCase):
    """Test cases for data integrity check management command"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123',
            email='test@example.com'
        )
        
        # Create accounts
        self.cash_account = Account.objects.create(
            code='202003',
            name='Cash',
            account_type='asset',
            description='Cash on hand',
            created_by=self.user
        )
        
        self.loan_account = Account.objects.create(
            code='202001',
            name='Loan Portfolio',
            account_type='asset',
            description='Outstanding loans',
            created_by=self.user
        )
        
        # Create fiscal period
        self.period = FiscalPeriod.objects.create(
            name='January 2024',
            period_type='monthly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 1, 31),
            status='open'
        )
    
    def test_data_integrity_check_finds_unbalanced_entries(self):
        """Test that data integrity check finds unbalanced journal entries"""
        # Create an unbalanced journal entry
        journal_entry = JournalEntry.objects.create(
            reference_number='JE-UNBALANCED',
            transaction_date=date(2024, 1, 15),
            description='Unbalanced entry',
            status='draft',
            created_by=self.user
        )
        
        # Create unbalanced lines (debits != credits)
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=self.cash_account,
            description='Debit side',
            debit_amount=Decimal('1000.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=self.loan_account,
            description='Credit side',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('500.00'),  # Intentionally unbalanced
            line_number=2
        )
        
        # Run data integrity check
        out = StringIO()
        call_command('data_integrity_check', stdout=out)
        output = out.getvalue()
        
        # Check that unbalanced entry was found
        self.assertIn('unbalanced journal entries', output.lower())
        self.assertIn('JE-UNBALANCED', output)
    
    def test_data_integrity_check_passes_with_balanced_entries(self):
        """Test that data integrity check passes with balanced entries"""
        # Create a balanced journal entry
        journal_entry = JournalEntry.objects.create(
            reference_number='JE-BALANCED',
            transaction_date=date(2024, 1, 15),
            description='Balanced entry',
            status='draft',
            created_by=self.user
        )
        
        # Create balanced lines
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=self.cash_account,
            description='Debit side',
            debit_amount=Decimal('1000.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        JournalEntryLine.objects.create(
            journal_entry=journal_entry,
            account=self.loan_account,
            description='Credit side',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('1000.00'),
            line_number=2
        )
        
        # Run data integrity check
        out = StringIO()
        call_command('data_integrity_check', stdout=out)
        output = out.getvalue()
        
        # Check that no unbalanced entries were found
        self.assertIn('No unbalanced journal entries found', output)
    
    def test_data_integrity_check_command_runs_successfully(self):
        """Test that data integrity check command runs without errors"""
        # Run command
        out = StringIO()
        call_command('data_integrity_check', stdout=out)
        output = out.getvalue()
        
        # Check that command ran successfully
        self.assertIn('Starting data integrity check', output)
        self.assertIn('DATA INTEGRITY CHECK REPORT', output)
