"""
Unit tests for accounting models.

Tests for task 1.2: Account model with hierarchical structure
Tests for task 1.3: JournalEntry model with reversal tracking
Tests for task 1.4: JournalEntryLine model for debit/credit lines
Tests for task 1.5: GeneralLedger model with running balances
Tests for task 1.6: FiscalPeriod model with opening/closing balances
Tests for task 1.7: AccountBalance model for cached balances
"""

from django.test import TestCase
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.utils import timezone
from decimal import Decimal
from datetime import date, datetime, timedelta

from accounting.models import (
    Account,
    JournalEntry,
    JournalEntryLine,
    GeneralLedger,
    FiscalPeriod,
    AccountBalance
)

User = get_user_model()


class AccountModelTest(TestCase):
    """Test cases for Account model (Task 1.2)"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123',
            email='test@example.com'
        )
    
    def test_create_account_with_all_required_fields(self):
        """Test creating an account with all required fields"""
        account = Account.objects.create(
            code='202001',
            name='Loan Portfolio',
            account_type='asset',
            subtype='loan_portfolio',
            description='Outstanding loan balances',
            is_active=True,
            is_system_account=True,
            created_by=self.user
        )
        
        self.assertEqual(account.code, '202001')
        self.assertEqual(account.name, 'Loan Portfolio')
        self.assertEqual(account.account_type, 'asset')
        self.assertEqual(account.subtype, 'loan_portfolio')
        self.assertEqual(account.description, 'Outstanding loan balances')
        self.assertTrue(account.is_active)
        self.assertTrue(account.is_system_account)
        self.assertEqual(account.created_by, self.user)
        self.assertIsNotNone(account.created_at)
        self.assertIsNotNone(account.updated_at)
    
    def test_account_code_must_be_unique(self):
        """Test that account code must be unique"""
        Account.objects.create(
            code='202001',
            name='Loan Portfolio',
            account_type='asset',
            description='Test account'
        )
        
        # Attempting to create another account with same code should fail
        with self.assertRaises(IntegrityError):
            Account.objects.create(
                code='202001',
                name='Duplicate Account',
                account_type='asset',
                description='Duplicate'
            )
    
    def test_account_types_choices(self):
        """Test that all account types are available"""
        account_types = ['asset', 'liability', 'equity', 'income', 'expense']
        
        for acc_type in account_types:
            account = Account.objects.create(
                code=f'20200{account_types.index(acc_type) + 1}',
                name=f'Test {acc_type}',
                account_type=acc_type,
                description=f'Test {acc_type} account'
            )
            self.assertEqual(account.account_type, acc_type)
    
    def test_asset_subtypes_choices(self):
        """Test that asset subtypes are available"""
        subtypes = ['current_asset', 'loan_portfolio', 'fixed_asset', 'other_asset']
        
        for subtype in subtypes:
            account = Account.objects.create(
                code=f'20201{subtypes.index(subtype) + 1}',
                name=f'Test {subtype}',
                account_type='asset',
                subtype=subtype,
                description=f'Test {subtype} account'
            )
            self.assertEqual(account.subtype, subtype)
    
    def test_liability_subtypes_choices(self):
        """Test that liability subtypes are available"""
        subtypes = ['current_liability', 'client_savings', 'borrowings', 'other_liability']
        
        for subtype in subtypes:
            account = Account.objects.create(
                code=f'20202{subtypes.index(subtype) + 1}',
                name=f'Test {subtype}',
                account_type='liability',
                subtype=subtype,
                description=f'Test {subtype} account'
            )
            self.assertEqual(account.subtype, subtype)
    
    def test_hierarchical_parent_child_relationship(self):
        """Test parent-child account hierarchy"""
        # Create parent account
        parent = Account.objects.create(
            code='202001',
            name='Assets',
            account_type='asset',
            description='Parent asset account'
        )
        
        # Create child account
        child = Account.objects.create(
            code='202002',
            name='Current Assets',
            account_type='asset',
            description='Child asset account',
            parent_account=parent
        )
        
        self.assertEqual(child.parent_account, parent)
        self.assertIn(child, parent.child_accounts.all())
    
    def test_multiple_child_accounts(self):
        """Test that a parent can have multiple children"""
        parent = Account.objects.create(
            code='202001',
            name='Assets',
            account_type='asset',
            description='Parent asset account'
        )
        
        # Create multiple child accounts
        child1 = Account.objects.create(
            code='202002',
            name='Current Assets',
            account_type='asset',
            description='Child 1',
            parent_account=parent
        )
        
        child2 = Account.objects.create(
            code='202003',
            name='Fixed Assets',
            account_type='asset',
            description='Child 2',
            parent_account=parent
        )
        
        self.assertEqual(parent.child_accounts.count(), 2)
        self.assertIn(child1, parent.child_accounts.all())
        self.assertIn(child2, parent.child_accounts.all())
    
    def test_account_is_active_flag(self):
        """Test is_active flag defaults to True"""
        account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
        self.assertTrue(account.is_active)
        
        # Test setting inactive
        account.is_active = False
        account.save()
        account.refresh_from_db()
        self.assertFalse(account.is_active)
    
    def test_account_is_system_account_flag(self):
        """Test is_system_account flag defaults to False"""
        account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
        self.assertFalse(account.is_system_account)
        
        # Test setting as system account
        account.is_system_account = True
        account.save()
        account.refresh_from_db()
        self.assertTrue(account.is_system_account)
    
    def test_account_audit_fields(self):
        """Test audit fields: created_by, created_at, updated_at"""
        account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test',
            created_by=self.user
        )
        
        self.assertEqual(account.created_by, self.user)
        self.assertIsNotNone(account.created_at)
        self.assertIsNotNone(account.updated_at)
        
        # Test that updated_at changes on save
        original_updated_at = account.updated_at
        account.name = 'Updated Account'
        account.save()
        self.assertGreater(account.updated_at, original_updated_at)
    
    def test_account_str_representation(self):
        """Test string representation of account"""
        account = Account.objects.create(
            code='202001',
            name='Loan Portfolio',
            account_type='asset',
            description='Test'
        )
        self.assertEqual(str(account), '202001 - Loan Portfolio')
    
    def test_account_ordering_by_code(self):
        """Test that accounts are ordered by code"""
        Account.objects.create(code='202003', name='Account 3', account_type='asset', description='Test')
        Account.objects.create(code='202001', name='Account 1', account_type='asset', description='Test')
        Account.objects.create(code='202002', name='Account 2', account_type='asset', description='Test')
        
        accounts = Account.objects.all()
        self.assertEqual(accounts[0].code, '202001')
        self.assertEqual(accounts[1].code, '202002')
        self.assertEqual(accounts[2].code, '202003')
    
    def test_account_db_table_name(self):
        """Test that db_table is set to 'accounting_accounts'"""
        self.assertEqual(Account._meta.db_table, 'accounting_accounts')
    
    def test_account_indexes_exist(self):
        """Test that required database indexes exist"""
        indexes = Account._meta.indexes
        
        # Check that we have indexes defined
        self.assertGreater(len(indexes), 0, "Account model should have indexes defined")
        
        # Check for account_type, is_active index by examining fields
        has_account_type_index = any(
            'account_type' in str(index.fields) for index in indexes
        )
        self.assertTrue(has_account_type_index, "Should have index on account_type")
        
        # Check for parent_account, is_active index
        has_parent_account_index = any(
            'parent_account' in str(index.fields) for index in indexes
        )
        self.assertTrue(has_parent_account_index, "Should have index on parent_account")
    
    def test_account_code_is_indexed(self):
        """Test that code field has db_index=True"""
        code_field = Account._meta.get_field('code')
        self.assertTrue(code_field.db_index)
    
    def test_account_subtype_is_optional(self):
        """Test that subtype field is optional (blank=True, null=True)"""
        account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test without subtype'
        )
        self.assertIsNone(account.subtype)


class JournalEntryModelTest(TestCase):
    """Test cases for JournalEntry model (Task 1.3)"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123',
            email='test@example.com'
        )
    
    def test_create_journal_entry(self):
        """Test creating a journal entry with required fields"""
        entry = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date(2024, 1, 15),
            description='Test journal entry',
            created_by=self.user
        )
        
        self.assertEqual(entry.reference_number, 'JE-2024-001')
        self.assertEqual(entry.transaction_date, date(2024, 1, 15))
        self.assertEqual(entry.description, 'Test journal entry')
        self.assertEqual(entry.status, 'draft')  # default status
        self.assertEqual(entry.created_by, self.user)
    
    def test_journal_entry_status_choices(self):
        """Test journal entry status choices"""
        statuses = ['draft', 'posted', 'reversed']
        
        for status in statuses:
            entry = JournalEntry.objects.create(
                reference_number=f'JE-{status}',
                transaction_date=date.today(),
                description=f'Test {status}',
                status=status
            )
            self.assertEqual(entry.status, status)
    
    def test_journal_entry_audit_fields(self):
        """Test audit trail fields"""
        entry = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date.today(),
            description='Test',
            created_by=self.user
        )
        
        self.assertIsNotNone(entry.created_at)
        self.assertIsNotNone(entry.updated_at)
        self.assertEqual(entry.created_by, self.user)
        self.assertIsNone(entry.posted_by)
        self.assertIsNone(entry.posted_at)
    
    def test_journal_entry_str_representation(self):
        """Test string representation"""
        entry = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date(2024, 1, 15),
            description='Test'
        )
        self.assertEqual(str(entry), 'JE-2024-001 - 2024-01-15')
    
    def test_journal_entry_db_table_name(self):
        """Test db_table is set correctly"""
        self.assertEqual(JournalEntry._meta.db_table, 'accounting_journal_entries')


