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

namespace CashLineERP.Api.Tests;

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

        var billService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

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

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

        var createdPayment = paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: 1,
            PaymentDate: new DateOnly(2026, 4, 15),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: postedBill.TotalAmount,
            ReferenceNumber: "SUPP-PAY-0001",
            Notes: "Supplier settlement QA flow",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, postedBill.TotalAmount, new DateOnly(2026, 4, 15))
            ]));

        var submittedPayment = paymentService.SubmitPayment(createdPayment.AccountsPayablePaymentId, 3);
        Assert.Equal("SUBMITTED", submittedPayment.StatusCode);

        var approvedPayment = paymentService.ApprovePayment(createdPayment.AccountsPayablePaymentId, 4);
        Assert.Equal("APPROVED", approvedPayment.StatusCode);
        Assert.Equal(3, approvedPayment.SubmittedByUserId);
        Assert.Equal(4, approvedPayment.ApprovedByUserId);
        Assert.NotNull(approvedPayment.ApprovedAtUtc);

        var postedPayment = paymentService.PostPayment(createdPayment.AccountsPayablePaymentId, 4);
        Assert.Equal("POSTED", postedPayment.StatusCode);
        Assert.NotNull(postedPayment.GeneralLedgerBatchId);
        Assert.Equal(0m, postedPayment.UnappliedAmount);

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-AP-001",
            StatementDate: new DateOnly(2026, 4, 15),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 25000m,
            ClosingBalanceAmount: 21922m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 15),
                    ExternalReference: "SUPP-PAY-0001",
                    DescriptionText: "Supplier bank transfer",
                    DebitAmount: postedPayment.AmountAmount,
                    CreditAmount: 0m,
                    RunningBalanceAmount: 21922m)
            ]));

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

        Assert.Equal("COMPLETED", settlementRun.RunStatusCode);
        Assert.Equal(1, settlementRun.MatchedItemCount);
        Assert.Equal(0, settlementRun.UnmatchedItemCount);
        Assert.Single(settlementRun.Items);
        Assert.Equal("MATCHED", settlementRun.Items.Single().MatchStatusCode);
        Assert.Equal("AP_PAYMENT", settlementRun.Items.Single().MatchedDocumentType);
        Assert.Equal(postedPayment.AccountsPayablePaymentId, settlementRun.Items.Single().AccountsPayablePaymentId);

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

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

        var billService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));
        var reconciliationService = new ReconciliationService(new ReconciliationRepository(dbContext));

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

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

        var payment = paymentService.PostPayment(
            paymentService.ApprovePayment(
                paymentService.SubmitPayment(
                paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
                    SupplierId: 1,
                    PaymentDate: new DateOnly(2026, 4, 17),
                    PaymentMethod: "BANK_TRANSFER",
                    CurrencyCode: "EGP",
                    AmountAmount: postedBill.TotalAmount,
                    ReferenceNumber: "SUPP-PAY-EX-0001",
                    Notes: "Supplier settlement exception QA flow",
                    Allocations:
                    [
                        new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, postedBill.TotalAmount, new DateOnly(2026, 4, 17))
                    ])).AccountsPayablePaymentId,
                3).AccountsPayablePaymentId,
                4).AccountsPayablePaymentId,
            4);

        var bankStatement = reconciliationService.CreateBankStatement(new CreateBankStatementRequest(
            BankAccountCode: "CIB-EGP-AP-002",
            StatementDate: new DateOnly(2026, 4, 17),
            CurrencyCode: "EGP",
            OpeningBalanceAmount: 10000m,
            ClosingBalanceAmount: 8450m,
            Lines:
            [
                new CreateBankStatementLineRequest(
                    TransactionDate: new DateOnly(2026, 4, 17),
                    ExternalReference: "SUPP-PAY-EX-0001",
                    DescriptionText: "Supplier bank transfer mismatch",
                    DebitAmount: payment.AmountAmount - 100m,
                    CreditAmount: 0m,
                    RunningBalanceAmount: 8450m)
            ]));

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

        Assert.Equal("COMPLETED", settlementRun.RunStatusCode);
        Assert.Equal(0, settlementRun.MatchedItemCount);
        Assert.Equal(1, settlementRun.ExceptionItemCount);
        Assert.Equal(0, settlementRun.UnmatchedItemCount);
        Assert.Single(settlementRun.Items);
        Assert.Equal("EXCEPTION", settlementRun.Items.Single().MatchStatusCode);
        Assert.Equal("AP_PAYMENT", settlementRun.Items.Single().MatchedDocumentType);
        Assert.Equal(payment.AccountsPayablePaymentId, settlementRun.Items.Single().AccountsPayablePaymentId);

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

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

        var billService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));

        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, 50m, "EA", 24m, "VAT14")
            ]));

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

        var createdPayment = paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: 1,
            PaymentDate: new DateOnly(2026, 4, 18),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: postedBill.TotalAmount,
            ReferenceNumber: "SUPP-PAY-MKCK-0001",
            Notes: "Maker checker validation",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, postedBill.TotalAmount, new DateOnly(2026, 4, 18))
            ]));

        var submittedPayment = paymentService.SubmitPayment(createdPayment.AccountsPayablePaymentId, 3);
        Assert.Equal("SUBMITTED", submittedPayment.StatusCode);

        var selfApproval = Record.Exception(() => paymentService.ApprovePayment(createdPayment.AccountsPayablePaymentId, 3));
        Assert.NotNull(selfApproval);
        Assert.IsType<InvalidOperationException>(selfApproval);
        Assert.Contains("self-approval", selfApproval.Message, StringComparison.OrdinalIgnoreCase);

        var postBeforeApproval = Record.Exception(() => paymentService.PostPayment(createdPayment.AccountsPayablePaymentId, 4));
        Assert.NotNull(postBeforeApproval);
        Assert.IsType<InvalidOperationException>(postBeforeApproval);
        Assert.Contains("approved", postBeforeApproval.Message, StringComparison.OrdinalIgnoreCase);

        var approvedPayment = paymentService.ApprovePayment(createdPayment.AccountsPayablePaymentId, 4);
        Assert.Equal("APPROVED", approvedPayment.StatusCode);
        Assert.Equal(3, approvedPayment.SubmittedByUserId);
        Assert.Equal(4, approvedPayment.ApprovedByUserId);

        var postedPayment = paymentService.PostPayment(createdPayment.AccountsPayablePaymentId, 4);
        Assert.Equal("POSTED", postedPayment.StatusCode);
        Assert.Equal(4, postedPayment.PostedByUserId);
    }

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

        var billService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));

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

        billService.SubmitBill(firstBill.AccountsPayableBillId, 3);
        billService.ApproveMatch(firstBill.AccountsPayableBillId, 4);
        var postedFirstBill = billService.PostBill(firstBill.AccountsPayableBillId, 4);

        var secondBill = billService.CreateBill(new CreateAccountsPayableBillRequest(
            SupplierId: 1,
            PurchaseOrderId: 1,
            GoodsReceiptId: 1,
            BillDate: new DateOnly(2026, 4, 19),
            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 payment = paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: 1,
            PaymentDate: new DateOnly(2026, 4, 19),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 2000m,
            ReferenceNumber: "SUPP-PAY-PARTIAL-0001",
            Notes: "Partial and multi-bill settlement",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(postedFirstBill.AccountsPayableBillId, postedFirstBill.TotalAmount, new DateOnly(2026, 4, 19)),
                new CreateAccountsPayablePaymentAllocationRequest(postedSecondBill.AccountsPayableBillId, 400m, new DateOnly(2026, 4, 19))
            ]));

        Assert.Equal(2, payment.Allocations.Count);
        Assert.Equal(232m, payment.UnappliedAmount);

        var firstAllocation = payment.Allocations.Single(allocation => allocation.AccountsPayableBillId == postedFirstBill.AccountsPayableBillId);
        Assert.Equal(postedFirstBill.BillNumber, firstAllocation.AccountsPayableBillNumber);
        Assert.Equal(postedFirstBill.TotalAmount, firstAllocation.AccountsPayableBillTotalAmount);
        Assert.Equal(0m, firstAllocation.PreviouslyAllocatedAmount);
        Assert.Equal(0m, firstAllocation.OutstandingAmountAfterAllocation);

        var secondAllocation = payment.Allocations.Single(allocation => allocation.AccountsPayableBillId == postedSecondBill.AccountsPayableBillId);
        Assert.Equal(postedSecondBill.BillNumber, secondAllocation.AccountsPayableBillNumber);
        Assert.Equal(postedSecondBill.TotalAmount, secondAllocation.AccountsPayableBillTotalAmount);
        Assert.Equal(0m, secondAllocation.PreviouslyAllocatedAmount);
        Assert.Equal(postedSecondBill.TotalAmount - 400m, secondAllocation.OutstandingAmountAfterAllocation);
    }

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

        var billService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));

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

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

        var firstPayment = paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: 1,
            PaymentDate: new DateOnly(2026, 4, 20),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: postedBill.TotalAmount,
            ReferenceNumber: "SUPP-PAY-FULL-0001",
            Notes: "Full settlement",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, postedBill.TotalAmount, new DateOnly(2026, 4, 20))
            ]));

        Assert.Equal(0m, firstPayment.UnappliedAmount);

        var overAllocation = Record.Exception(() => paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: 1,
            PaymentDate: new DateOnly(2026, 4, 21),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 100m,
            ReferenceNumber: "SUPP-PAY-OVER-0001",
            Notes: "Over allocation attempt",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, 100m, new DateOnly(2026, 4, 21))
            ])));

        Assert.NotNull(overAllocation);
        Assert.IsType<InvalidOperationException>(overAllocation);
        Assert.Contains("outstanding amount", overAllocation.Message, StringComparison.OrdinalIgnoreCase);
    }

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

        var billService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));

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

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

        var postedPayment = paymentService.PostPayment(
            paymentService.ApprovePayment(
                paymentService.SubmitPayment(
                    paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
                        SupplierId: 1,
                        PaymentDate: new DateOnly(2026, 4, 21),
                        PaymentMethod: "BANK_TRANSFER",
                        CurrencyCode: "EGP",
                        AmountAmount: postedBill.TotalAmount,
                        ReferenceNumber: "SUPP-PAY-RVS-0001",
                        Notes: "Original supplier payment",
                        Allocations:
                        [
                            new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, postedBill.TotalAmount, new DateOnly(2026, 4, 21))
                        ])).AccountsPayablePaymentId,
                    3).AccountsPayablePaymentId,
                4).AccountsPayablePaymentId,
            4);

        var reversedPayment = paymentService.ReversePayment(
            postedPayment.AccountsPayablePaymentId,
            4,
            new ReverseAccountsPayablePaymentRequest(
                ReversalDate: new DateOnly(2026, 4, 22),
                ReasonCode: "BANK_ERROR",
                Notes: "Supplier payment must be re-issued."));

        Assert.Equal("REVERSED", reversedPayment.StatusCode);
        Assert.Equal("BANK_ERROR", reversedPayment.ReversalReasonCode);
        Assert.NotNull(reversedPayment.ReversalGeneralLedgerBatchId);

        var replacementPayment = paymentService.RepostPayment(
            reversedPayment.AccountsPayablePaymentId,
            4,
            new RepostAccountsPayablePaymentRequest(
                PaymentDate: new DateOnly(2026, 4, 22),
                PaymentMethod: "BANK_TRANSFER",
                ReferenceNumber: "SUPP-PAY-RVS-0001-R1",
                Notes: "Replacement supplier payment.",
                Allocations:
                [
                    new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, postedBill.TotalAmount, new DateOnly(2026, 4, 22))
                ]));

        Assert.Equal("DRAFT", replacementPayment.StatusCode);
        Assert.Equal(reversedPayment.AccountsPayablePaymentId, replacementPayment.RepostedFromPaymentId);

        var refreshedReversedPayment = paymentService.GetPayment(reversedPayment.AccountsPayablePaymentId);
        Assert.NotNull(refreshedReversedPayment);
        Assert.Equal(replacementPayment.AccountsPayablePaymentId, refreshedReversedPayment!.RepostPaymentId);

        var repostedPayment = paymentService.PostPayment(
            paymentService.ApprovePayment(
                paymentService.SubmitPayment(replacementPayment.AccountsPayablePaymentId, 3).AccountsPayablePaymentId,
                4).AccountsPayablePaymentId,
            4);

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