using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.SupportAlerts;
using Baya.Application.Features.Refunds.Commands.ConfirmRefundSettlement;
using Baya.Application.Features.Refunds.Commands.CreateRefund;
using Baya.Application.Features.Refunds.Commands.MarkRefundSettlementFailed;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.Refunds;
using Microsoft.EntityFrameworkCore;
using NSubstitute;

namespace Baya.Test.Foundation.Refunds;

/// <summary>
/// The refinement-phase-6 fix: a BNPL/manual refund that lands in <c>processing</c> can be settled (admin
/// confirm / provider cash-back callback) so the deferred <c>refund_payable ↔ escrow_held</c> clearing posts and
/// the ledger reconciles — previously that path was unreachable. Also covers <c>mark_failed</c> and replay
/// idempotency.
/// </summary>
public class RefundSettlementTests
{
    private static readonly DateTimeOffset Now = new(2026, 8, 10, 10, 0, 0, TimeSpan.Zero);
    private static readonly DateTimeOffset Later = new(2026, 8, 12, 10, 0, 0, TimeSpan.Zero);

    private static CreateRefundCommandHandler CreateHandler(RefundsTestHost host)
        => new(
            host.UnitOfWork, host.Lock(), host.Config(), host.Clock(Now), host.AsAdmin(),
            host.Card(), host.Bnpl(), host.PayoutStatus(paid: false),
            Substitute.For<ISupportAlertService>(), Substitute.For<INotificationDispatcher>(), host.Senders());

    private static ConfirmRefundSettlementCommandHandler ConfirmHandler(RefundsTestHost host, INotificationDispatcher notifications)
        => new(host.UnitOfWork, host.Lock(), host.Clock(Later), notifications);

    private static async Task<long> SeedProcessingBnplRefundAsync(RefundsTestHost host)
    {
        var (bookingId, _) = host.SeedCapturedBooking(gross: 10_000_000, commission: 1_500_000, gatewayType: PaymentGatewayType.Bnpl);
        var created = await CreateHandler(host).Handle(
            new CreateRefundCommand(bookingId, TicketId: null, RefundPercentage: 1m, null, null, "customer_request", null, null, null),
            CancellationToken.None);

        Assert.True(created.IsSuccess);
        Assert.Equal(RefundStatus.Processing, created.Result.Status); // BNPL waits — no clearing yet
        Assert.Equal(3, host.LedgerFor(bookingId).Count); // reversal only
        return created.Result.RefundId;
    }

    [Fact]
    public async Task Confirm_settlement_posts_the_deferred_clearing_and_the_ledger_reconciles()
    {
        using var host = new RefundsTestHost();
        var refundId = await SeedProcessingBnplRefundAsync(host);
        var bookingId = host.Db.Set<Refund>().AsNoTracking().Single(r => r.Id == refundId).BookingId;
        var notifications = Substitute.For<INotificationDispatcher>();

        var result = await ConfirmHandler(host, notifications).Handle(new ConfirmRefundSettlementCommand(refundId), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(RefundStatus.Succeeded, result.Result.Status);
        Assert.NotNull(result.Result.CompletedAt);

        var legs = host.LedgerFor(bookingId);
        Assert.Equal(5, legs.Count); // reversal (3) + clearing (2)
        Assert.Equal(
            legs.Where(l => l.Direction == LedgerDirection.Debit).Sum(l => l.AmountIrr),
            legs.Where(l => l.Direction == LedgerDirection.Credit).Sum(l => l.AmountIrr));
        // The clearing leg: refund_payable is fully drained (credit at reversal == debit at clearing) and escrow
        // is credited back the refunded total — i.e. escrow no longer overstates the held funds.
        Assert.Equal(10_000_000, legs.Where(l => l.AccountType == LedgerAccountType.RefundPayable && l.Direction == LedgerDirection.Debit).Sum(l => l.AmountIrr));
        Assert.Equal(10_000_000, legs.Where(l => l.AccountType == LedgerAccountType.EscrowHeld && l.Direction == LedgerDirection.Credit).Sum(l => l.AmountIrr));

        await notifications.Received(1).DispatchAsync(Arg.Any<Notification>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Replayed_confirm_settlement_is_an_idempotent_no_op()
    {
        using var host = new RefundsTestHost();
        var refundId = await SeedProcessingBnplRefundAsync(host);
        var bookingId = host.Db.Set<Refund>().AsNoTracking().Single(r => r.Id == refundId).BookingId;
        var handler = ConfirmHandler(host, Substitute.For<INotificationDispatcher>());

        var first = await handler.Handle(new ConfirmRefundSettlementCommand(refundId), CancellationToken.None);
        var replay = await handler.Handle(new ConfirmRefundSettlementCommand(refundId), CancellationToken.None);

        Assert.True(first.IsSuccess);
        Assert.True(replay.IsSuccess);
        Assert.Equal(RefundStatus.Succeeded, replay.Result.Status);
        // The clearing is posted exactly once — no double credit to escrow.
        Assert.Equal(5, host.LedgerFor(bookingId).Count);
    }

    [Fact]
    public async Task Confirm_settlement_of_a_missing_refund_is_not_found()
    {
        using var host = new RefundsTestHost();
        var result = await ConfirmHandler(host, Substitute.For<INotificationDispatcher>())
            .Handle(new ConfirmRefundSettlementCommand(999_999), CancellationToken.None);

        Assert.False(result.IsSuccess);
        Assert.True(result.IsNotFound);
    }

    [Fact]
    public async Task Mark_failed_transitions_processing_to_failed_without_posting_a_clearing()
    {
        using var host = new RefundsTestHost();
        var refundId = await SeedProcessingBnplRefundAsync(host);
        var bookingId = host.Db.Set<Refund>().AsNoTracking().Single(r => r.Id == refundId).BookingId;

        var handler = new MarkRefundSettlementFailedCommandHandler(host.UnitOfWork, host.Lock(), host.Clock(Later));
        var result = await handler.Handle(new MarkRefundSettlementFailedCommand(refundId, "bank_rejected"), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(RefundStatus.Failed, result.Result.Status);
        Assert.Equal(3, host.LedgerFor(bookingId).Count); // no clearing posted

        // A settled confirm can no longer be applied to a failed refund.
        var confirm = await ConfirmHandler(host, Substitute.For<INotificationDispatcher>())
            .Handle(new ConfirmRefundSettlementCommand(refundId), CancellationToken.None);
        Assert.False(confirm.IsSuccess);
        Assert.True(confirm.IsConflict);
    }
}