class JournalEntryLineModelTest(TestCase):
    """Test cases for JournalEntryLine model (Task 1.4)"""
    
    def setUp(self):
        """Set up test data"""
        self.account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
        
        self.entry = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date.today(),
            description='Test entry'
        )
    
    def test_create_journal_entry_line_with_debit(self):
        """Test creating a line with debit amount"""
        line = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Debit entry',
            debit_amount=Decimal('1000.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        self.assertEqual(line.journal_entry, self.entry)
        self.assertEqual(line.account, self.account)
        self.assertEqual(line.debit_amount, Decimal('1000.00'))
        self.assertEqual(line.credit_amount, Decimal('0.00'))
        self.assertEqual(line.line_number, 1)
    
    def test_create_journal_entry_line_with_credit(self):
        """Test creating a line with credit amount"""
        line = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Credit entry',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('1000.00'),
            line_number=2
        )
        
        self.assertEqual(line.credit_amount, Decimal('1000.00'))
        self.assertEqual(line.debit_amount, Decimal('0.00'))
    
    def test_journal_entry_line_default_amounts(self):
        """Test default values for debit and credit amounts"""
        line = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Test',
            line_number=1
        )
        
        self.assertEqual(line.debit_amount, Decimal('0.00'))
        self.assertEqual(line.credit_amount, Decimal('0.00'))
    
    def test_journal_entry_line_str_representation(self):
        """Test string representation"""
        line = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Test',
            line_number=1
        )
        self.assertEqual(str(line), f'{self.entry.reference_number} - Line 1')
    
    def test_journal_entry_line_db_table_name(self):
        """Test db_table is set correctly"""
        self.assertEqual(JournalEntryLine._meta.db_table, 'accounting_journal_entry_lines')


class GeneralLedgerModelTest(TestCase):
    """Test cases for GeneralLedger model (Task 1.5)"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        
        self.account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
        
        self.entry = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date.today(),
            description='Test entry'
        )
        
        self.line = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Test line',
            debit_amount=Decimal('1000.00'),
            line_number=1
        )
    
    def test_create_general_ledger_entry(self):
        """Test creating a general ledger entry"""
        ledger = GeneralLedger.objects.create(
            account=self.account,
            journal_entry=self.entry,
            journal_entry_line=self.line,
            transaction_date=date.today(),
            description='Test ledger entry',
            reference_number='JE-2024-001',
            debit_amount=Decimal('1000.00'),
            credit_amount=Decimal('0.00'),
            balance=Decimal('1000.00'),
            posted_at=datetime.now(),
            posted_by=self.user
        )
        
        self.assertEqual(ledger.account, self.account)
        self.assertEqual(ledger.journal_entry, self.entry)
        self.assertEqual(ledger.journal_entry_line, self.line)
        self.assertEqual(ledger.balance, Decimal('1000.00'))
    
    def test_general_ledger_db_table_name(self):
        """Test db_table is set correctly"""
        self.assertEqual(GeneralLedger._meta.db_table, 'accounting_general_ledger')


class FiscalPeriodModelTest(TestCase):
    """Test cases for FiscalPeriod model (Task 1.6)"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
    
    def test_create_fiscal_period(self):
        """Test creating a fiscal period"""
        period = FiscalPeriod.objects.create(
            name='January 2024',
            period_type='monthly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 1, 31),
            status='open'
        )
        
        self.assertEqual(period.name, 'January 2024')
        self.assertEqual(period.period_type, 'monthly')
        self.assertEqual(period.start_date, date(2024, 1, 1))
        self.assertEqual(period.end_date, date(2024, 1, 31))
        self.assertEqual(period.status, 'open')
    
    def test_fiscal_period_types(self):
        """Test period type choices"""
        types = ['monthly', 'quarterly', 'annual']
        
        for i, period_type in enumerate(types):
            period = FiscalPeriod.objects.create(
                name=f'Period {i+1}',
                period_type=period_type,
                start_date=date(2024, 1, 1),
                end_date=date(2024, 1, 31)
            )
            self.assertEqual(period.period_type, period_type)
    
    def test_fiscal_period_status_choices(self):
        """Test status choices"""
        statuses = ['open', 'closed']
        
        for i, status in enumerate(statuses):
            period = FiscalPeriod.objects.create(
                name=f'Period {i+1}',
                period_type='monthly',
                start_date=date(2024, i+1, 1),
                end_date=date(2024, i+1, 28),
                status=status
            )
            self.assertEqual(period.status, status)
    
    def test_fiscal_period_json_fields(self):
        """Test opening and closing balances JSONField"""
        period = FiscalPeriod.objects.create(
            name='Test Period',
            period_type='monthly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 1, 31)
        )
        
        # Default should be empty dict
        self.assertEqual(period.opening_balances, {})
        self.assertEqual(period.closing_balances, {})
        
        # Test setting JSON data
        period.opening_balances = {'202001': '1000.00', '202002': '500.00'}
        period.closing_balances = {'202001': '1500.00', '202002': '800.00'}
        period.save()
        
        period.refresh_from_db()
        self.assertEqual(period.opening_balances['202001'], '1000.00')
        self.assertEqual(period.closing_balances['202001'], '1500.00')
    
    def test_fiscal_period_db_table_name(self):
        """Test db_table is set correctly"""
        self.assertEqual(FiscalPeriod._meta.db_table, 'accounting_fiscal_periods')


