"""
Unit tests for the three new DataIntegrityChecker checks.

Tests cover:
- check_tb_balance: flags HIGH when Trial Balance is unbalanced
- check_cross_statement_reconciliation: flags HIGH when IS–SFP reconciliation fails
- check_opening_balance_continuity: flags HIGH when opening balance mismatches

Requirements: 10.2, 10.3, 10.4
"""

import uuid
from datetime import date, timedelta
from decimal import Decimal
from io import StringIO
from unittest.mock import patch, MagicMock

from django.test import TestCase
from django.contrib.auth import get_user_model

from accounting.management.commands.data_integrity_check import Command
from accounting.models import Account, FiscalPeriod
from accounting.services.reconciliation_service import TBBalanceResult, ReconciliationResult

User = get_user_model()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_command():
    """Instantiate Command with a StringIO stdout, ready to run a single check."""
    cmd = Command()
    cmd.stdout = StringIO()
    cmd.stderr = StringIO()
    cmd.style = MagicMock()
    # style methods return the string they receive so descriptions survive
    for method_name in ('SUCCESS', 'ERROR', 'WARNING'):
        getattr(cmd.style, method_name).side_effect = lambda s: s
    cmd.fix_mode = False
    cmd.verbose = False
    cmd.issues_found = []
    return cmd


# ---------------------------------------------------------------------------
# 10.2 – Trial Balance imbalance
# ---------------------------------------------------------------------------

class CheckTBBalanceTest(TestCase):
    """
    Tests for Command.check_tb_balance (Requirement 10.2).

    An unbalanced Trial Balance must be flagged as a HIGH severity issue.
    """

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_unbalanced_tb_flags_high_severity_issue(self, MockReconciliationService):
        """
        When verify_tb_balance returns balanced=False, check_tb_balance must
        append exactly one HIGH-severity 'Trial Balance Imbalance' issue.

        Validates: Requirements 10.2
        """
        mock_service = MagicMock()
        mock_service.verify_tb_balance.return_value = TBBalanceResult(
            balanced=False,
            total_debits=Decimal('10000.00'),
            total_credits=Decimal('9500.00'),
            difference=Decimal('500.00'),
        )
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_tb_balance()

        self.assertEqual(len(cmd.issues_found), 1)
        issue = cmd.issues_found[0]
        self.assertEqual(issue['severity'], 'HIGH')
        self.assertEqual(issue['type'], 'Trial Balance Imbalance')
        self.assertIn('500', issue['description'])

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_balanced_tb_produces_no_issues(self, MockReconciliationService):
        """
        When verify_tb_balance returns balanced=True, no issues are appended.

        Validates: Requirements 10.2
        """
        mock_service = MagicMock()
        mock_service.verify_tb_balance.return_value = TBBalanceResult(
            balanced=True,
            total_debits=Decimal('10000.00'),
            total_credits=Decimal('10000.00'),
            difference=Decimal('0.00'),
        )
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_tb_balance()

        self.assertEqual(len(cmd.issues_found), 0)

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_unbalanced_tb_issue_reference_is_trial_balance(self, MockReconciliationService):
        """
        The issue reference for a TB imbalance should be 'Trial Balance'.

        Validates: Requirements 10.2
        """
        mock_service = MagicMock()
        mock_service.verify_tb_balance.return_value = TBBalanceResult(
            balanced=False,
            total_debits=Decimal('5000.00'),
            total_credits=Decimal('4999.00'),
            difference=Decimal('1.00'),
        )
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_tb_balance()

        self.assertEqual(cmd.issues_found[0]['reference'], 'Trial Balance')


# ---------------------------------------------------------------------------
# 10.3 – IS–SFP cross-statement reconciliation
# ---------------------------------------------------------------------------

