using Baya.Application.Features.Bnpl.Commands.InitiateBnplOrder;
using Baya.Application.Features.Bnpl.Commands.SettleBnplOrder;
using Baya.Application.Features.Bnpl.Queries.CheckBnplEligibility;
using Baya.Domain.Entities.Bnpl;
using Baya.Domain.Entities.Payments;
using Microsoft.EntityFrameworkCore;
using BookingEntity = Baya.Domain.Entities.Booking.Booking;

namespace Baya.Test.Foundation.Bnpl;

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

    [Fact]
    public async Task Eligibility_creates_row_eligible_and_returns_plan()
    {
        using var host = new BnplTestHost();
        var requestId = host.AddAcceptedRequest(10_000_000);

        var handler = new CheckBnplEligibilityQueryHandler(host.AsCustomer(), host.UnitOfWork, host.Resolver(), host.Config());
        var result = await handler.Handle(new CheckBnplEligibilityQuery(requestId, BnplProviderCodes.SnappPay), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.True(result.Result.IsEligible);
        Assert.Equal(BnplEligibilityStatus.Eligible, result.Result.EligibilityStatus);
        Assert.Equal(4, result.Result.InstallmentCount);

        var bnpl = host.Db.Set<BnplTransaction>().AsNoTracking().Single();
        Assert.Equal(BnplStatus.Eligible, bnpl.Status);
        Assert.Equal(BnplEligibilityStatus.Eligible, bnpl.EligibilityStatus);
        Assert.Equal(10_000_000, bnpl.OrderAmountIrr);
    }

    [Fact]
    public async Task Initiate_issues_token_and_is_strictly_one_to_one()
    {
        using var host = new BnplTestHost();
        var requestId = host.AddAcceptedRequest(10_000_000);

        var handler = new InitiateBnplOrderCommandHandler(
            host.AsCustomer(), host.UnitOfWork, host.Resolver(), host.Currency, host.Config(), host.Lock, host.Clock(Now));

        var first = await handler.Handle(new InitiateBnplOrderCommand(requestId, BnplProviderCodes.SnappPay, "idem-1"), CancellationToken.None);
        Assert.True(first.IsSuccess);
        Assert.Equal(BnplStatus.TokenIssued, first.Result.Status);
        Assert.False(string.IsNullOrEmpty(first.Result.ExternalPaymentToken));

        // A second initiate reuses the same 1:1 row (the UNIQUE(payment_transaction_id) guard) — no second order.
        var second = await handler.Handle(new InitiateBnplOrderCommand(requestId, BnplProviderCodes.SnappPay, "idem-1"), CancellationToken.None);
        Assert.True(second.IsSuccess);
        Assert.Equal(first.Result.BnplTransactionId, second.Result.BnplTransactionId);
        Assert.Equal(1, host.Db.Set<BnplTransaction>().AsNoTracking().Count());
    }

    [Fact]
    public async Task Settle_posts_net_of_fee_group_and_nurse_payout_is_invariant_to_method()
    {
        using var host = new BnplTestHost();
        var (bnplId, requestId, _) = host.SeedVerifiedOrder(10_000_000);

        var handler = SettleHandler(host, commissionRate: 0.10m);
        var result = await handler.Handle(new SettleBnplOrderCommand(bnplId), CancellationToken.None);
        Assert.True(result.IsSuccess);

        var bnpl = host.ReloadBnpl(bnplId);
        Assert.Equal(BnplStatus.Settled, bnpl.Status);
        Assert.Equal(1_000_000, bnpl.BnplCommissionIrr);   // 10% of 10M
        Assert.Equal(9_000_000, bnpl.SettledAmountIrr);    // net = order − commission

        var bookingId = host.Db.Set<BookingEntity>().AsNoTracking().Single(b => b.BookingRequestId == requestId).Id;
        var legs = host.LedgerFor(bookingId);

        Assert.Equal(5, legs.Count); // capture (3) + provider-fee (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 bnpl_fee_expense leg, and escrow reflecting NET cash (10M debit − 1M credit = 9M = settled).
        Assert.Equal(1_000_000, Leg(legs, LedgerAccountType.BnplFeeExpense, LedgerDirection.Debit));
        Assert.Equal(10_000_000, Leg(legs, LedgerAccountType.EscrowHeld, LedgerDirection.Debit));
        Assert.Equal(1_000_000, Leg(legs, LedgerAccountType.EscrowHeld, LedgerDirection.Credit));

        // Payout invariant to method: nurse_payable = gross − commission(0.15) = 8.5M, independent of the BNPL fee.
        Assert.Equal(8_500_000, Leg(legs, LedgerAccountType.NursePayable, LedgerDirection.Credit));
        Assert.Equal(1_500_000, Leg(legs, LedgerAccountType.PlatformRevenue, LedgerDirection.Credit));

        // Identical to the card-capture path (the DoD payout-invariance assertion).
        var card = LedgerPosting.CardCapture(bookingId, host.NurseId, 10_000_000, 1_500_000, 8_500_000, 1, Now.UtcDateTime);
        Assert.Equal(
            card.Single(l => l.AccountType == LedgerAccountType.NursePayable).AmountIrr,
            Leg(legs, LedgerAccountType.NursePayable, LedgerDirection.Credit));
    }

    [Fact]
    public async Task Replayed_settle_is_a_noop_no_second_ledger_group()
    {
        using var host = new BnplTestHost();
        var (bnplId, requestId, _) = host.SeedVerifiedOrder(10_000_000);
        var handler = SettleHandler(host, commissionRate: 0.10m);

        await handler.Handle(new SettleBnplOrderCommand(bnplId), CancellationToken.None);
        var bookingId = host.Db.Set<BookingEntity>().AsNoTracking().Single(b => b.BookingRequestId == requestId).Id;
        var before = host.LedgerFor(bookingId).Count;

        var replay = await handler.Handle(new SettleBnplOrderCommand(bnplId), CancellationToken.None);
        Assert.True(replay.IsSuccess); // state guard → idempotent no-op
        Assert.Equal(before, host.LedgerFor(bookingId).Count);
    }

    [Fact]
    public async Task Settle_before_verify_is_rejected_by_the_state_guard()
    {
        using var host = new BnplTestHost();
        var requestId = host.AddAcceptedRequest(10_000_000);

        // Drive only to token_issued (skip verify) via the real initiate handler.
        var initiate = new InitiateBnplOrderCommandHandler(
            host.AsCustomer(), host.UnitOfWork, host.Resolver(), host.Currency, host.Config(), host.Lock, host.Clock(Now));
        var order = await initiate.Handle(new InitiateBnplOrderCommand(requestId, BnplProviderCodes.SnappPay, "idem-1"), CancellationToken.None);

        var settle = SettleHandler(host, commissionRate: 0.10m);
        var result = await settle.Handle(new SettleBnplOrderCommand(order.Result.BnplTransactionId), CancellationToken.None);

        Assert.False(result.IsSuccess);
        Assert.True(result.IsConflict);
        Assert.Empty(host.Db.Set<LedgerEntry>().AsNoTracking().ToList());
    }

    private static SettleBnplOrderCommandHandler SettleHandler(BnplTestHost host, decimal commissionRate)
        => new(host.UnitOfWork, host.Resolver(commissionRate), host.Config(), host.Serializer,
            host.Settlement, host.Lock, host.Clock(Now), host.Notifications(), TestSenders.WithTicketHooks());

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