class AccountBalanceModelTest(TestCase):
    """Test cases for AccountBalance model (Task 1.7)"""
    
    def setUp(self):
        """Set up test data"""
        self.account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
    
    def test_create_account_balance(self):
        """Test creating an account balance record"""
        balance = AccountBalance.objects.create(
            account=self.account,
            as_of_date=date.today(),
            debit_balance=Decimal('1000.00'),
            credit_balance=Decimal('500.00'),
            net_balance=Decimal('500.00')
        )
        
        self.assertEqual(balance.account, self.account)
        self.assertEqual(balance.debit_balance, Decimal('1000.00'))
        self.assertEqual(balance.credit_balance, Decimal('500.00'))
        self.assertEqual(balance.net_balance, Decimal('500.00'))
    
    def test_account_balance_unique_constraint(self):
        """Test unique_together constraint for account, branch, as_of_date"""
        AccountBalance.objects.create(
            account=self.account,
            branch=None,
            as_of_date=date.today(),
            debit_balance=Decimal('1000.00'),
            credit_balance=Decimal('0.00'),
            net_balance=Decimal('1000.00')
        )
        
        # Attempting to create duplicate should fail
        # Note: unique_together constraint is defined in Meta
        try:
            AccountBalance.objects.create(
                account=self.account,
                branch=None,
                as_of_date=date.today(),
                debit_balance=Decimal('2000.00'),
                credit_balance=Decimal('0.00'),
                net_balance=Decimal('2000.00')
            )
            # If no exception, check that constraint is defined
            self.assertIn('account', str(AccountBalance._meta.unique_together))
        except IntegrityError:
            # Expected behavior - constraint is working
            pass
    
    def test_account_balance_db_table_name(self):
        """Test db_table is set correctly"""
        self.assertEqual(AccountBalance._meta.db_table, 'accounting_account_balances')


# Additional comprehensive tests for task 1.8
class AccountValidationTest(TestCase):
    """Additional validation tests for Account model"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
    
    def test_account_code_starting_from_202001(self):
        """Test accounts can be created starting from code 202001 (Requirement 1.1)"""
        account = Account.objects.create(
            code='202001',
            name='First Account',
            account_type='asset',
            description='First account starting from 202001'
        )
        self.assertEqual(account.code, '202001')
        
        # Create additional accounts with sequential codes
        account2 = Account.objects.create(
            code='202002',
            name='Second Account',
            account_type='asset',
            description='Second account'
        )
        self.assertEqual(account2.code, '202002')
    
    def test_account_name_field_exists(self):
        """Test that account name field is defined"""
        account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
        self.assertEqual(account.name, 'Test Account')
        
        # Verify field is not nullable
        name_field = Account._meta.get_field('name')
        self.assertFalse(name_field.null)
    
    def test_account_code_field_exists(self):
        """Test that account code field is defined and required"""
        account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
        self.assertEqual(account.code, '202001')
        
        # Verify field is not nullable
        code_field = Account._meta.get_field('code')
        self.assertFalse(code_field.null)
    
    def test_account_type_field_exists(self):
        """Test that account type field is defined and required"""
        account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
        self.assertEqual(account.account_type, 'asset')
        
        # Verify field is not nullable
        account_type_field = Account._meta.get_field('account_type')
        self.assertFalse(account_type_field.null)
    
    def test_account_description_field_exists(self):
        """Test that description field is defined and required"""
        account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test description'
        )
        self.assertEqual(account.description, 'Test description')
        
        # Verify field is not nullable
        description_field = Account._meta.get_field('description')
        self.assertFalse(description_field.null)
    
    def test_cascading_delete_with_children(self):
        """Test that deleting parent deletes children (CASCADE relationship)"""
        parent = Account.objects.create(
            code='202001',
            name='Parent',
            account_type='asset',
            description='Parent account'
        )
        
        child = Account.objects.create(
            code='202002',
            name='Child',
            account_type='asset',
            description='Child account',
            parent_account=parent
        )
        
        parent_id = parent.id
        child_id = child.id
        
        parent.delete()
        
        # Verify both are deleted
        self.assertFalse(Account.objects.filter(id=parent_id).exists())
        self.assertFalse(Account.objects.filter(id=child_id).exists())
    
    def test_multiple_levels_hierarchy(self):
        """Test multiple levels of account hierarchy"""
        level1 = Account.objects.create(
            code='202001',
            name='Level 1',
            account_type='asset',
            description='Top level'
        )
        
        level2 = Account.objects.create(
            code='202002',
            name='Level 2',
            account_type='asset',
            description='Second level',
            parent_account=level1
        )
        
        level3 = Account.objects.create(
            code='202003',
            name='Level 3',
            account_type='asset',
            description='Third level',
            parent_account=level2
        )
        
        self.assertEqual(level3.parent_account, level2)
        self.assertEqual(level2.parent_account, level1)
        self.assertIsNone(level1.parent_account)


class JournalEntryValidationTest(TestCase):
    """Additional validation tests for JournalEntry model"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
    
    def test_reference_number_uniqueness(self):
        """Test that reference numbers must be unique"""
        JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date.today(),
            description='First entry'
        )
        
        with self.assertRaises(IntegrityError):
            JournalEntry.objects.create(
                reference_number='JE-2024-001',
                transaction_date=date.today(),
                description='Duplicate entry'
            )
    
    def test_reference_number_is_indexed(self):
        """Test that reference_number has db_index=True"""
        field = JournalEntry._meta.get_field('reference_number')
        self.assertTrue(field.db_index)
    
    def test_transaction_date_is_indexed(self):
        """Test that transaction_date has db_index=True"""
        field = JournalEntry._meta.get_field('transaction_date')
        self.assertTrue(field.db_index)
    
    def test_reversal_relationship(self):
        """Test reversal tracking OneToOne relationship"""
        original = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date.today(),
            description='Original entry'
        )
        
        reversal = JournalEntry.objects.create(
            reference_number='JE-2024-002',
            transaction_date=date.today(),
            description='Reversal entry',
            reverses=original
        )
        
        self.assertEqual(reversal.reverses, original)
        self.assertEqual(original.reversed_by, reversal)