class CheckCrossStatementReconciliationTest(TestCase):
    """
    Tests for Command.check_cross_statement_reconciliation (Requirement 10.3).

    A failed IS–SFP reconciliation must be flagged as HIGH severity.
    """

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_unreconciled_is_sfp_flags_high_severity_issue(self, MockReconciliationService):
        """
        When verify_is_sfp_reconciliation returns reconciled=False, the check
        must append exactly one HIGH-severity 'Cross-Statement Reconciliation
        Failure' issue.

        Validates: Requirements 10.3
        """
        mock_service = MagicMock()
        mock_service.verify_is_sfp_reconciliation.return_value = ReconciliationResult(
            reconciled=False,
            is_net_income=Decimal('20000.00'),
            equity_opening=Decimal('50000.00'),
            equity_closing=Decimal('68000.00'),
            equity_change=Decimal('18000.00'),
            difference=Decimal('2000.00'),
        )
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_cross_statement_reconciliation()

        self.assertEqual(len(cmd.issues_found), 1)
        issue = cmd.issues_found[0]
        self.assertEqual(issue['severity'], 'HIGH')
        self.assertEqual(issue['type'], 'Cross-Statement Reconciliation Failure')

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_unreconciled_issue_contains_relevant_amounts(self, MockReconciliationService):
        """
        The issue description should include IS net income, equity change, and
        the difference so that the operator can triage quickly.

        Validates: Requirements 10.3
        """
        mock_service = MagicMock()
        mock_service.verify_is_sfp_reconciliation.return_value = ReconciliationResult(
            reconciled=False,
            is_net_income=Decimal('12345.00'),
            equity_opening=Decimal('100000.00'),
            equity_closing=Decimal('111111.00'),
            equity_change=Decimal('11111.00'),
            difference=Decimal('1234.00'),
        )
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_cross_statement_reconciliation()

        description = cmd.issues_found[0]['description']
        self.assertIn('12345', description)
        self.assertIn('11111', description)
        self.assertIn('1234', description)

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_reconciled_is_sfp_produces_no_issues(self, MockReconciliationService):
        """
        When verify_is_sfp_reconciliation returns reconciled=True, no issues
        are appended.

        Validates: Requirements 10.3
        """
        mock_service = MagicMock()
        mock_service.verify_is_sfp_reconciliation.return_value = ReconciliationResult(
            reconciled=True,
            is_net_income=Decimal('15000.00'),
            equity_opening=Decimal('50000.00'),
            equity_closing=Decimal('65000.00'),
            equity_change=Decimal('15000.00'),
            difference=Decimal('0.00'),
        )
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_cross_statement_reconciliation()

        self.assertEqual(len(cmd.issues_found), 0)

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_unreconciled_issue_reference_is_is_vs_sfp(self, MockReconciliationService):
        """
        The issue reference for a reconciliation failure should be 'IS vs SFP'.

        Validates: Requirements 10.3
        """
        mock_service = MagicMock()
        mock_service.verify_is_sfp_reconciliation.return_value = ReconciliationResult(
            reconciled=False,
            is_net_income=Decimal('1.00'),
            equity_opening=Decimal('0.00'),
            equity_closing=Decimal('0.00'),
            equity_change=Decimal('0.00'),
            difference=Decimal('1.00'),
        )
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_cross_statement_reconciliation()

        self.assertEqual(cmd.issues_found[0]['reference'], 'IS vs SFP')


# ---------------------------------------------------------------------------
# 10.4 – Opening balance continuity
# ---------------------------------------------------------------------------

