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

namespace Baya.Test.Foundation.Refunds;

public class RefundHandlerTests
{
    private static readonly DateTimeOffset Now = new(2026, 8, 10, 10, 0, 0, TimeSpan.Zero);

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

    private static CreateRefundCommand FullRefund(long bookingId)
        => new(bookingId, TicketId: null, RefundPercentage: 1m, null, null, "customer_request", null, null, null);

    [Fact]
    public async Task PrePayout_full_refund_posts_balanced_reversal_and_clearing()
    {
        using var host = new RefundsTestHost();
        var (bookingId, _) = host.SeedCapturedBooking(gross: 10_000_000, commission: 1_500_000);

        var result = await Handler(host, nursePaid: false).Handle(FullRefund(bookingId), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(RefundStatus.Succeeded, result.Result.Status);
        Assert.Equal("10000000", result.Result.Amount);
        Assert.Equal("1500000", result.Result.PlatformFeeRefundedIrr);
        Assert.Equal("8500000", result.Result.NursePayoutRefundedIrr);
        Assert.Null(result.Result.ClawbackId);

        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));

        Assert.Equal(1_500_000, Leg(legs, LedgerAccountType.PlatformRevenue, LedgerDirection.Debit));
        Assert.Equal(8_500_000, Leg(legs, LedgerAccountType.NursePayable, LedgerDirection.Debit));
        Assert.Equal(10_000_000, Leg(legs, LedgerAccountType.RefundPayable, LedgerDirection.Credit));
        Assert.Equal(10_000_000, Leg(legs, LedgerAccountType.RefundPayable, LedgerDirection.Debit));
        Assert.Equal(10_000_000, Leg(legs, LedgerAccountType.EscrowHeld, LedgerDirection.Credit));

        // No clawback pre-payout.
        Assert.Empty(host.Db.Set<NurseClawback>().AsNoTracking().ToList());
    }

    [Fact]
    public async Task Partial_refund_decomposes_legs_and_second_over_refund_is_rejected()
    {
        using var host = new RefundsTestHost();
        var (bookingId, _) = host.SeedCapturedBooking(gross: 10_000_000, commission: 1_500_000);
        var handler = Handler(host, nursePaid: false);

        var half = new CreateRefundCommand(bookingId, null, RefundPercentage: 0.5m, null, null, "shortened_visit", null, null, null);
        var first = await handler.Handle(half, CancellationToken.None);

        Assert.True(first.IsSuccess);
        Assert.Equal("5000000", first.Result.Amount);
        Assert.Equal("750000", first.Result.PlatformFeeRefundedIrr);
        Assert.Equal("4250000", first.Result.NursePayoutRefundedIrr);

        // A second 60% refund would push the total to 11M > 10M captured → rejected, no new ledger.
        var ledgerBefore = host.LedgerFor(bookingId).Count;
        var tooMuch = new CreateRefundCommand(bookingId, null, RefundPercentage: 0.6m, null, null, "again", null, null, null);
        var second = await handler.Handle(tooMuch, CancellationToken.None);

        Assert.False(second.IsSuccess);
        Assert.True(second.IsConflict);
        Assert.Equal(ledgerBefore, host.LedgerFor(bookingId).Count);
    }

    [Fact]
    public async Task PostPayout_refund_opens_clawback_and_posts_receivable_leg()
    {
        using var host = new RefundsTestHost();
        var (bookingId, _) = host.SeedCapturedBooking(gross: 10_000_000, commission: 1_500_000);

        var result = await Handler(host, nursePaid: true).Handle(FullRefund(bookingId), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.NotNull(result.Result.ClawbackId);

        var legs = host.LedgerFor(bookingId);
        // The payout leg debits the receivable (not nurse_payable); the platform fee leg is unchanged.
        Assert.Equal(8_500_000, Leg(legs, LedgerAccountType.NurseClawbackReceivable, LedgerDirection.Debit));
        Assert.Equal(0, legs.Count(l => l.AccountType == LedgerAccountType.NursePayable));
        Assert.Equal(
            legs.Where(l => l.Direction == LedgerDirection.Debit).Sum(l => l.AmountIrr),
            legs.Where(l => l.Direction == LedgerDirection.Credit).Sum(l => l.AmountIrr));

        var clawback = Assert.Single(host.Db.Set<NurseClawback>().AsNoTracking().ToList());
        Assert.Equal(ClawbackStatus.Pending, clawback.Status);
        Assert.Equal(8_500_000, clawback.AmountIrr);
        Assert.Equal(host.NurseId, clawback.NurseId);
    }

    [Fact]
    public async Task Card_and_bnpl_post_the_same_reversal_legs()
    {
        using var cardHost = new RefundsTestHost();
        var (cardBooking, _) = cardHost.SeedCapturedBooking(gross: 10_000_000, commission: 1_500_000, gatewayType: PaymentGatewayType.Standard);
        var cardResult = await Handler(cardHost, nursePaid: false).Handle(FullRefund(cardBooking), CancellationToken.None);

        using var bnplHost = new RefundsTestHost();
        var (bnplBooking, _) = bnplHost.SeedCapturedBooking(gross: 10_000_000, commission: 1_500_000, gatewayType: PaymentGatewayType.Bnpl);
        var bnplResult = await Handler(bnplHost, nursePaid: false).Handle(FullRefund(bnplBooking), CancellationToken.None);

        Assert.Equal(RefundChannel.PspCard, cardResult.Result.RefundChannel);
        Assert.Equal(RefundChannel.BnplRevert, bnplResult.Result.RefundChannel);
        // The card refund is immediate (succeeded, no ETA); the BNPL revert waits in processing with an ETA.
        Assert.Equal(RefundStatus.Succeeded, cardResult.Result.Status);
        Assert.Null(cardResult.Result.ExpectedCustomerRefundEta);
        Assert.Equal(RefundStatus.Processing, bnplResult.Result.Status);
        Assert.NotNull(bnplResult.Result.ExpectedCustomerRefundEta);

        // Reversal legs (account × direction × amount) are identical across channels.
        var cardReversal = Reversal(cardHost.LedgerFor(cardBooking));
        var bnplReversal = Reversal(bnplHost.LedgerFor(bnplBooking));
        Assert.Equal(cardReversal, bnplReversal);
    }

    private static long Leg(IReadOnlyList<LedgerEntry> legs, string account, string direction)
        => legs.Where(l => l.AccountType == account && l.Direction == direction).Sum(l => l.AmountIrr);

    private static IReadOnlyList<(string, string, long)> Reversal(IReadOnlyList<LedgerEntry> legs)
        => legs.Where(l => l.SourceRefType == LedgerSourceRefType.Refund && l.AccountType != LedgerAccountType.EscrowHeld)
            .Where(l => !(l.AccountType == LedgerAccountType.RefundPayable && l.Direction == LedgerDirection.Debit))
            .Select(l => (l.AccountType, l.Direction, l.AmountIrr))
            .OrderBy(t => t.Item1).ThenBy(t => t.Item2).ToList();
}