# Validation tests for task 23.3
class ValidationUnitTest(TestCase):
    """
    Unit tests for data validation (Task 23.3).
    Tests Requirements 16.1-16.9
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123',
            email='test@example.com'
        )
        
        # Create a valid account
        self.account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Valid test account'
        )
        
        # Create a 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_account_clean_rejects_invalid_code_non_numeric(self):
        """
        Test Account.clean() rejects invalid codes (non-numeric).
        Requirements: 16.1, 16.2
        """
        account = Account(
            code='ABC123',
            name='Invalid Account',
            account_type='asset',
            description='Account with non-numeric code'
        )
        
        with self.assertRaises(ValidationError) as context:
            account.full_clean()
        
        self.assertIn('code', context.exception.message_dict)
        self.assertIn('numeric', str(context.exception))
    
    def test_account_clean_rejects_negative_code(self):
        """
        Test Account.clean() rejects negative or zero codes.
        Requirements: 16.1, 16.2
        """
        account = Account(
            code='0',
            name='Invalid Account',
            account_type='asset',
            description='Account with zero code'
        )
        
        with self.assertRaises(ValidationError) as context:
            account.full_clean()
        
        self.assertIn('code', context.exception.message_dict)
        self.assertIn('positive', str(context.exception))
    
    def test_account_clean_accepts_valid_code(self):
        """
        Test Account.clean() accepts valid codes.
        Requirements: 16.1, 16.2
        """
        account = Account(
            code='202050',
            name='Valid Account',
            account_type='asset',
            description='Account with valid code',
            created_by=self.user
        )
        
        # Should not raise ValidationError
        account.full_clean()
        account.save()
        
        self.assertEqual(account.code, '202050')
    
    def test_journal_entry_clean_rejects_future_date(self):
        """
        Test JournalEntry.clean() rejects future dates.
        Requirements: 16.5
        """
        future_date = date.today() + timedelta(days=10)
        
        entry = JournalEntry(
            reference_number='JE-FUTURE-001',
            transaction_date=future_date,
            description='Entry with future date'
        )
        
        with self.assertRaises(ValidationError) as context:
            entry.full_clean()
        
        self.assertIn('transaction_date', context.exception.message_dict)
        self.assertIn('future', str(context.exception))
    
    def test_journal_entry_clean_accepts_today(self):
        """
        Test JournalEntry.clean() accepts today's date.
        Requirements: 16.5
        """
        entry = JournalEntry(
            reference_number='JE-TODAY-001',
            transaction_date=date.today(),
            description='Entry with today\'s date',
            created_by=self.user,
            posted_by=self.user
        )
        
        # Should not raise ValidationError
        entry.clean()  # Only test clean(), not full_clean()
        entry.save()
        
        self.assertEqual(entry.transaction_date, date.today())
    
    def test_journal_entry_clean_accepts_past_date(self):
        """
        Test JournalEntry.clean() accepts past dates.
        Requirements: 16.5
        """
        past_date = date.today() - timedelta(days=10)
        
        entry = JournalEntry(
            reference_number='JE-PAST-001',
            transaction_date=past_date,
            description='Entry with past date',
            created_by=self.user,
            posted_by=self.user
        )
        
        # Should not raise ValidationError
        entry.clean()  # Only test clean(), not full_clean()
        entry.save()
        
        self.assertEqual(entry.transaction_date, past_date)
    
    def test_journal_entry_line_clean_rejects_both_debit_and_credit(self):
        """
        Test JournalEntryLine.clean() rejects both debit and credit.
        Requirements: 16.3, 16.7
        """
        entry = JournalEntry.objects.create(
            reference_number='JE-TEST-001',
            transaction_date=date.today(),
            description='Test entry'
        )
        
        line = JournalEntryLine(
            journal_entry=entry,
            account=self.account,
            description='Line with both debit and credit',
            debit_amount=Decimal('100.00'),
            credit_amount=Decimal('50.00'),
            line_number=1
        )
        
        with self.assertRaises(ValidationError) as context:
            line.full_clean()
        
        error_message = str(context.exception)
        self.assertIn('both debit and credit', error_message)
    
    def test_journal_entry_line_clean_accepts_debit_only(self):
        """
        Test JournalEntryLine.clean() accepts debit only.
        Requirements: 16.3, 16.7
        """
        entry = JournalEntry.objects.create(
            reference_number='JE-TEST-002',
            transaction_date=date.today(),
            description='Test entry'
        )
        
        line = JournalEntryLine(
            journal_entry=entry,
            account=self.account,
            description='Line with debit only',
            debit_amount=Decimal('100.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        # Should not raise ValidationError
        line.full_clean()
        line.save()
        
        self.assertEqual(line.debit_amount, Decimal('100.00'))
        self.assertEqual(line.credit_amount, Decimal('0.00'))
    
    def test_journal_entry_line_clean_accepts_credit_only(self):
        """
        Test JournalEntryLine.clean() accepts credit only.
        Requirements: 16.3, 16.7
        """
        entry = JournalEntry.objects.create(
            reference_number='JE-TEST-003',
            transaction_date=date.today(),
            description='Test entry'
        )
        
        line = JournalEntryLine(
            journal_entry=entry,
            account=self.account,
            description='Line with credit only',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('100.00'),
            line_number=1
        )
        
        # Should not raise ValidationError
        line.full_clean()
        line.save()
        
        self.assertEqual(line.debit_amount, Decimal('0.00'))
        self.assertEqual(line.credit_amount, Decimal('100.00'))
    
    def test_journal_entry_line_validates_positive_debit(self):
        """
        Test JournalEntryLine validates positive debit amounts.
        Requirements: 16.3
        """
        entry = JournalEntry.objects.create(
            reference_number='JE-TEST-004',
            transaction_date=date.today(),
            description='Test entry'
        )
        
        line = JournalEntryLine(
            journal_entry=entry,
            account=self.account,
            description='Line with negative debit',
            debit_amount=Decimal('-100.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        with self.assertRaises(ValidationError) as context:
            line.full_clean()
        
        self.assertIn('debit_amount', context.exception.message_dict)
    
    def test_journal_entry_line_validates_positive_credit(self):
        """
        Test JournalEntryLine validates positive credit amounts.
        Requirements: 16.3
        """
        entry = JournalEntry.objects.create(
            reference_number='JE-TEST-005',
            transaction_date=date.today(),
            description='Test entry'
        )
        
        line = JournalEntryLine(
            journal_entry=entry,
            account=self.account,
            description='Line with negative credit',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('-100.00'),
            line_number=1
        )
        
        with self.assertRaises(ValidationError) as context:
            line.full_clean()
        
        self.assertIn('credit_amount', context.exception.message_dict)
    
    def test_journal_entry_line_validates_max_two_decimal_places_debit(self):
        """
        Test JournalEntryLine validates max 2 decimal places for debit.
        Requirements: 16.3
        """
        entry = JournalEntry.objects.create(
            reference_number='JE-TEST-006',
            transaction_date=date.today(),
            description='Test entry'
        )
        
        line = JournalEntryLine(
            journal_entry=entry,
            account=self.account,
            description='Line with too many decimal places',
            debit_amount=Decimal('100.123'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        with self.assertRaises(ValidationError) as context:
            line.full_clean()
        
        self.assertIn('debit_amount', context.exception.message_dict)
        self.assertIn('2 decimal places', str(context.exception))
    
    def test_journal_entry_line_validates_max_two_decimal_places_credit(self):
        """
        Test JournalEntryLine validates max 2 decimal places for credit.
        Requirements: 16.3
        """
        entry = JournalEntry.objects.create(
            reference_number='JE-TEST-007',
            transaction_date=date.today(),
            description='Test entry'
        )
        
        line = JournalEntryLine(
            journal_entry=entry,
            account=self.account,
            description='Line with too many decimal places',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('100.999'),
            line_number=1
        )
        
        with self.assertRaises(ValidationError) as context:
            line.full_clean()
        
        self.assertIn('credit_amount', context.exception.message_dict)
        self.assertIn('2 decimal places', str(context.exception))
    
    def test_custom_validator_positive_decimal(self):
        """
        Test validate_positive_decimal custom validator.
        Requirements: 16.3
        """
        from accounting.models import validate_positive_decimal
        
        # Valid positive value
        validate_positive_decimal(Decimal('100.00'))
        
        # Invalid negative value
        with self.assertRaises(ValidationError) as context:
            validate_positive_decimal(Decimal('-100.00'))
        
        self.assertIn('positive', str(context.exception))
    
    def test_custom_validator_two_decimal_places(self):
        """
        Test validate_two_decimal_places custom validator.
        Requirements: 16.3
        """
        from accounting.models import validate_two_decimal_places
        
        # Valid 2 decimal places
        validate_two_decimal_places(Decimal('100.00'))
        validate_two_decimal_places(Decimal('100.5'))
        
        # Invalid 3 decimal places
        with self.assertRaises(ValidationError) as context:
            validate_two_decimal_places(Decimal('100.123'))
        
        self.assertIn('2 decimal places', str(context.exception))
    
    def test_posted_status_with_posted_by_and_posted_at(self):
        """Test that posted entries can have posted_by and posted_at"""
        entry = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date.today(),
            description='Test entry',
            status='posted',
            created_by=self.user,
            posted_by=self.user,
            posted_at=datetime.now()
        )
        
        self.assertEqual(entry.status, 'posted')
        self.assertEqual(entry.posted_by, self.user)
        self.assertIsNotNone(entry.posted_at)
    
    def test_journal_entry_ordering(self):
        """Test that journal entries are ordered by transaction_date desc, created_at desc"""
        entry1 = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date(2024, 1, 1),
            description='First'
        )
        
        entry2 = JournalEntry.objects.create(
            reference_number='JE-2024-002',
            transaction_date=date(2024, 1, 15),
            description='Second'
        )
        
        entry3 = JournalEntry.objects.create(
            reference_number='JE-2024-003',
            transaction_date=date(2024, 1, 10),
            description='Third'
        )
        
        entries = list(JournalEntry.objects.all())
        self.assertEqual(entries[0].reference_number, 'JE-2024-002')
        self.assertEqual(entries[1].reference_number, 'JE-2024-003')
        self.assertEqual(entries[2].reference_number, 'JE-2024-001')


class JournalEntryLineValidationTest(TestCase):
    """Additional validation tests for JournalEntryLine model"""
    
    def setUp(self):
        """Set up test data"""
        self.account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
        
        self.entry = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date.today(),
            description='Test entry'
        )
    
    def test_decimal_precision(self):
        """Test that amounts have correct decimal precision (15, 2)"""
        line = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Test precision',
            debit_amount=Decimal('9999999999999.99'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        self.assertEqual(line.debit_amount, Decimal('9999999999999.99'))
    
    def test_multiple_lines_per_entry(self):
        """Test multiple lines can be created for one journal entry"""
        line1 = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Line 1',
            debit_amount=Decimal('1000.00'),
            line_number=1
        )
        
        account2 = Account.objects.create(
            code='202002',
            name='Second Account',
            account_type='asset',
            description='Test'
        )
        
        line2 = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=account2,
            description='Line 2',
            credit_amount=Decimal('1000.00'),
            line_number=2
        )
        
        self.assertEqual(self.entry.lines.count(), 2)
        self.assertIn(line1, self.entry.lines.all())
        self.assertIn(line2, self.entry.lines.all())
    
    def test_line_ordering(self):
        """Test that lines are ordered by journal_entry and line_number"""
        line2 = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Line 2',
            debit_amount=Decimal('500.00'),
            line_number=2
        )
        
        line1 = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Line 1',
            debit_amount=Decimal('1000.00'),
            line_number=1
        )
        
        lines = list(self.entry.lines.all())
        self.assertEqual(lines[0].line_number, 1)
        self.assertEqual(lines[1].line_number, 2)
    
    def test_cascading_delete_with_journal_entry(self):
        """Test that deleting journal entry deletes lines (CASCADE)"""
        line = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Test',
            debit_amount=Decimal('1000.00'),
            line_number=1
        )
        
        line_id = line.id
        self.entry.delete()
        
        self.assertFalse(JournalEntryLine.objects.filter(id=line_id).exists())
    
    def test_protect_account_deletion(self):
        """Test that account cannot be deleted if used in journal lines (PROTECT)"""
        line = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Test',
            debit_amount=Decimal('1000.00'),
            line_number=1
        )
        
        from django.db.models import ProtectedError
        with self.assertRaises(ProtectedError):
            self.account.delete()


class GeneralLedgerValidationTest(TestCase):
    """Additional validation tests for GeneralLedger model"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        
        self.account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
        
        self.entry = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date.today(),
            description='Test entry'
        )
        
        self.line = JournalEntryLine.objects.create(
            journal_entry=self.entry,
            account=self.account,
            description='Test line',
            debit_amount=Decimal('1000.00'),
            line_number=1
        )
    
    def test_balance_calculation_for_asset_account(self):
        """Test balance increases with debits for asset accounts"""
        ledger1 = GeneralLedger.objects.create(
            account=self.account,
            journal_entry=self.entry,
            journal_entry_line=self.line,
            transaction_date=date.today(),
            description='First entry',
            reference_number='JE-2024-001',
            debit_amount=Decimal('1000.00'),
            credit_amount=Decimal('0.00'),
            balance=Decimal('1000.00'),
            posted_at=datetime.now(),
            posted_by=self.user
        )
        
        self.assertEqual(ledger1.balance, Decimal('1000.00'))
        
        # Create second entry
        entry2 = JournalEntry.objects.create(
            reference_number='JE-2024-002',
            transaction_date=date.today(),
            description='Second entry'
        )
        
        line2 = JournalEntryLine.objects.create(
            journal_entry=entry2,
            account=self.account,
            description='Second line',
            debit_amount=Decimal('500.00'),
            line_number=1
        )
        
        ledger2 = GeneralLedger.objects.create(
            account=self.account,
            journal_entry=entry2,
            journal_entry_line=line2,
            transaction_date=date.today(),
            description='Second entry',
            reference_number='JE-2024-002',
            debit_amount=Decimal('500.00'),
            credit_amount=Decimal('0.00'),
            balance=Decimal('1500.00'),  # Previous 1000 + 500
            posted_at=datetime.now(),
            posted_by=self.user
        )
        
        self.assertEqual(ledger2.balance, Decimal('1500.00'))
    
    def test_balance_calculation_for_liability_account(self):
        """Test balance increases with credits for liability accounts"""
        liability_account = Account.objects.create(
            code='302001',
            name='Test Liability',
            account_type='liability',
            description='Test'
        )
        
        entry = JournalEntry.objects.create(
            reference_number='JE-2024-LIA-001',
            transaction_date=date.today(),
            description='Liability entry'
        )
        
        line = JournalEntryLine.objects.create(
            journal_entry=entry,
            account=liability_account,
            description='Credit liability',
            credit_amount=Decimal('2000.00'),
            line_number=1
        )
        
        ledger = GeneralLedger.objects.create(
            account=liability_account,
            journal_entry=entry,
            journal_entry_line=line,
            transaction_date=date.today(),
            description='Credit to liability',
            reference_number='JE-2024-LIA-001',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('2000.00'),
            balance=Decimal('2000.00'),  # Credits increase liability
            posted_at=datetime.now(),
            posted_by=self.user
        )
        
        self.assertEqual(ledger.balance, Decimal('2000.00'))
    
    def test_general_ledger_ordering(self):
        """Test general ledger entries are ordered correctly"""
        # The ordering should be: account, transaction_date, posted_at
        ordering = GeneralLedger._meta.ordering
        self.assertEqual(ordering, ['account', 'transaction_date', 'posted_at'])
    
    def test_protect_account_deletion_from_ledger(self):
        """Test that accounts with ledger entries cannot be deleted"""
        GeneralLedger.objects.create(
            account=self.account,
            journal_entry=self.entry,
            journal_entry_line=self.line,
            transaction_date=date.today(),
            description='Test',
            reference_number='JE-2024-001',
            debit_amount=Decimal('1000.00'),
            credit_amount=Decimal('0.00'),
            balance=Decimal('1000.00'),
            posted_at=datetime.now(),
            posted_by=self.user
        )
        
        from django.db.models import ProtectedError
        with self.assertRaises(ProtectedError):
            self.account.delete()
    
    def test_reference_number_is_indexed(self):
        """Test that reference_number field is indexed"""
        field = GeneralLedger._meta.get_field('reference_number')
        self.assertTrue(field.db_index)
    
    def test_transaction_date_is_indexed(self):
        """Test that transaction_date field is indexed"""
        field = GeneralLedger._meta.get_field('transaction_date')
        self.assertTrue(field.db_index)
    
    def test_posted_at_is_indexed(self):
        """Test that posted_at field is indexed"""
        field = GeneralLedger._meta.get_field('posted_at')
        self.assertTrue(field.db_index)