class CheckOpeningBalanceContinuityTest(TestCase):
    """
    Tests for Command.check_opening_balance_continuity (Requirement 10.4).

    A mismatch between an account's opening balance and the prior day's closing
    balance must be flagged as HIGH severity.
    """

    def setUp(self):
        # Account.created_by is null=True, so we can omit the user to avoid
        # hitting unrelated schema gaps in the test database (loans app).
        today = date.today()
        self.period = FiscalPeriod.objects.create(
            name=f'OBC Test Period {uuid.uuid4().hex[:6]}',
            period_type='monthly',
            start_date=today - timedelta(days=30),
            end_date=today + timedelta(days=1),
            status='open',
        )
        self.account = Account.objects.create(
            code=f'OBC{uuid.uuid4().hex[:5]}',
            name='OBC Test Account',
            account_type='asset',
            subtype='current_asset',
            description='Opening balance continuity test account',
            is_active=True,
            created_by=None,
        )

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_mismatch_flags_high_severity_issue(self, MockReconciliationService):
        """
        When verify_opening_balance_continuity returns False for a specific
        account + period, check_opening_balance_continuity must append a
        HIGH-severity 'Opening Balance Mismatch' issue for that account.

        Validates: Requirements 10.4
        """
        account_code = self.account.code
        period_start = self.period.start_date

        def fake_verify(account, period_start_arg, branch=None):
            # Return False only for our seeded account+period
            if account.code == account_code and period_start_arg == period_start:
                return False
            return True

        mock_service = MagicMock()
        mock_service.verify_opening_balance_continuity.side_effect = fake_verify
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_opening_balance_continuity()

        matching = [
            i for i in cmd.issues_found
            if i['type'] == 'Opening Balance Mismatch'
            and account_code in i['reference']
        ]
        self.assertGreaterEqual(len(matching), 1)
        self.assertEqual(matching[0]['severity'], 'HIGH')

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_mismatch_issue_description_contains_account_and_date(
        self, MockReconciliationService
    ):
        """
        The issue description must include the account code and the period
        start date so operators can locate the problem.

        Validates: Requirements 10.4
        """
        account_code = self.account.code
        period_start = self.period.start_date

        mock_service = MagicMock()
        mock_service.verify_opening_balance_continuity.return_value = False
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_opening_balance_continuity()

        self.assertTrue(len(cmd.issues_found) > 0)
        issue = cmd.issues_found[0]
        self.assertIn(account_code, issue['description'])
        self.assertIn(str(period_start), issue['description'])

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_no_mismatch_produces_no_issues(self, MockReconciliationService):
        """
        When verify_opening_balance_continuity returns True for all combinations,
        no 'Opening Balance Mismatch' issues are appended.

        Validates: Requirements 10.4
        """
        mock_service = MagicMock()
        mock_service.verify_opening_balance_continuity.return_value = True
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_opening_balance_continuity()

        self.assertEqual(len(cmd.issues_found), 0)

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_mismatch_issue_type_is_opening_balance_mismatch(self, MockReconciliationService):
        """
        The 'type' field of the flagged issue must be exactly
        'Opening Balance Mismatch'.

        Validates: Requirements 10.4
        """
        mock_service = MagicMock()
        mock_service.verify_opening_balance_continuity.return_value = False
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_opening_balance_continuity()

        self.assertTrue(len(cmd.issues_found) > 0)
        self.assertEqual(cmd.issues_found[0]['type'], 'Opening Balance Mismatch')

    @patch(
        'accounting.management.commands.data_integrity_check.ReconciliationService'
    )
    def test_only_periods_within_90_days_are_checked(self, MockReconciliationService):
        """
        Fiscal periods whose end_date is older than 90 days must be skipped.
        Only recent/open periods should be evaluated.

        Validates: Requirements 10.4
        """
        # Create an old closed period outside the 90-day window
        old_period = FiscalPeriod.objects.create(
            name=f'Old Period {uuid.uuid4().hex[:6]}',
            period_type='monthly',
            start_date=date.today() - timedelta(days=200),
            end_date=date.today() - timedelta(days=95),
            status='closed',
        )

        call_args_list = []

        def capturing_verify(account, period_start_arg, branch=None):
            call_args_list.append((account.pk, period_start_arg))
            return True

        mock_service = MagicMock()
        mock_service.verify_opening_balance_continuity.side_effect = capturing_verify
        MockReconciliationService.return_value = mock_service

        cmd = _make_command()
        cmd.check_opening_balance_continuity()

        # old_period.start_date must not appear in any call
        checked_starts = {args[1] for args in call_args_list}
        self.assertNotIn(old_period.start_date, checked_starts)
