using CashLineERP.Application.Finance;
using CashLineERP.Application.Procurement;
using CashLineERP.Application.Sales;
using CashLineERP.Application.Tax;
using CashLineERP.Contracts.Finance;
using CashLineERP.Contracts.Procurement;
using CashLineERP.Contracts.Sales;
using CashLineERP.Contracts.Tax;
using CashLineERP.Infrastructure.Persistence.Repositories;

namespace CashLineERP.Api.Tests;

public sealed class ReconciliationAndCloseServiceQaTests
{
    [Fact]
    public void Bank_Statement_Import_Normalizes_Header_And_Line_Values()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

        var statement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "  cib-egp-import-001  ",
            StatementDate: new DateOnly(2026, 4, 25),
            CurrencyCode: " egp ",
            OpeningBalanceAmount: 1000.125m,
            ClosingBalanceAmount: 1275.555m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 25),
                    ExternalReference: "  bank  ref  001 ",
                    DescriptionText: "  Customer   transfer   inbound  ",
                    DebitAmount: 0m,
                    CreditAmount: 275.555m,
                    RunningBalanceAmount: 1275.555m)
            ]));

        Assert.Equal("CIB-EGP-IMPORT-001", statement.BankAccountCode);
        Assert.Equal("EGP", statement.CurrencyCode);
        Assert.Equal(1000.13m, statement.OpeningBalanceAmount);
        Assert.Equal(1275.56m, statement.ClosingBalanceAmount);
        Assert.Single(statement.Lines);
        Assert.Equal("BANK REF 001", statement.Lines.Single().ExternalReference);
        Assert.Equal("Customer transfer inbound", statement.Lines.Single().DescriptionText);
        Assert.Equal(275.56m, statement.Lines.Single().CreditAmount);
        Assert.Equal(1275.56m, statement.Lines.Single().RunningBalanceAmount);
    }

    [Fact]
    public void Bank_Statement_Import_Blocks_Duplicate_Lines_Within_The_Same_File()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

        var exception = Assert.Throws<InvalidOperationException>(() =>
            reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
                BankAccountCode: "CIB-EGP-DUP-001",
                StatementDate: new DateOnly(2026, 4, 25),
                CurrencyCode: "EGP",
                OpeningBalanceAmount: 500m,
                ClosingBalanceAmount: 900m,
                Lines:
                [
                    new CreateBankStatementLineRequest(
                        TransactionDate: new DateOnly(2026, 4, 25),
                        ExternalReference: "dup-ref-001",
                        DescriptionText: "First duplicate row",
                        DebitAmount: 0m,
                        CreditAmount: 400m,
                        RunningBalanceAmount: 900m),
                    new CreateBankStatementLineRequest(
                        TransactionDate: new DateOnly(2026, 4, 25),
                        ExternalReference: " DUP-REF-001 ",
                        DescriptionText: " First   duplicate   row ",
                        DebitAmount: 0m,
                        CreditAmount: 400.000m,
                        RunningBalanceAmount: 900.00m)
                ])));

        Assert.Contains("duplicate lines", exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void Bank_Statement_Import_Blocks_Duplicate_Reimport_For_The_Same_Scope()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

        reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-DUP-002",
            StatementDate: new DateOnly(2026, 4, 26),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 700m,
            ClosingBalanceAmount: 950m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 26),
                    ExternalReference: "reimport-ref-001",
                    DescriptionText: "Original import line",
                    DebitAmount: 0m,
                    CreditAmount: 250m,
                    RunningBalanceAmount: 950m)
            ]));

        var exception = Assert.Throws<InvalidOperationException>(() =>
            reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
                BankAccountCode: " cib-egp-dup-002 ",
                StatementDate: new DateOnly(2026, 4, 26),
                CurrencyCode: " egp ",
                OpeningBalanceAmount: 700m,
                ClosingBalanceAmount: 950m,
                Lines:
                [
                    new CreateBankStatementLineRequest(
                        TransactionDate: new DateOnly(2026, 4, 26),
                        ExternalReference: " REIMPORT-REF-001 ",
                        DescriptionText: " Original   import line ",
                        DebitAmount: 0m,
                        CreditAmount: 250.000m,
                        RunningBalanceAmount: 950.00m)
                ])));

        Assert.Contains("already imported lines", exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void Posted_Receipt_Reconciliation_Clears_Statement_Lines_But_Close_Readiness_Still_Respects_Other_Month_End_Blockers()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var salesOrderService = new SalesOrderService(new SalesOrderRepository(dbContext));
        var dispatchService = new DispatchOrderService(new DispatchOrderRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var etaService = new EtaComplianceService(new EtaComplianceRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));
        var accountingPeriodService = new AccountingPeriodService(new AccountingPeriodRepository(dbContext));

        var salesOrder = salesOrderService.CreateSalesOrder(new CreateSalesOrderRequest(
            BranchId: 1,
            CustomerId: 2,
            OrderDate: new DateOnly(2026, 4, 10),
            CurrencyCode: "EGP",
            ExchangeRate: 1m,
            Lines:
            [
                new CreateSalesOrderLineRequest(1001, 1, 2m, "EA", 32m, "VAT14")
            ]));

        salesOrderService.SubmitSalesOrder(salesOrder.SalesOrderId, 2);

        var dispatch = dispatchService.CreateDispatchOrder(new CreateDispatchOrderRequest(
            BranchId: 1,
            SalesOrderId: salesOrder.SalesOrderId,
            DispatchWarehouseId: 1,
            PlannedShipAtUtc: new DateTime(2026, 4, 10, 9, 0, 0, DateTimeKind.Utc),
            Lines:
            [
                new CreateDispatchOrderLineRequest(salesOrder.Lines.First().SalesOrderLineId, 1001, 2m, null, "BATCH-1001-C", null)
            ]));

        dispatchService.PostDispatchOrder(dispatch.DispatchOrderId, new PostDispatchOrderRequest(
            ActorUserId: 6,
            ShippedAtUtc: new DateTime(2026, 4, 10, 10, 0, 0, DateTimeKind.Utc),
            DeliveredAtUtc: new DateTime(2026, 4, 10, 14, 0, 0, DateTimeKind.Utc),
            ProofOfDeliveryReceiver: "Alex Receiver"));

        var invoice = invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
            CustomerId: 2,
            SalesOrderId: salesOrder.SalesOrderId,
            DispatchOrderId: dispatch.DispatchOrderId,
            InvoiceType: "TAX",
            InvoiceDate: new DateOnly(2026, 4, 10),
            TaxDate: new DateOnly(2026, 4, 10),
            DueDate: new DateOnly(2026, 4, 18),
            CurrencyCode: "EGP",
            Lines:
            [
                new CreateAccountsReceivableInvoiceLineRequest(salesOrder.Lines.First().SalesOrderLineId, 1001, 2m, "EA", 32m, "VAT14")
            ]));

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(invoiceService, invoice.AccountsReceivableInvoiceId);
        var etaSubmission = etaService.SubmitInvoice(postedInvoice.AccountsReceivableInvoiceId, new SubmitInvoiceToEtaRequest(
            ActorUserId: 4,
            PayloadText: null));
        etaService.RecordInvoiceResponse(
            postedInvoice.AccountsReceivableInvoiceId,
            etaSubmission.EtaSubmissionLogId,
            new RecordEtaInvoiceResponseRequest(
                ActorUserId: 4,
                StatusCode: "ACCEPTED",
                EtaUuid: "ETA-CLOSE-0001",
                ResponseText: "{\"status\":\"accepted\"}",
                ErrorCode: null,
                ErrorMessage: null,
                NotesText: "Accepted before period close."));

        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(
            receiptService,
            receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
                CustomerId: 2,
                ReceiptDate: new DateOnly(2026, 4, 11),
                ReceiptMethod: "BANK_TRANSFER",
                CurrencyCode: "EGP",
                AmountAmount: postedInvoice.TotalAmount,
                ReferenceNumber: "BANK-RECON-0001",
                Notes: "Receipt for reconciliation QA",
                Allocations:
                [
                    new CreateAccountsReceivableReceiptAllocationRequest(postedInvoice.AccountsReceivableInvoiceId, postedInvoice.TotalAmount, new DateOnly(2026, 4, 11))
                ])).AccountsReceivableReceiptId);

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-001",
            StatementDate: new DateOnly(2026, 4, 11),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 10000m,
            ClosingBalanceAmount: 10064m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 11),
                    ExternalReference: "BANK-RECON-0001",
                    DescriptionText: "Customer transfer",
                    DebitAmount: 0m,
                    CreditAmount: postedReceipt.AmountAmount,
                    RunningBalanceAmount: 10064m)
            ]));

        var run = reconciliationService.RunReceiptReconciliation(new RunReceiptReconciliationRequest(1, bankStatement.BankStatementId, 4));
        Assert.Equal("COMPLETED", run.RunStatusCode);
        Assert.Equal(1, run.MatchedItemCount);
        Assert.Equal(0, run.UnmatchedItemCount);
        Assert.All(run.Items, item => Assert.Equal("MATCHED", item.MatchStatusCode));

        var readiness = accountingPeriodService.GetCloseReadiness(1);
        Assert.False(readiness.IsReadyToClose);
        Assert.Equal(0, readiness.UnreconciledStatementLineCount);
        Assert.True(readiness.OpenArInvoiceCount >= 1);
        Assert.True(readiness.OpenArReceiptCount >= 1);
        Assert.Contains(readiness.BlockingReasons, reason => reason.Contains("invoices", StringComparison.OrdinalIgnoreCase));
        Assert.Contains(readiness.BlockingReasons, reason => reason.Contains("receipts", StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public void Close_Readiness_Flags_Pending_Eta_Open_Ap_Payment_And_Exception_Items()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var etaService = new EtaComplianceService(new EtaComplianceRepository(dbContext));
        var billService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));
        var accountingPeriodService = new AccountingPeriodService(new AccountingPeriodRepository(dbContext));

        var invoice = invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
            CustomerId: 2,
            SalesOrderId: 1,
            DispatchOrderId: 1,
            InvoiceType: "TAX",
            InvoiceDate: new DateOnly(2026, 4, 18),
            TaxDate: new DateOnly(2026, 4, 18),
            DueDate: new DateOnly(2026, 4, 28),
            CurrencyCode: "EGP",
            Lines:
            [
                new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 1m, "EA", 32m, "VAT14")
            ]));

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(invoiceService, invoice.AccountsReceivableInvoiceId);
        var payloadPreview = etaService.GetPayloadPreview(postedInvoice.AccountsReceivableInvoiceId);
        Assert.NotNull(payloadPreview);
        Assert.True(payloadPreview!.IsStatutoryReady);

        var bill = billService.CreateBill(new CreateAccountsPayableBillRequest(
            SupplierId: 1,
            PurchaseOrderId: 1,
            GoodsReceiptId: 1,
            BillDate: new DateOnly(2026, 4, 18),
            DueDate: new DateOnly(2026, 4, 30),
            CurrencyCode: "EGP",
            Lines:
            [
                new CreateAccountsPayableBillLineRequest(1, 1001, 10m, "EA", 24m, "VAT14")
            ]));

        billService.SubmitBill(bill.AccountsPayableBillId, 3);
        billService.ApproveMatch(bill.AccountsPayableBillId, 4);
        var postedBill = billService.PostBill(bill.AccountsPayableBillId, 4);

        var createdExceptionPayment = paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: 1,
            PaymentDate: new DateOnly(2026, 4, 18),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: postedBill.TotalAmount,
            ReferenceNumber: "PERIOD-EXCEPTION-0001",
            Notes: "Month-end exception payment",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, postedBill.TotalAmount, new DateOnly(2026, 4, 18))
            ]));
        var submittedExceptionPayment = paymentService.SubmitPayment(createdExceptionPayment.AccountsPayablePaymentId, 3);
        var approvedExceptionPayment = paymentService.ApprovePayment(submittedExceptionPayment.AccountsPayablePaymentId, 4);
        var postedExceptionPayment = paymentService.PostPayment(approvedExceptionPayment.AccountsPayablePaymentId, 4);

        var secondBill = billService.CreateBill(new CreateAccountsPayableBillRequest(
            SupplierId: 1,
            PurchaseOrderId: 1,
            GoodsReceiptId: 1,
            BillDate: new DateOnly(2026, 4, 18),
            DueDate: new DateOnly(2026, 4, 30),
            CurrencyCode: "EGP",
            Lines:
            [
                new CreateAccountsPayableBillLineRequest(2, 1002, 30m, "BOX", 50m, "VAT14")
            ]));

        billService.SubmitBill(secondBill.AccountsPayableBillId, 3);
        billService.ApproveMatch(secondBill.AccountsPayableBillId, 4);
        var postedSecondBill = billService.PostBill(secondBill.AccountsPayableBillId, 4);

        var submittedPayment = paymentService.SubmitPayment(
            paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
                SupplierId: 1,
                PaymentDate: new DateOnly(2026, 4, 18),
                PaymentMethod: "BANK_TRANSFER",
                CurrencyCode: "EGP",
                AmountAmount: 100m,
                ReferenceNumber: "PERIOD-BLOCK-0001",
                Notes: "Month-end blocker payment",
                Allocations:
                [
                    new CreateAccountsPayablePaymentAllocationRequest(postedSecondBill.AccountsPayableBillId, 100m, new DateOnly(2026, 4, 18))
                ])).AccountsPayablePaymentId,
            3);

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-CLOSE-001",
            StatementDate: new DateOnly(2026, 4, 18),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 12000m,
            ClosingBalanceAmount: 10000m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 18),
                    ExternalReference: "PERIOD-EXCEPTION-0001",
                    DescriptionText: "Supplier settlement mismatch",
                    DebitAmount: postedExceptionPayment.AmountAmount - 25m,
                    CreditAmount: 0m,
                    RunningBalanceAmount: 10000m)
            ]));

        var settlementRun = reconciliationService.RunSettlementReconciliation(
            new RunReceiptReconciliationRequest(1, bankStatement.BankStatementId, 4));

        Assert.Equal(1, settlementRun.ExceptionItemCount);

        var readiness = accountingPeriodService.GetCloseReadiness(1);

        Assert.False(readiness.IsReadyToClose);
        Assert.Equal(1, readiness.ExceptionReconciliationItemCount);
        Assert.Equal(1, readiness.PendingEtaInvoiceCount);
        Assert.Equal(1, readiness.OpenApPaymentCount);
        Assert.Contains(readiness.BlockingReasons, reason => reason.Contains("ETA", StringComparison.OrdinalIgnoreCase));
        Assert.Contains(readiness.BlockingReasons, reason => reason.Contains("payments remain unposted", StringComparison.OrdinalIgnoreCase));
        Assert.Contains(readiness.BlockingReasons, reason => reason.Contains("exception items", StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public void Reconciliation_Exception_Queue_Returns_Exception_And_Unmatched_Items_With_Run_And_Statement_Context()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

        var receipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 21),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 500m,
            ReferenceNumber: "AR-QUEUE-0001",
            Notes: "Queue exception receipt",
            Allocations: []));
        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(receiptService, receipt.AccountsReceivableReceiptId);

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-QUEUE-001",
            StatementDate: new DateOnly(2026, 4, 21),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 10000m,
            ClosingBalanceAmount: 9250m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 21),
                    ExternalReference: "AR-QUEUE-0001",
                    DescriptionText: "Likely receipt mismatch",
                    DebitAmount: 0m,
                    CreditAmount: postedReceipt.AmountAmount - 50m,
                    RunningBalanceAmount: 9550m),
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 21),
                    ExternalReference: "AR-NO-MATCH-0001",
                    DescriptionText: "Unmatched receipt line",
                    DebitAmount: 0m,
                    CreditAmount: 300m,
                    RunningBalanceAmount: 9250m)
            ]));

        var run = reconciliationService.RunReceiptReconciliation(
            new RunReceiptReconciliationRequest(1, bankStatement.BankStatementId, 4));

        Assert.Equal(1, run.ExceptionItemCount);
        Assert.Equal(1, run.UnmatchedItemCount);

        var queue = reconciliationService.GetExceptionQueue();
        Assert.Equal(2, queue.Count);
        Assert.All(queue, item => Assert.Equal(run.ReceiptReconciliationRunId, item.ReceiptReconciliationRunId));
        Assert.All(queue, item => Assert.Equal(bankStatement.BankStatementId, item.BankStatementId));
        Assert.All(queue, item => Assert.Equal("CIB-EGP-QUEUE-001", item.BankAccountCode));
        Assert.Contains(queue, item => item.MatchStatusCode == "EXCEPTION" && item.AccountsReceivableReceiptId == postedReceipt.AccountsReceivableReceiptId);
        Assert.Contains(queue, item => item.MatchStatusCode == "UNMATCHED" && item.AccountsReceivableReceiptId is null);
    }

    [Fact]
    public void Reconciliation_Exception_Queue_Can_Be_Filtered_By_Status()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

        var receipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 22),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 400m,
            ReferenceNumber: "AR-QUEUE-FLT-0001",
            Notes: "Queue filter receipt",
            Allocations: []));
        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(receiptService, receipt.AccountsReceivableReceiptId);

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-QUEUE-002",
            StatementDate: new DateOnly(2026, 4, 22),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 8000m,
            ClosingBalanceAmount: 7300m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 22),
                    ExternalReference: "AR-QUEUE-FLT-0001",
                    DescriptionText: "Exception candidate",
                    DebitAmount: 0m,
                    CreditAmount: postedReceipt.AmountAmount - 25m,
                    RunningBalanceAmount: 7575m),
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 22),
                    ExternalReference: "AR-QUEUE-FLT-UNMATCHED",
                    DescriptionText: "Unmatched candidate",
                    DebitAmount: 0m,
                    CreditAmount: 275m,
                    RunningBalanceAmount: 7300m)
            ]));

        reconciliationService.RunReceiptReconciliation(new RunReceiptReconciliationRequest(1, bankStatement.BankStatementId, 4));

        var exceptionOnly = reconciliationService.GetExceptionQueue("EXCEPTION");
        Assert.Single(exceptionOnly);
        Assert.All(exceptionOnly, item => Assert.Equal("EXCEPTION", item.MatchStatusCode));

        var unmatchedOnly = reconciliationService.GetExceptionQueue("UNMATCHED");
        Assert.Single(unmatchedOnly);
        Assert.All(unmatchedOnly, item => Assert.Equal("UNMATCHED", item.MatchStatusCode));
    }

    [Fact]
    public void Manual_Match_Can_Resolve_Unmatched_Reconciliation_Item()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));
        var accountingPeriodService = new AccountingPeriodService(new AccountingPeriodRepository(dbContext));

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-MANUAL-001",
            StatementDate: new DateOnly(2026, 4, 23),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 9000m,
            ClosingBalanceAmount: 9450m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 23),
                    ExternalReference: "AR-MISMATCH-REF-0001",
                    DescriptionText: "Needs manual match",
                    DebitAmount: 0m,
                    CreditAmount: 450m,
                    RunningBalanceAmount: 9450m)
            ]));

        reconciliationService.RunReceiptReconciliation(new RunReceiptReconciliationRequest(1, bankStatement.BankStatementId, 4));
        var queueItem = reconciliationService.GetExceptionQueue("UNMATCHED").Single();

        var receipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 23),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 450m,
            ReferenceNumber: "AR-MANUAL-0001",
            Notes: "Manual match receipt",
            Allocations: []));
        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(receiptService, receipt.AccountsReceivableReceiptId);

        var resolvedItem = reconciliationService.ManualMatch(
            queueItem.ReceiptReconciliationItemId,
            new ManualMatchReconciliationItemRequest(
                ActorUserId: 4,
                MatchedDocumentType: "AR_RECEIPT",
                AccountsReceivableReceiptId: postedReceipt.AccountsReceivableReceiptId,
                AccountsPayablePaymentId: null,
                Notes: "Finance manual match approved."));

        Assert.Equal("MANUAL_MATCHED", resolvedItem.MatchStatusCode);
        Assert.Equal("MANUAL_MATCHED", resolvedItem.ResolutionCode);
        Assert.Equal(postedReceipt.AccountsReceivableReceiptId, resolvedItem.AccountsReceivableReceiptId);
        Assert.NotNull(resolvedItem.ResolvedAtUtc);
        Assert.Equal(4, resolvedItem.ResolvedByUserId);

        var remainingQueue = reconciliationService.GetExceptionQueue();
        Assert.DoesNotContain(remainingQueue, item => item.ReceiptReconciliationItemId == queueItem.ReceiptReconciliationItemId);

        var refreshedStatement = reconciliationService.GetBankStatement(bankStatement.BankStatementId);
        Assert.NotNull(refreshedStatement);
        Assert.True(refreshedStatement!.Lines.Single().IsReconciled);
        Assert.Equal(postedReceipt.AccountsReceivableReceiptId, refreshedStatement.Lines.Single().AccountsReceivableReceiptId);

        var readiness = accountingPeriodService.GetCloseReadiness(1);
        Assert.Equal(0, readiness.UnreconciledStatementLineCount);
    }

    [Fact]
    public void Override_Can_Resolve_Exception_Reconciliation_Item_With_Approved_Variance()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));
        var accountingPeriodService = new AccountingPeriodService(new AccountingPeriodRepository(dbContext));

        var receipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 24),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 520m,
            ReferenceNumber: "AR-OVERRIDE-0001",
            Notes: "Override candidate receipt",
            Allocations: []));
        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(receiptService, receipt.AccountsReceivableReceiptId);

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-OVERRIDE-001",
            StatementDate: new DateOnly(2026, 4, 24),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 10000m,
            ClosingBalanceAmount: 10480m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 24),
                    ExternalReference: "AR-OVERRIDE-0001",
                    DescriptionText: "Approved variance case",
                    DebitAmount: 0m,
                    CreditAmount: postedReceipt.AmountAmount - 40m,
                    RunningBalanceAmount: 10480m)
            ]));

        reconciliationService.RunReceiptReconciliation(new RunReceiptReconciliationRequest(1, bankStatement.BankStatementId, 4));
        var queueItem = reconciliationService.GetExceptionQueue("EXCEPTION").Single();

        var resolvedItem = reconciliationService.OverrideItem(
            queueItem.ReceiptReconciliationItemId,
            new OverrideReconciliationItemRequest(
                ActorUserId: 4,
                MatchedDocumentType: "AR_RECEIPT",
                AccountsReceivableReceiptId: postedReceipt.AccountsReceivableReceiptId,
                AccountsPayablePaymentId: null,
                ApprovalRoleCode: "FINANCE_MANAGER",
                OverrideReasonCode: "TIMING_DIFFERENCE",
                Notes: "Variance approved by finance checker."));

        Assert.Equal("OVERRIDDEN", resolvedItem.MatchStatusCode);
        Assert.Equal("OVERRIDDEN", resolvedItem.ResolutionCode);
        Assert.Equal("FINANCE_MANAGER", resolvedItem.OverrideApprovalRoleCode);
        Assert.Equal("TIMING_DIFFERENCE", resolvedItem.OverrideReasonCode);
        Assert.Equal(postedReceipt.AccountsReceivableReceiptId, resolvedItem.AccountsReceivableReceiptId);
        Assert.Equal(-40m, resolvedItem.DifferenceAmount);
        Assert.Contains("Variance approved", resolvedItem.Notes, StringComparison.OrdinalIgnoreCase);

        var exceptionQueue = reconciliationService.GetExceptionQueue("EXCEPTION");
        Assert.DoesNotContain(exceptionQueue, item => item.ReceiptReconciliationItemId == queueItem.ReceiptReconciliationItemId);

        var refreshedStatement = reconciliationService.GetBankStatement(bankStatement.BankStatementId);
        Assert.NotNull(refreshedStatement);
        Assert.True(refreshedStatement!.Lines.Single().IsReconciled);

        var readiness = accountingPeriodService.GetCloseReadiness(1);
        Assert.Equal(0, readiness.ExceptionReconciliationItemCount);
    }

    [Fact]
    public void Override_Rejects_Invalid_Approval_Role_And_Reason_Taxonomy()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

        var receipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 24),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 410m,
            ReferenceNumber: "AR-OVERRIDE-TAXONOMY-0001",
            Notes: "Override taxonomy candidate",
            Allocations: []));
        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(receiptService, receipt.AccountsReceivableReceiptId);

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-OVERRIDE-TAXONOMY-001",
            StatementDate: new DateOnly(2026, 4, 24),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 10000m,
            ClosingBalanceAmount: 10380m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 24),
                    ExternalReference: "AR-OVERRIDE-TAXONOMY-0001",
                    DescriptionText: "Invalid taxonomy case",
                    DebitAmount: 0m,
                    CreditAmount: postedReceipt.AmountAmount - 30m,
                    RunningBalanceAmount: 10380m)
            ]));

        reconciliationService.RunReceiptReconciliation(new RunReceiptReconciliationRequest(1, bankStatement.BankStatementId, 4));
        var queueItem = reconciliationService.GetExceptionQueue("EXCEPTION").Single();

        var exception = Assert.Throws<InvalidOperationException>(() =>
            reconciliationService.OverrideItem(
                queueItem.ReceiptReconciliationItemId,
                new OverrideReconciliationItemRequest(
                    ActorUserId: 4,
                    MatchedDocumentType: "AR_RECEIPT",
                    AccountsReceivableReceiptId: postedReceipt.AccountsReceivableReceiptId,
                    AccountsPayablePaymentId: null,
                    ApprovalRoleCode: "FINANCE_CHECKER",
                    OverrideReasonCode: "APPROVED_WRITE_OFF",
                    Notes: "Should fail taxonomy validation.")));

        Assert.Contains("cannot approve", exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void Reconciliation_History_And_Audit_Views_Preserve_Resolution_Timeline()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

        var receipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 24),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 520m,
            ReferenceNumber: "AR-AUDIT-0001",
            Notes: "Audit candidate receipt",
            Allocations: []));
        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(receiptService, receipt.AccountsReceivableReceiptId);

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-AUDIT-001",
            StatementDate: new DateOnly(2026, 4, 24),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 10000m,
            ClosingBalanceAmount: 10470m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 24),
                    ExternalReference: "AR-AUDIT-0001",
                    DescriptionText: "Audit history case",
                    DebitAmount: 0m,
                    CreditAmount: postedReceipt.AmountAmount - 50m,
                    RunningBalanceAmount: 10470m)
            ]));

        var run = reconciliationService.RunReceiptReconciliation(new RunReceiptReconciliationRequest(1, bankStatement.BankStatementId, 4));
        var queueItem = reconciliationService.GetExceptionQueue("EXCEPTION").Single();

        reconciliationService.OverrideItem(
            queueItem.ReceiptReconciliationItemId,
            new OverrideReconciliationItemRequest(
                ActorUserId: 4,
                MatchedDocumentType: "AR_RECEIPT",
                AccountsReceivableReceiptId: postedReceipt.AccountsReceivableReceiptId,
                AccountsPayablePaymentId: null,
                ApprovalRoleCode: "FINANCE_MANAGER",
                OverrideReasonCode: "TIMING_DIFFERENCE",
                Notes: "Approved with timeline evidence."));

        var history = reconciliationService.GetHistory(receiptReconciliationRunId: run.ReceiptReconciliationRunId);
        var historyItem = Assert.Single(history);
        Assert.Equal(queueItem.ReceiptReconciliationItemId, historyItem.ReceiptReconciliationItemId);
        Assert.Equal("OVERRIDDEN", historyItem.ResolutionCode);
        Assert.Equal("FINANCE_MANAGER", historyItem.OverrideApprovalRoleCode);
        Assert.Equal("TIMING_DIFFERENCE", historyItem.OverrideReasonCode);

        var audit = reconciliationService.GetAudit(queueItem.ReceiptReconciliationItemId);
        Assert.NotNull(audit);
        Assert.Equal("OVERRIDDEN", audit!.Item.ResolutionCode);
        Assert.Equal(2, audit.Events.Count);
        Assert.Contains(audit.Events, item => item.ActionCode == "EXCEPTION_IDENTIFIED");
        Assert.Contains(audit.Events, item => item.ActionCode == "OVERRIDDEN");
    }

    [Fact]
    public void Bulk_Resolve_Allows_Mixed_Success_And_Failure_For_Finance_Operations()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

        var firstReceipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 24),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 500m,
            ReferenceNumber: "AR-BULK-0001",
            Notes: "First bulk receipt",
            Allocations: []));
        var postedFirstReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(receiptService, firstReceipt.AccountsReceivableReceiptId);

        var secondReceipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 24),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 620m,
            ReferenceNumber: "AR-BULK-0002",
            Notes: "Second bulk receipt",
            Allocations: []));
        var postedSecondReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(receiptService, secondReceipt.AccountsReceivableReceiptId);

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-BULK-001",
            StatementDate: new DateOnly(2026, 4, 24),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 10000m,
            ClosingBalanceAmount: 11100m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 24),
                    ExternalReference: "AR-BULK-0001",
                    DescriptionText: "Bulk exception one",
                    DebitAmount: 0m,
                    CreditAmount: postedFirstReceipt.AmountAmount - 20m,
                    RunningBalanceAmount: 10480m),
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 24),
                    ExternalReference: "AR-BULK-0002",
                    DescriptionText: "Bulk exception two",
                    DebitAmount: 0m,
                    CreditAmount: postedSecondReceipt.AmountAmount - 30m,
                    RunningBalanceAmount: 11070m)
            ]));

        reconciliationService.RunReceiptReconciliation(new RunReceiptReconciliationRequest(1, bankStatement.BankStatementId, 4));
        var queueItems = reconciliationService.GetExceptionQueue("EXCEPTION")
            .Where(item => item.BankStatementId == bankStatement.BankStatementId)
            .OrderBy(item => item.BankStatementLineNumber)
            .ToArray();

        var response = reconciliationService.BulkResolve(new BulkResolveReconciliationItemsRequest(
            ActorUserId: 4,
            Items:
            [
                new BulkResolveReconciliationItemRequest(
                    queueItems[0].ReceiptReconciliationItemId,
                    "OVERRIDE",
                    "AR_RECEIPT",
                    postedFirstReceipt.AccountsReceivableReceiptId,
                    null,
                    "FINANCE_MANAGER",
                    "TIMING_DIFFERENCE",
                    "Bulk approved variance",
                    null),
                new BulkResolveReconciliationItemRequest(
                    queueItems[1].ReceiptReconciliationItemId,
                    "OVERRIDE",
                    "AR_RECEIPT",
                    postedSecondReceipt.AccountsReceivableReceiptId,
                    null,
                    "FINANCE_CHECKER",
                    "APPROVED_WRITE_OFF",
                    "Should fail role taxonomy",
                    null)
            ]));

        Assert.Equal(2, response.RequestedCount);
        Assert.Equal(1, response.ResolvedCount);
        Assert.Equal(1, response.FailedCount);
        Assert.Equal(2, response.Items.Count);
        Assert.Contains(response.Items, item => item.Succeeded && item.Item?.ResolutionCode == "OVERRIDDEN");
        Assert.Contains(response.Items, item => !item.Succeeded && item.ErrorMessage is not null && item.ErrorMessage.Contains("cannot approve", StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public void Manual_Match_Cannot_Reconcile_The_Same_Ar_Receipt_To_Multiple_Bank_Statement_Lines()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

        var firstStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-DUP-MATCH-001",
            StatementDate: new DateOnly(2026, 4, 24),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 1000m,
            ClosingBalanceAmount: 1450m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 24),
                    ExternalReference: "FIRST-UNMATCHED",
                    DescriptionText: "First unmatched line",
                    DebitAmount: 0m,
                    CreditAmount: 450m,
                    RunningBalanceAmount: 1450m)
            ]));

        reconciliationService.RunReceiptReconciliation(new RunReceiptReconciliationRequest(1, firstStatement.BankStatementId, 4));
        var firstQueueItem = reconciliationService.GetExceptionQueue("UNMATCHED").Single();

        var receipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 24),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 450m,
            ReferenceNumber: "AR-DUP-RECON-0001",
            Notes: "Manual-match duplicate guard",
            Allocations: []));
        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(receiptService, receipt.AccountsReceivableReceiptId);

        var firstResolution = reconciliationService.ManualMatch(
            firstQueueItem.ReceiptReconciliationItemId,
            new ManualMatchReconciliationItemRequest(
                ActorUserId: 4,
                MatchedDocumentType: "AR_RECEIPT",
                AccountsReceivableReceiptId: postedReceipt.AccountsReceivableReceiptId,
                AccountsPayablePaymentId: null,
                Notes: "Initial manual match."));

        Assert.Equal("MANUAL_MATCHED", firstResolution.ResolutionCode);

        var secondStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-DUP-MATCH-002",
            StatementDate: new DateOnly(2026, 4, 24),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 2000m,
            ClosingBalanceAmount: 2450m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 24),
                    ExternalReference: "SECOND-UNMATCHED",
                    DescriptionText: "Second unmatched line",
                    DebitAmount: 0m,
                    CreditAmount: 450m,
                    RunningBalanceAmount: 2450m)
            ]));

        reconciliationService.RunReceiptReconciliation(new RunReceiptReconciliationRequest(1, secondStatement.BankStatementId, 4));
        var secondQueueItem = reconciliationService.GetExceptionQueue().Single(item => item.BankStatementId == secondStatement.BankStatementId);

        var duplicateMatchException = Assert.Throws<InvalidOperationException>(() =>
            reconciliationService.ManualMatch(
                secondQueueItem.ReceiptReconciliationItemId,
                new ManualMatchReconciliationItemRequest(
                    ActorUserId: 4,
                    MatchedDocumentType: "AR_RECEIPT",
                    AccountsReceivableReceiptId: postedReceipt.AccountsReceivableReceiptId,
                    AccountsPayablePaymentId: null,
                    Notes: "Duplicate reconciliation attempt.")));

        Assert.Contains("already reconciled", duplicateMatchException.Message, StringComparison.OrdinalIgnoreCase);
    }
}