class FiscalPeriodValidationTest(TestCase):
    """Additional validation tests for FiscalPeriod model"""
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
    
    def test_fiscal_period_name_uniqueness(self):
        """Test that fiscal period names must be unique"""
        FiscalPeriod.objects.create(
            name='January 2024',
            period_type='monthly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 1, 31)
        )
        
        with self.assertRaises(IntegrityError):
            FiscalPeriod.objects.create(
                name='January 2024',
                period_type='monthly',
                start_date=date(2024, 1, 1),
                end_date=date(2024, 1, 31)
            )
    
    def test_fiscal_period_date_ranges(self):
        """Test various fiscal period date ranges"""
        # Monthly period
        monthly = FiscalPeriod.objects.create(
            name='Jan 2024',
            period_type='monthly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 1, 31)
        )
        self.assertEqual((monthly.end_date - monthly.start_date).days, 30)
        
        # Quarterly period
        quarterly = FiscalPeriod.objects.create(
            name='Q1 2024',
            period_type='quarterly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 3, 31)
        )
        self.assertEqual((quarterly.end_date - quarterly.start_date).days, 90)
        
        # Annual period
        annual = FiscalPeriod.objects.create(
            name='FY 2024',
            period_type='annual',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 12, 31)
        )
        self.assertEqual((annual.end_date - annual.start_date).days, 365)
    
    def test_fiscal_period_closing(self):
        """Test closing a fiscal period"""
        period = FiscalPeriod.objects.create(
            name='Test Period',
            period_type='monthly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 1, 31),
            status='open'
        )
        
        # Close the period
        period.status = 'closed'
        period.closed_by = self.user
        period.closed_at = datetime.now()
        period.save()
        
        period.refresh_from_db()
        self.assertEqual(period.status, 'closed')
        self.assertEqual(period.closed_by, self.user)
        self.assertIsNotNone(period.closed_at)
    
    def test_fiscal_period_default_status_is_open(self):
        """Test that default status is 'open'"""
        period = FiscalPeriod.objects.create(
            name='Test Period',
            period_type='monthly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 1, 31)
        )
        self.assertEqual(period.status, 'open')
    
    def test_fiscal_period_str_representation(self):
        """Test string representation includes name and type"""
        period = FiscalPeriod.objects.create(
            name='January 2024',
            period_type='monthly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 1, 31)
        )
        self.assertEqual(str(period), 'January 2024 (monthly)')
    
    def test_fiscal_period_ordering(self):
        """Test fiscal periods are ordered by start_date descending"""
        period1 = FiscalPeriod.objects.create(
            name='Jan 2024',
            period_type='monthly',
            start_date=date(2024, 1, 1),
            end_date=date(2024, 1, 31)
        )
        
        period2 = FiscalPeriod.objects.create(
            name='Mar 2024',
            period_type='monthly',
            start_date=date(2024, 3, 1),
            end_date=date(2024, 3, 31)
        )
        
        period3 = FiscalPeriod.objects.create(
            name='Feb 2024',
            period_type='monthly',
            start_date=date(2024, 2, 1),
            end_date=date(2024, 2, 29)
        )
        
        periods = list(FiscalPeriod.objects.all())
        self.assertEqual(periods[0].name, 'Mar 2024')
        self.assertEqual(periods[1].name, 'Feb 2024')
        self.assertEqual(periods[2].name, 'Jan 2024')


