"""
Performance tests for the accounting module.
Tests performance requirements as specified in Requirements 20.1-20.7.

Performance targets:
- Trial balance generates in < 5 seconds for 1 year of data (Req 20.1, 20.6)
- Journal entry posting completes in < 2 seconds (Req 20.7)
- Account list renders in < 1 second with 100 accounts (Req 20.5)
- Report caching reduces generation time on second load (Req 20.4)
"""

import time
from decimal import Decimal
from datetime import date, timedelta
from django.test import TestCase, TransactionTestCase
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.db import connection
from django.test.utils import override_settings

from accounting.models import (
    Account,
    JournalEntry,
    JournalEntryLine,
    GeneralLedger,
    FiscalPeriod
)
from accounting.services.accounting_service import AccountingService
from accounting.services.report_service import ReportService
from users.models import Branch

User = get_user_model()


class PerformanceTestCase(TransactionTestCase):
    """Base test case with performance measurement utilities."""
    
    def setUp(self):
        """Set up test data common to all performance tests."""
        # Clear cache before each test
        cache.clear()
        
        # Create test user
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123'
        )
        
        # Create test branch
        self.branch = Branch.objects.create(
            name='Test Branch',
            code='TB001',
            address='Test Address'
        )
        
        # Create fiscal period
        self.fiscal_period = FiscalPeriod.objects.create(
            name='Test Period 2024',
            period_type='annual',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 12, 31),
            status='open'
        )
    
    def measure_time(self, func, *args, **kwargs):
        """
        Measure execution time of a function.
        
        Returns:
            tuple: (result, elapsed_time_in_seconds)
        """
        start_time = time.time()
        result = func(*args, **kwargs)
        elapsed_time = time.time() - start_time
        return result, elapsed_time
    
    def count_queries(self, func, *args, **kwargs):
        """
        Count database queries executed by a function.
        
        Returns:
            tuple: (result, query_count)
        """
        # Reset query log
        connection.queries_log.clear()
        
        # Enable query logging
        from django.conf import settings
        original_debug = settings.DEBUG
        settings.DEBUG = True
        
        try:
            result = func(*args, **kwargs)
            query_count = len(connection.queries)
        finally:
            settings.DEBUG = original_debug
        
        return result, query_count


