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

namespace CashLineERP.Api.Tests;

public sealed class OrderToCashServiceQaTests
{
    [Fact]
    public void Sales_Dispatch_Invoice_Receipt_Flow_Produces_Finance_Traceability()
    {
        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 salesOrder = salesOrderService.CreateSalesOrder(new CreateSalesOrderRequest(
            BranchId: 1,
            CustomerId: 2,
            OrderDate: new DateOnly(2026, 4, 8),
            CurrencyCode: "EGP",
            ExchangeRate: 1m,
            Lines:
            [
                new CreateSalesOrderLineRequest(1001, 1, 8m, "EA", 32m, "VAT14"),
                new CreateSalesOrderLineRequest(1002, 1, 5m, "BOX", 49m, "VAT14")
            ]));

        var submittedSalesOrder = salesOrderService.SubmitSalesOrder(salesOrder.SalesOrderId, 2);
        Assert.Equal("SUBMITTED", submittedSalesOrder.StatusCode);

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

        var postedDispatch = dispatchService.PostDispatchOrder(dispatch.DispatchOrderId, new PostDispatchOrderRequest(
            ActorUserId: 6,
            ShippedAtUtc: new DateTime(2026, 4, 8, 12, 0, 0, DateTimeKind.Utc),
            DeliveredAtUtc: new DateTime(2026, 4, 8, 15, 0, 0, DateTimeKind.Utc),
            ProofOfDeliveryReceiver: "Cairo Retail Receiver"));
        Assert.Equal("POSTED", postedDispatch.StatusCode);

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

        var submittedInvoice = invoiceService.SubmitInvoice(invoice.AccountsReceivableInvoiceId, 3);
        Assert.Equal("SUBMITTED", submittedInvoice.StatusCode);

        var approvedInvoice = invoiceService.ApproveInvoice(invoice.AccountsReceivableInvoiceId, 4);
        Assert.Equal("APPROVED", approvedInvoice.StatusCode);

        var postedInvoice = invoiceService.PostInvoice(invoice.AccountsReceivableInvoiceId, 4);
        Assert.Equal("POSTED", postedInvoice.StatusCode);
        Assert.NotNull(postedInvoice.GeneralLedgerBatchId);

        var receipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 9),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: postedInvoice.TotalAmount,
            ReferenceNumber: "BANK-AR-0002",
            Notes: "Customer paid full invoice",
            Allocations:
            [
                new CreateAccountsReceivableReceiptAllocationRequest(postedInvoice.AccountsReceivableInvoiceId, postedInvoice.TotalAmount, new DateOnly(2026, 4, 9))
            ]));

        var submittedReceipt = receiptService.SubmitReceipt(receipt.AccountsReceivableReceiptId, 3);
        Assert.Equal("SUBMITTED", submittedReceipt.StatusCode);

        var approvedReceipt = receiptService.ApproveReceipt(receipt.AccountsReceivableReceiptId, 4);
        Assert.Equal("APPROVED", approvedReceipt.StatusCode);

        var postedReceipt = receiptService.PostReceipt(receipt.AccountsReceivableReceiptId, 4);
        Assert.Equal("POSTED", postedReceipt.StatusCode);
        Assert.Equal(0m, postedReceipt.UnappliedAmount);
        Assert.NotNull(postedReceipt.GeneralLedgerBatchId);
    }

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

        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));

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

        invoiceService.SubmitInvoice(invoice.AccountsReceivableInvoiceId, 3);

        var invoiceException = Assert.Throws<InvalidOperationException>(() =>
            invoiceService.PostInvoice(invoice.AccountsReceivableInvoiceId, 4));
        Assert.Contains("approved", invoiceException.Message, StringComparison.OrdinalIgnoreCase);

        var postedInvoice = invoiceService.PostInvoice(
            invoiceService.ApproveInvoice(invoice.AccountsReceivableInvoiceId, 4).AccountsReceivableInvoiceId,
            4);

        var receipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 4, 9),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: postedInvoice.TotalAmount,
            ReferenceNumber: "BANK-AR-CHECK-0001",
            Notes: "Approval control receipt",
            Allocations:
            [
                new CreateAccountsReceivableReceiptAllocationRequest(postedInvoice.AccountsReceivableInvoiceId, postedInvoice.TotalAmount, new DateOnly(2026, 4, 9))
            ]));

        receiptService.SubmitReceipt(receipt.AccountsReceivableReceiptId, 3);

        var receiptException = Assert.Throws<InvalidOperationException>(() =>
            receiptService.PostReceipt(receipt.AccountsReceivableReceiptId, 4));
        Assert.Contains("approved", receiptException.Message, StringComparison.OrdinalIgnoreCase);
    }

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

        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));

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

        var wrongCustomerException = Assert.Throws<InvalidOperationException>(() =>
            receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
                CustomerId: 999,
                ReceiptDate: new DateOnly(2026, 4, 11),
                ReceiptMethod: "BANK_TRANSFER",
                CurrencyCode: "EGP",
                AmountAmount: postedInvoice.TotalAmount,
                ReferenceNumber: "BANK-AR-ALLOC-0001",
                Notes: "Wrong customer allocation",
                Allocations:
                [
                    new CreateAccountsReceivableReceiptAllocationRequest(postedInvoice.AccountsReceivableInvoiceId, postedInvoice.TotalAmount, new DateOnly(2026, 4, 11))
                ])));
        Assert.Contains("different customer", wrongCustomerException.Message, StringComparison.OrdinalIgnoreCase);

        var overAllocationException = Assert.Throws<InvalidOperationException>(() =>
            receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
                CustomerId: 2,
                ReceiptDate: new DateOnly(2026, 4, 11),
                ReceiptMethod: "BANK_TRANSFER",
                CurrencyCode: "EGP",
                AmountAmount: postedInvoice.TotalAmount + 1m,
                ReferenceNumber: "BANK-AR-ALLOC-0002",
                Notes: "Over allocation",
                Allocations:
                [
                    new CreateAccountsReceivableReceiptAllocationRequest(postedInvoice.AccountsReceivableInvoiceId, postedInvoice.TotalAmount + 1m, new DateOnly(2026, 4, 11))
                ])));
        Assert.Contains("outstanding", overAllocationException.Message, StringComparison.OrdinalIgnoreCase);
    }

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

        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));

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

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

        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(
            receiptService,
            receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
                CustomerId: 2,
                ReceiptDate: new DateOnly(2026, 4, 14),
                ReceiptMethod: "BANK_TRANSFER",
                CurrencyCode: "EGP",
                AmountAmount: postedInvoiceOne.TotalAmount,
                ReferenceNumber: "BANK-AR-REALLOC-0001",
                Notes: "Original receipt allocation",
                Allocations:
                [
                    new CreateAccountsReceivableReceiptAllocationRequest(postedInvoiceOne.AccountsReceivableInvoiceId, postedInvoiceOne.TotalAmount, new DateOnly(2026, 4, 14))
                ])).AccountsReceivableReceiptId);

        var reversedReceipt = receiptService.ReverseReceipt(
            postedReceipt.AccountsReceivableReceiptId,
            4,
            new ReverseAccountsReceivableReceiptRequest(
                ReversalDate: new DateOnly(2026, 4, 15),
                ReasonCode: "MISAPPLIED",
                Notes: "Receipt was allocated to the wrong customer invoice."));

        Assert.Equal("REVERSED", reversedReceipt.StatusCode);
        Assert.Equal("MISAPPLIED", reversedReceipt.ReversalReasonCode);
        Assert.NotNull(reversedReceipt.ReversalGeneralLedgerBatchId);

        var replacementReceipt = receiptService.ReallocateReceipt(
            reversedReceipt.AccountsReceivableReceiptId,
            4,
            new ReallocateAccountsReceivableReceiptRequest(
                ReceiptDate: new DateOnly(2026, 4, 15),
                ReceiptMethod: "BANK_TRANSFER",
                ReferenceNumber: "BANK-AR-REALLOC-0001-R1",
                Notes: "Replacement allocation after reversal.",
                Allocations:
                [
                    new CreateAccountsReceivableReceiptAllocationRequest(postedInvoiceTwo.AccountsReceivableInvoiceId, postedInvoiceTwo.TotalAmount, new DateOnly(2026, 4, 15))
                ]));

        Assert.Equal("DRAFT", replacementReceipt.StatusCode);
        Assert.Equal(reversedReceipt.AccountsReceivableReceiptId, replacementReceipt.ReallocatedFromReceiptId);

        var refreshedReversedReceipt = receiptService.GetReceipt(reversedReceipt.AccountsReceivableReceiptId);
        Assert.NotNull(refreshedReversedReceipt);
        Assert.Equal(replacementReceipt.AccountsReceivableReceiptId, refreshedReversedReceipt!.ReallocationReceiptId);

        var repostedReplacementReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(
            receiptService,
            replacementReceipt.AccountsReceivableReceiptId);

        Assert.Equal("POSTED", repostedReplacementReceipt.StatusCode);
        Assert.NotNull(repostedReplacementReceipt.GeneralLedgerBatchId);
    }

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

        var salesOrderService = new SalesOrderService(new SalesOrderRepository(dbContext));
        var dispatchService = new DispatchOrderService(new DispatchOrderRepository(dbContext));

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

        var secondOrder = salesOrderService.CreateSalesOrder(new CreateSalesOrderRequest(
            BranchId: 1,
            CustomerId: 2,
            OrderDate: new DateOnly(2026, 4, 20),
            CurrencyCode: "EGP",
            ExchangeRate: 1m,
            Lines:
            [
                new CreateSalesOrderLineRequest(1002, 1, 1m, "EA", 49m, "VAT14")
            ]));

        salesOrderService.SubmitSalesOrder(firstOrder.SalesOrderId, 2);
        salesOrderService.SubmitSalesOrder(secondOrder.SalesOrderId, 2);

        var foreignLineException = Assert.Throws<InvalidOperationException>(() =>
            dispatchService.CreateDispatchOrder(new CreateDispatchOrderRequest(
                BranchId: 1,
                SalesOrderId: firstOrder.SalesOrderId,
                DispatchWarehouseId: 1,
                PlannedShipAtUtc: new DateTime(2026, 4, 20, 10, 0, 0, DateTimeKind.Utc),
                Lines:
                [
                    new CreateDispatchOrderLineRequest(secondOrder.Lines.Single().SalesOrderLineId, 1002, 1m, null, "BATCH-FGN-0001", null)
                ])));

        Assert.Contains("source sales order", foreignLineException.Message, StringComparison.OrdinalIgnoreCase);

        var overAllocationException = Assert.Throws<InvalidOperationException>(() =>
            dispatchService.CreateDispatchOrder(new CreateDispatchOrderRequest(
                BranchId: 1,
                SalesOrderId: firstOrder.SalesOrderId,
                DispatchWarehouseId: 1,
                PlannedShipAtUtc: new DateTime(2026, 4, 20, 10, 30, 0, DateTimeKind.Utc),
                Lines:
                [
                    new CreateDispatchOrderLineRequest(firstOrder.Lines.Single().SalesOrderLineId, 1001, 3m, null, "BATCH-OVER-0001", null)
                ])));

        Assert.Contains("remaining unallocated", overAllocationException.Message, StringComparison.OrdinalIgnoreCase);
    }

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

        var dispatchService = new DispatchOrderService(new DispatchOrderRepository(dbContext));

        var postedDispatch = dispatchService.PostDispatchOrder(1, new PostDispatchOrderRequest(
            ActorUserId: 3,
            ShippedAtUtc: new DateTime(2026, 5, 12, 8, 30, 0, DateTimeKind.Utc),
            DeliveredAtUtc: new DateTime(2026, 5, 12, 12, 0, 0, DateTimeKind.Utc),
            ProofOfDeliveryReceiver: "Original receiver"));

        Assert.Equal("POSTED", postedDispatch.StatusCode);
        Assert.All(postedDispatch.Lines, line => Assert.True(line.QuantityDelivered > 0m));

        var proofException = Assert.Throws<InvalidOperationException>(() =>
            dispatchService.RecordProofOfDelivery(1, 4, new RecordProofOfDeliveryRequest(
                DeliveredAtUtc: new DateTime(2026, 5, 13, 11, 0, 0, DateTimeKind.Utc),
                ProofOfDeliveryReceiver: "Warehouse Receiver",
                DeliveryConditionCode: "ACCEPTED",
                ProofOfDeliveryReference: "POD-RETRY-001",
                Notes: "Late duplicate capture")));

        Assert.Contains("cannot be captured again", proofException.Message, StringComparison.OrdinalIgnoreCase);

        var exceptionCapture = Assert.Throws<InvalidOperationException>(() =>
            dispatchService.RecordDeliveryException(1, 4, new RecordDeliveryExceptionRequest(
                DeliveredAtUtc: new DateTime(2026, 5, 13, 11, 30, 0, DateTimeKind.Utc),
                DeliveryConditionCode: "FAILED",
                DeliveryExceptionCode: "LATE_EXCEPTION",
                ProofOfDeliveryReceiver: "Driver",
                ProofOfDeliveryReference: "FD-LATE-001",
                Notes: "Late duplicate exception capture",
                Lines:
                [
                    new RecordDeliveryExceptionLineRequest(1, 1m)
                ])));

        Assert.Contains("cannot be captured again", exceptionCapture.Message, StringComparison.OrdinalIgnoreCase);
    }
}