class AccountBalanceValidationTest(TestCase):
    """Additional validation tests for AccountBalance model"""
    
    def setUp(self):
        """Set up test data"""
        self.account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
    
    def test_account_balance_with_negative_net_balance(self):
        """Test account balance can have negative net balance"""
        balance = AccountBalance.objects.create(
            account=self.account,
            as_of_date=date.today(),
            debit_balance=Decimal('500.00'),
            credit_balance=Decimal('1000.00'),
            net_balance=Decimal('-500.00')
        )
        self.assertEqual(balance.net_balance, Decimal('-500.00'))
    
    def test_account_balance_str_representation(self):
        """Test string representation of account balance"""
        balance = AccountBalance.objects.create(
            account=self.account,
            as_of_date=date(2024, 1, 31),
            debit_balance=Decimal('1000.00'),
            credit_balance=Decimal('0.00'),
            net_balance=Decimal('1000.00')
        )
        expected = f"{self.account.code} - 2024-01-31"
        self.assertEqual(str(balance), expected)
    
    def test_account_balance_auto_update_timestamp(self):
        """Test that updated_at is automatically set"""
        balance = AccountBalance.objects.create(
            account=self.account,
            as_of_date=date.today(),
            debit_balance=Decimal('1000.00'),
            credit_balance=Decimal('0.00'),
            net_balance=Decimal('1000.00')
        )
        
        original_updated = balance.updated_at
        
        # Update the balance
        balance.net_balance = Decimal('1500.00')
        balance.save()
        
        balance.refresh_from_db()
        self.assertGreater(balance.updated_at, original_updated)
    
    def test_account_balance_as_of_date_is_indexed(self):
        """Test that as_of_date field is indexed"""
        field = AccountBalance._meta.get_field('as_of_date')
        self.assertTrue(field.db_index)