class TrialBalancePerformanceTest(PerformanceTestCase):
    """
    Test trial balance generation performance.
    Requirement 20.1, 20.2, 20.6: Should generate in < 5 seconds for 1 year of data.
    """
    
    def setUp(self):
        super().setUp()
        
        # Create 100 accounts (representative of real-world usage)
        self.accounts = []
        for i in range(100):
            account_type = ['asset', 'liability', 'equity', 'income', 'expense'][i % 5]
            account = Account.objects.create(
                code=f'{202001 + i}',
                name=f'Test Account {i}',
                account_type=account_type,
                description=f'Test account {i}',
                is_active=True,
                created_by=self.user
            )
            self.accounts.append(account)
    
    def test_trial_balance_generation_with_one_year_data(self):
        """
        Test trial balance generation with 1 year of transaction data.
        Validates: Requirement 20.1, 20.2, 20.6
        
        Target: < 5 seconds
        """
        # Create journal entries for 1 year (1000 entries = ~3 per day)
        start_date = date(2024, 1, 1)
        
        for i in range(1000):
            # Create journal entry
            transaction_date = start_date + timedelta(days=i % 365)
            entry = JournalEntry.objects.create(
                reference_number=f'JE-TEST-{i:04d}',
                transaction_date=transaction_date,
                description=f'Test entry {i}',
                branch=self.branch,
                created_by=self.user,
                status='draft'
            )
            
            # Create 2 lines (debit and credit)
            account1 = self.accounts[i % 50]  # Use first 50 accounts
            account2 = self.accounts[50 + (i % 50)]  # Use second 50 accounts
            amount = Decimal('1000.00') * (i % 10 + 1)
            
            JournalEntryLine.objects.create(
                journal_entry=entry,
                account=account1,
                description=f'Debit line {i}',
                debit_amount=amount,
                credit_amount=Decimal('0.00'),
                line_number=1
            )
            
            JournalEntryLine.objects.create(
                journal_entry=entry,
                account=account2,
                description=f'Credit line {i}',
                debit_amount=Decimal('0.00'),
                credit_amount=amount,
                line_number=2
            )
            
            # Post the entry
            accounting_service = AccountingService()
            accounting_service.post_journal_entry(entry, self.user)
        
        # Measure trial balance generation time
        report_service = ReportService()
        as_of_date = date(2024, 12, 31)
        
        result, elapsed_time = self.measure_time(
            report_service.generate_trial_balance,
            as_of_date,
            self.branch
        )
        
        # Assert performance requirement
        self.assertLess(
            elapsed_time,
            5.0,
            f'Trial balance generation took {elapsed_time:.2f}s, '
            f'should be < 5s for 1 year of data (Requirement 20.1, 20.6)'
        )
        
        # Validate result
        self.assertIsNotNone(result)
        self.assertIn('accounts', result)
        self.assertIn('totals', result)
        self.assertTrue(result['totals']['balanced'])
        
        print(f'\n[OK] Trial balance generated in {elapsed_time:.2f}s (target: < 5s)')
    
    def test_trial_balance_caching(self):
        """
        Test that trial balance caching reduces generation time on second load.
        Validates: Requirement 20.4
        """
        # Create some test data
        entry = JournalEntry.objects.create(
            reference_number='JE-CACHE-TEST',
            transaction_date=date(2024, 6, 1),
            description='Test entry for caching',
            branch=self.branch,
            created_by=self.user,
            status='draft'
        )
        
        JournalEntryLine.objects.create(
            journal_entry=entry,
            account=self.accounts[0],
            description='Debit line',
            debit_amount=Decimal('5000.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        JournalEntryLine.objects.create(
            journal_entry=entry,
            account=self.accounts[1],
            description='Credit line',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('5000.00'),
            line_number=2
        )
        
        accounting_service = AccountingService()
        accounting_service.post_journal_entry(entry, self.user)
        
        # First generation (uncached)
        report_service = ReportService()
        as_of_date = date(2024, 12, 31)
        
        result1, time1 = self.measure_time(
            report_service.generate_trial_balance,
            as_of_date,
            self.branch
        )
        
        # Second generation (should be cached)
        result2, time2 = self.measure_time(
            report_service.generate_trial_balance,
            as_of_date,
            self.branch
        )
        
        # Assert caching improves performance
        self.assertLess(
            time2,
            time1,
            f'Cached generation ({time2:.4f}s) should be faster than '
            f'initial generation ({time1:.4f}s) - Requirement 20.4'
        )
        
        # Assert results are identical
        self.assertEqual(result1['totals'], result2['totals'])
        
        print(f'\n[OK] Trial balance caching working:')
        print(f'  Initial: {time1:.4f}s')
        print(f'  Cached: {time2:.4f}s')
        print(f'  Improvement: {((time1 - time2) / time1 * 100):.1f}%')


class JournalEntryPostingPerformanceTest(PerformanceTestCase):
    """
    Test journal entry posting performance.
    Requirement 20.7: Should complete in < 2 seconds.
    """
    
    def setUp(self):
        super().setUp()
        
        # Create test accounts
        self.cash_account = Account.objects.create(
            code='202001',
            name='Cash Account',
            account_type='asset',
            description='Cash account for testing',
            is_active=True,
            created_by=self.user
        )
        
        self.revenue_account = Account.objects.create(
            code='401001',
            name='Revenue Account',
            account_type='income',
            description='Revenue account for testing',
            is_active=True,
            created_by=self.user
        )
    
    def test_journal_entry_posting_speed(self):
        """
        Test journal entry posting completes in < 2 seconds.
        Validates: Requirement 20.7
        """
        # Create a journal entry with 10 lines (representative of complex entry)
        entry = JournalEntry.objects.create(
            reference_number='JE-PERF-TEST',
            transaction_date=date(2024, 6, 15),
            description='Performance test entry',
            branch=self.branch,
            created_by=self.user,
            status='draft'
        )
        
        # Create 10 line items (5 debits, 5 credits)
        total_amount = Decimal('0.00')
        for i in range(5):
            amount = Decimal('1000.00') * (i + 1)
            total_amount += amount
            
            JournalEntryLine.objects.create(
                journal_entry=entry,
                account=self.cash_account,
                description=f'Debit line {i}',
                debit_amount=amount,
                credit_amount=Decimal('0.00'),
                line_number=i * 2 + 1
            )
            
            JournalEntryLine.objects.create(
                journal_entry=entry,
                account=self.revenue_account,
                description=f'Credit line {i}',
                debit_amount=Decimal('0.00'),
                credit_amount=amount,
                line_number=i * 2 + 2
            )
        
        # Measure posting time
        accounting_service = AccountingService()
        
        result, elapsed_time = self.measure_time(
            accounting_service.post_journal_entry,
            entry,
            self.user
        )
        
        # Assert performance requirement
        self.assertLess(
            elapsed_time,
            2.0,
            f'Journal entry posting took {elapsed_time:.2f}s, '
            f'should be < 2s (Requirement 20.7)'
        )
        
        # Validate posting succeeded
        self.assertTrue(result)
        entry.refresh_from_db()
        self.assertEqual(entry.status, 'posted')
        
        # Verify ledger entries created
        ledger_count = GeneralLedger.objects.filter(journal_entry=entry).count()
        self.assertEqual(ledger_count, 10)
        
        print(f'\n[OK] Journal entry posting completed in {elapsed_time:.4f}s (target: < 2s)')


class AccountListPerformanceTest(PerformanceTestCase):
    """
    Test account list view performance.
    Requirement 20.5: Should render in < 1 second with 100 accounts.
    """
    
    def test_account_list_with_100_accounts(self):
        """
        Test account list renders in < 1 second with 100 accounts.
        Validates: Requirement 20.5
        """
        # Create 100 accounts
        for i in range(100):
            account_type = ['asset', 'liability', 'equity', 'income', 'expense'][i % 5]
            Account.objects.create(
                code=f'{202001 + i}',
                name=f'Account {i}',
                account_type=account_type,
                description=f'Account {i} description',
                is_active=True,
                created_by=self.user
            )
        
        # Measure query time for account list with optimizations
        def fetch_accounts():
            return list(
                Account.objects.select_related('parent_account', 'created_by')
                .only(
                    'id', 'code', 'name', 'account_type', 'subtype', 'is_active',
                    'is_system_account', 'created_at',
                    'parent_account__code', 'parent_account__name',
                    'created_by__username'
                )
                .all()
            )
        
        result, elapsed_time = self.measure_time(fetch_accounts)
        
        # Assert performance requirement
        self.assertLess(
            elapsed_time,
            1.0,
            f'Account list query took {elapsed_time:.2f}s, '
            f'should be < 1s with 100 accounts (Requirement 20.5)'
        )
        
        # Validate result
        self.assertEqual(len(result), 100)
        
        print(f'\n[OK] Account list fetched in {elapsed_time:.4f}s (target: < 1s)')


class QueryOptimizationTest(PerformanceTestCase):
    """
    Test query optimizations (select_related, prefetch_related, aggregate).
    Validates: Requirements 20.1, 20.2
    """
    
    def setUp(self):
        super().setUp()
        
        # Create accounts
        self.accounts = []
        for i in range(10):
            account = Account.objects.create(
                code=f'{202001 + i}',
                name=f'Account {i}',
                account_type='asset',
                description=f'Account {i}',
                is_active=True,
                created_by=self.user
            )
            self.accounts.append(account)
        
        # Create journal entries
        for i in range(5):
            entry = JournalEntry.objects.create(
                reference_number=f'JE-{i:04d}',
                transaction_date=date(2024, 6, 1) + timedelta(days=i),
                description=f'Entry {i}',
                branch=self.branch,
                created_by=self.user,
                status='posted',
                posted_by=self.user,
                posted_at=date(2024, 6, 1)
            )
            
            # Create lines
            JournalEntryLine.objects.create(
                journal_entry=entry,
                account=self.accounts[i % 10],
                description='Debit',
                debit_amount=Decimal('1000.00'),
                credit_amount=Decimal('0.00'),
                line_number=1
            )
            
            JournalEntryLine.objects.create(
                journal_entry=entry,
                account=self.accounts[(i + 1) % 10],
                description='Credit',
                debit_amount=Decimal('0.00'),
                credit_amount=Decimal('1000.00'),
                line_number=2
            )
    
    def test_select_related_optimization(self):
        """
        Test select_related reduces query count for foreign keys.
        Validates: Requirement 20.1
        """
        # Without select_related
        def fetch_without_optimization():
            entries = list(JournalEntry.objects.all())
            # Access foreign keys (triggers additional queries)
            for entry in entries:
                _ = entry.branch
                _ = entry.created_by
                _ = entry.posted_by
            return entries
        
        result1, query_count1 = self.count_queries(fetch_without_optimization)
        
        # With select_related
        def fetch_with_optimization():
            entries = list(
                JournalEntry.objects.select_related(
                    'branch', 'created_by', 'posted_by'
                ).all()
            )
            # Access foreign keys (should not trigger additional queries)
            for entry in entries:
                _ = entry.branch
                _ = entry.created_by
                _ = entry.posted_by
            return entries
        
        result2, query_count2 = self.count_queries(fetch_with_optimization)
        
        # Assert optimization reduces queries
        self.assertLess(
            query_count2,
            query_count1,
            f'select_related should reduce queries. '
            f'Without: {query_count1}, With: {query_count2} (Requirement 20.1)'
        )
        
        print(f'\n[OK] select_related optimization:')
        print(f'  Without: {query_count1} queries')
        print(f'  With: {query_count2} queries')
        print(f'  Reduction: {query_count1 - query_count2} queries')
    
    def test_prefetch_related_optimization(self):
        """
        Test prefetch_related reduces query count for reverse foreign keys.
        Validates: Requirement 20.2
        """
        # Without prefetch_related
        def fetch_without_optimization():
            entries = list(JournalEntry.objects.all())
            # Access reverse foreign keys (triggers additional queries)
            for entry in entries:
                _ = list(entry.lines.all())
            return entries
        
        result1, query_count1 = self.count_queries(fetch_without_optimization)
        
        # With prefetch_related
        def fetch_with_optimization():
            entries = list(
                JournalEntry.objects.prefetch_related('lines').all()
            )
            # Access reverse foreign keys (should not trigger additional queries)
            for entry in entries:
                _ = list(entry.lines.all())
            return entries
        
        result2, query_count2 = self.count_queries(fetch_with_optimization)
        
        # Assert optimization reduces queries
        self.assertLess(
            query_count2,
            query_count1,
            f'prefetch_related should reduce queries. '
            f'Without: {query_count1}, With: {query_count2} (Requirement 20.2)'
        )
        
        print(f'\n[OK] prefetch_related optimization:')
        print(f'  Without: {query_count1} queries')
        print(f'  With: {query_count2} queries')
        print(f'  Reduction: {query_count1 - query_count2} queries')
    
    def test_aggregate_optimization(self):
        """
        Test aggregate() performs calculations at database level.
        Validates: Requirement 20.1, 20.2
        """
        account = self.accounts[0]
        
        # Create ledger entries
        for i in range(100):
            GeneralLedger.objects.create(
                account=account,
                journal_entry=JournalEntry.objects.first(),
                journal_entry_line=JournalEntryLine.objects.first(),
                transaction_date=date(2024, 6, 1),
                description=f'Entry {i}',
                reference_number=f'REF-{i}',
                debit_amount=Decimal('100.00'),
                credit_amount=Decimal('0.00'),
                balance=Decimal('100.00') * (i + 1),
                posted_at=date(2024, 6, 1),
                posted_by=self.user
            )
        
        # Using aggregate (database-level calculation)
        def calculate_with_aggregate():
            from django.db.models import Sum
            result = GeneralLedger.objects.filter(account=account).aggregate(
                total_debits=Sum('debit_amount'),
                total_credits=Sum('credit_amount')
            )
            return result
        
        result, query_count = self.count_queries(calculate_with_aggregate)
        
        # Should only execute 1 query (aggregate at database level)
        self.assertEqual(
            query_count,
            1,
            f'aggregate() should use 1 query, used {query_count} (Requirement 20.1, 20.2)'
        )
        
        # Validate result
        self.assertEqual(result['total_debits'], Decimal('10000.00'))
        self.assertEqual(result['total_credits'], Decimal('0.00'))
        
        print(f'\n[OK] aggregate() optimization: 1 query for sum of 100 records')


class CachingPerformanceTest(PerformanceTestCase):
    """
    Test caching implementation and performance improvements.
    Validates: Requirement 20.4
    """
    
    def setUp(self):
        super().setUp()
        
        # Create test accounts
        self.accounts = []
        for i in range(10):
            account = Account.objects.create(
                code=f'{202001 + i}',
                name=f'Account {i}',
                account_type='asset',
                description=f'Account {i}',
                is_active=True,
                created_by=self.user
            )
            self.accounts.append(account)
    
    def test_chart_of_accounts_caching(self):
        """
        Test Chart of Accounts caching with 24-hour timeout.
        Validates: Requirement 20.4
        """
        accounting_service = AccountingService()
        
        # First call (uncached)
        result1, time1 = self.measure_time(
            accounting_service.get_chart_of_accounts,
            include_inactive=False
        )
        
        # Second call (should be cached)
        result2, time2 = self.measure_time(
            accounting_service.get_chart_of_accounts,
            include_inactive=False
        )
        
        # Assert caching improves performance
        self.assertLess(
            time2,
            time1,
            f'Chart of Accounts caching should improve performance. '
            f'Initial: {time1:.4f}s, Cached: {time2:.4f}s (Requirement 20.4)'
        )
        
        # Assert results are identical
        self.assertEqual(result1['total_accounts'], result2['total_accounts'])
        
        print(f'\n[OK] Chart of Accounts caching working:')
        print(f'  Initial: {time1:.4f}s')
        print(f'  Cached: {time2:.4f}s')
        print(f'  24-hour cache timeout configured')
    
    def test_account_balance_caching(self):
        """
        Test account balance caching with proper cache key format.
        Validates: Requirement 20.4
        
        Cache key format: "balance_{code}_{date}_{branch}"
        """
        account = self.accounts[0]
        as_of_date = date(2024, 6, 1)
        
        # Create a journal entry to give account a balance
        entry = JournalEntry.objects.create(
            reference_number='JE-BALANCE-TEST',
            transaction_date=as_of_date,
            description='Test entry',
            branch=self.branch,
            created_by=self.user,
            status='draft'
        )
        
        JournalEntryLine.objects.create(
            journal_entry=entry,
            account=account,
            description='Debit',
            debit_amount=Decimal('5000.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        JournalEntryLine.objects.create(
            journal_entry=entry,
            account=self.accounts[1],
            description='Credit',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('5000.00'),
            line_number=2
        )
        
        accounting_service = AccountingService()
        accounting_service.post_journal_entry(entry, self.user)
        
        # Clear cache to test fresh calculation
        cache.clear()
        
        # First calculation (uncached)
        result1, time1 = self.measure_time(
            accounting_service.calculate_account_balance,
            account,
            as_of_date,
            self.branch
        )
        
        # Second calculation (should be cached)
        result2, time2 = self.measure_time(
            accounting_service.calculate_account_balance,
            account,
            as_of_date,
            self.branch
        )
        
        # Assert caching improves performance
        self.assertLess(
            time2,
            time1,
            f'Account balance caching should improve performance. '
            f'Initial: {time1:.4f}s, Cached: {time2:.4f}s (Requirement 20.4)'
        )
        
        # Assert results are identical
        self.assertEqual(result1, result2)
        self.assertEqual(result1, Decimal('5000.00'))
        
        # Verify cache key format
        expected_cache_key = f"balance_{account.code}_{as_of_date}_{self.branch.id}"
        cached_value = cache.get(expected_cache_key)
        self.assertIsNotNone(
            cached_value,
            f'Cache key format should be "balance_{{code}}_{{date}}_{{branch}}" (Requirement 20.4)'
        )
        
        print(f'\n[OK] Account balance caching working:')
        print(f'  Initial: {time1:.4f}s')
        print(f'  Cached: {time2:.4f}s')
        print(f'  Cache key format: balance_{{code}}_{{date}}_{{branch}}')