# ==================== Task 23.3: Validation Tests ====================

class AccountCleanValidationTest(TestCase):
    """
    Test cases for Account.clean() method validation.
    Tests requirement 16.1, 16.2
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
    
    def test_account_clean_rejects_non_numeric_code(self):
        """Test that Account.clean() rejects non-numeric account codes"""
        account = Account(
            code='ABC123',
            name='Invalid Code Account',
            account_type='asset',
            description='Test with non-numeric code'
        )
        
        with self.assertRaises(ValidationError) as context:
            account.clean()
        
        self.assertIn('code', context.exception.message_dict)
        self.assertIn('numeric', context.exception.message_dict['code'][0].lower())
    
    def test_account_clean_rejects_zero_or_negative_code(self):
        """Test that Account.clean() rejects zero or negative codes"""
        account = Account(
            code='0',
            name='Invalid Code Account',
            account_type='asset',
            description='Test with zero code',
            created_by=self.user
        )
        
        with self.assertRaises(ValidationError) as context:
            account.clean()
        
        self.assertIn('code', context.exception.message_dict)
        self.assertIn('positive', context.exception.message_dict['code'][0])
    
    def test_account_clean_accepts_code_202001(self):
        """Test that Account.clean() accepts code 202001 (minimum valid)"""
        account = Account(
            code='202001',
            name='Valid Code Account',
            account_type='asset',
            description='Test with minimum valid code'
        )
        
        # Should not raise ValidationError
        try:
            account.clean()
        except ValidationError:
            self.fail("Account.clean() raised ValidationError unexpectedly for code 202001")
    
    def test_account_clean_accepts_higher_codes(self):
        """Test that Account.clean() accepts codes above 202001"""
        codes = ['202002', '300000', '999999']
        
        for code in codes:
            account = Account(
                code=code,
                name=f'Account {code}',
                account_type='asset',
                description='Test'
            )
            
            try:
                account.clean()
            except ValidationError:
                self.fail(f"Account.clean() raised ValidationError unexpectedly for code {code}")
    
    def test_account_clean_rejects_alphanumeric_code(self):
        """Test that Account.clean() rejects alphanumeric codes"""
        account = Account(
            code='20200A',
            name='Alphanumeric Code',
            account_type='asset',
            description='Test'
        )
        
        with self.assertRaises(ValidationError) as context:
            account.clean()
        
        self.assertIn('code', context.exception.message_dict)


class JournalEntryCleanValidationTest(TestCase):
    """
    Test cases for JournalEntry.clean() method validation.
    Tests requirement 16.5
    """
    
    def setUp(self):
        """Set up test data"""
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
    
    def test_journal_entry_clean_rejects_future_date(self):
        """Test that JournalEntry.clean() rejects future transaction dates"""
        future_date = date.today() + timedelta(days=10)
        
        entry = JournalEntry(
            reference_number='JE-FUTURE-001',
            transaction_date=future_date,
            description='Future dated entry'
        )
        
        with self.assertRaises(ValidationError) as context:
            entry.clean()
        
        self.assertIn('transaction_date', context.exception.message_dict)
        self.assertIn('future', context.exception.message_dict['transaction_date'][0].lower())
    
    def test_journal_entry_clean_accepts_today_date(self):
        """Test that JournalEntry.clean() accepts today's date"""
        entry = JournalEntry(
            reference_number='JE-TODAY-001',
            transaction_date=date.today(),
            description='Today dated entry'
        )
        
        # Should not raise ValidationError
        try:
            entry.clean()
        except ValidationError:
            self.fail("JournalEntry.clean() raised ValidationError unexpectedly for today's date")
    
    def test_journal_entry_clean_accepts_past_date(self):
        """Test that JournalEntry.clean() accepts past dates"""
        past_date = date.today() - timedelta(days=30)
        
        entry = JournalEntry(
            reference_number='JE-PAST-001',
            transaction_date=past_date,
            description='Past dated entry'
        )
        
        # Should not raise ValidationError
        try:
            entry.clean()
        except ValidationError:
            self.fail("JournalEntry.clean() raised ValidationError unexpectedly for past date")


class JournalEntryLineCleanValidationTest(TestCase):
    """
    Test cases for JournalEntryLine.clean() method validation.
    Tests requirement 16.3, 16.7
    """
    
    def setUp(self):
        """Set up test data"""
        self.account = Account.objects.create(
            code='202001',
            name='Test Account',
            account_type='asset',
            description='Test'
        )
        
        self.entry = JournalEntry.objects.create(
            reference_number='JE-2024-001',
            transaction_date=date.today(),
            description='Test entry'
        )
    
    def test_journal_entry_line_clean_rejects_both_debit_and_credit(self):
        """Test that JournalEntryLine.clean() rejects lines with both debit and credit amounts"""
        line = JournalEntryLine(
            journal_entry=self.entry,
            account=self.account,
            description='Invalid line with both debit and credit',
            debit_amount=Decimal('1000.00'),
            credit_amount=Decimal('500.00'),
            line_number=1
        )
        
        with self.assertRaises(ValidationError) as context:
            line.clean()
        
        # Check that error message mentions mutual exclusivity
        error_msg = str(context.exception).lower()
        self.assertTrue(
            'both' in error_msg or 'only one' in error_msg,
            f"Error message should mention mutual exclusivity, got: {error_msg}"
        )
    
    def test_journal_entry_line_clean_accepts_debit_only(self):
        """Test that JournalEntryLine.clean() accepts line with debit only"""
        line = JournalEntryLine(
            journal_entry=self.entry,
            account=self.account,
            description='Valid debit line',
            debit_amount=Decimal('1000.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        # Should not raise ValidationError
        try:
            line.clean()
        except ValidationError:
            self.fail("JournalEntryLine.clean() raised ValidationError unexpectedly for debit-only line")
    
    def test_journal_entry_line_clean_accepts_credit_only(self):
        """Test that JournalEntryLine.clean() accepts line with credit only"""
        line = JournalEntryLine(
            journal_entry=self.entry,
            account=self.account,
            description='Valid credit line',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('1000.00'),
            line_number=1
        )
        
        # Should not raise ValidationError
        try:
            line.clean()
        except ValidationError:
            self.fail("JournalEntryLine.clean() raised ValidationError unexpectedly for credit-only line")
    
    def test_journal_entry_line_clean_rejects_negative_debit(self):
        """Test that JournalEntryLine.clean() rejects negative debit amounts"""
        line = JournalEntryLine(
            journal_entry=self.entry,
            account=self.account,
            description='Invalid negative debit',
            debit_amount=Decimal('-100.00'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        with self.assertRaises(ValidationError) as context:
            line.clean()
        
        self.assertIn('debit_amount', context.exception.message_dict)
        self.assertIn('positive', context.exception.message_dict['debit_amount'][0].lower())
    
    def test_journal_entry_line_clean_rejects_negative_credit(self):
        """Test that JournalEntryLine.clean() rejects negative credit amounts"""
        line = JournalEntryLine(
            journal_entry=self.entry,
            account=self.account,
            description='Invalid negative credit',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('-100.00'),
            line_number=1
        )
        
        with self.assertRaises(ValidationError) as context:
            line.clean()
        
        self.assertIn('credit_amount', context.exception.message_dict)
        self.assertIn('positive', context.exception.message_dict['credit_amount'][0].lower())
    
    def test_journal_entry_line_clean_rejects_more_than_2_decimal_places_debit(self):
        """Test that JournalEntryLine.clean() rejects debit amounts with more than 2 decimal places"""
        line = JournalEntryLine(
            journal_entry=self.entry,
            account=self.account,
            description='Invalid debit with 3 decimal places',
            debit_amount=Decimal('100.123'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        with self.assertRaises(ValidationError) as context:
            line.clean()
        
        self.assertIn('debit_amount', context.exception.message_dict)
        self.assertIn('2 decimal', context.exception.message_dict['debit_amount'][0].lower())
    
    def test_journal_entry_line_clean_rejects_more_than_2_decimal_places_credit(self):
        """Test that JournalEntryLine.clean() rejects credit amounts with more than 2 decimal places"""
        line = JournalEntryLine(
            journal_entry=self.entry,
            account=self.account,
            description='Invalid credit with 3 decimal places',
            debit_amount=Decimal('0.00'),
            credit_amount=Decimal('100.456'),
            line_number=1
        )
        
        with self.assertRaises(ValidationError) as context:
            line.clean()
        
        self.assertIn('credit_amount', context.exception.message_dict)
        self.assertIn('2 decimal', context.exception.message_dict['credit_amount'][0].lower())
    
    def test_journal_entry_line_clean_accepts_2_decimal_places(self):
        """Test that JournalEntryLine.clean() accepts amounts with exactly 2 decimal places"""
        line = JournalEntryLine(
            journal_entry=self.entry,
            account=self.account,
            description='Valid line with 2 decimal places',
            debit_amount=Decimal('100.99'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        # Should not raise ValidationError
        try:
            line.clean()
        except ValidationError:
            self.fail("JournalEntryLine.clean() raised ValidationError unexpectedly for 2 decimal places")
    
    def test_journal_entry_line_clean_accepts_1_decimal_place(self):
        """Test that JournalEntryLine.clean() accepts amounts with 1 decimal place"""
        line = JournalEntryLine(
            journal_entry=self.entry,
            account=self.account,
            description='Valid line with 1 decimal place',
            debit_amount=Decimal('100.5'),
            credit_amount=Decimal('0.00'),
            line_number=1
        )
        
        # Should not raise ValidationError
        try:
            line.clean()
        except ValidationError:
            self.fail("JournalEntryLine.clean() raised ValidationError unexpectedly for 1 decimal place")
    
    def test_journal_entry_line_clean_accepts_no_decimal_places(self):
        """Test that JournalEntryLine.clean() accepts whole number amounts"""
        line = JournalEntryLine(
            journal_entry=self.entry,
            account=self.account,
            description='Valid line with no decimal places',
            debit_amount=Decimal('100'),
            credit_amount=Decimal('0'),
            line_number=1
        )
        
        # Should not raise ValidationError
        try:
            line.clean()
        except ValidationError:
            self.fail("JournalEntryLine.clean() raised ValidationError unexpectedly for whole numbers")
