using Baya.Application.Features.Payouts.Commands.ExecutePayoutBatch;
using Baya.Application.Features.Payouts.Commands.GeneratePayoutBatch;
using Baya.Application.Features.Payouts.Commands.RetryFailedPayout;
using Baya.Application.Features.Payouts.Queries.ComputeEligibleEarnings;
using Baya.Application.Contracts.Holidays;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.Payouts;
using Baya.Domain.Entities.Refunds;
using Microsoft.EntityFrameworkCore;

namespace Baya.Test.Foundation.Payouts;

public class PayoutHandlerTests
{
    private static readonly DateTimeOffset Now = new(2026, 7, 1, 10, 0, 0, TimeSpan.Zero);
    private static readonly DateTime Past = new(2026, 6, 15, 0, 0, 0, DateTimeKind.Utc);   // dispute window closed
    private static readonly DateTime Future = new(2026, 8, 1, 0, 0, 0, DateTimeKind.Utc);  // still open
    private static readonly DateOnly PeriodStart = new(2026, 6, 1);
    private static readonly DateOnly PeriodEnd = new(2026, 6, 30);

    private static GeneratePayoutBatchCommand GenCmd => new(PeriodStart, PeriodEnd);

    private static GeneratePayoutBatchCommandHandler Gen(PayoutsTestHost host, IHolidayCalendar? holidays = null)
        => new(host.UnitOfWork, host.Lock(), holidays ?? host.Holidays(), host.Config(), host.Clock(Now), host.AsAdmin());

    private static ExecutePayoutBatchCommandHandler Exec(PayoutsTestHost host, Baya.Application.Contracts.Payments.IBankTransferProvider? bank = null)
        => new(host.UnitOfWork, host.Lock(), bank ?? host.Bank(), host.Config(), host.Clock(Now));

    private static ComputeEligibleEarningsQueryHandler Preview(PayoutsTestHost host)
        => new(host.UnitOfWork, host.Holidays(), host.Config(), host.Clock(Now));

    [Fact]
    public async Task Preview_includes_only_closed_window_and_flags_missing_iban()
    {
        using var host = new PayoutsTestHost();
        var nurseA = host.SeedNurse();
        var nurseB = host.SeedNurse(verifiedIban: null);
        host.SeedCompletedBooking(nurseA, Past);              // eligible
        host.SeedCompletedBooking(nurseA, Future);            // future window — excluded
        host.SeedCompletedBooking(nurseA, Past, disputed: true); // disputed — excluded
        host.SeedCompletedBooking(nurseB, Past);             // eligible earnings but no verified IBAN

        var result = await Preview(host).Handle(new ComputeEligibleEarningsQuery(PeriodStart, PeriodEnd), CancellationToken.None);

        Assert.True(result.IsSuccess);
        var items = result.Result.Items;
        Assert.Equal(2, items.Count);

        var a = items.Single(x => x.NurseId == nurseA);
        Assert.Equal(1, a.BookingCount);                     // only the one closed-window, non-disputed booking
        Assert.Equal("8500000", a.GrossEarningsIrr);
        Assert.True(a.HasVerifiedPrimaryIban);

        var b = items.Single(x => x.NurseId == nurseB);
        Assert.False(b.HasVerifiedPrimaryIban);
    }

    [Fact]
    public async Task Generate_materializes_one_payout_per_nurse_and_nets_clawback()
    {
        using var host = new PayoutsTestHost();
        var nurse = host.SeedNurse();
        host.SeedCompletedBooking(nurse, Past, gross: 10_000_000, commission: 1_500_000); // eligible, payout 8.5M
        var prior = host.SeedCompletedBooking(nurse, Past);   // gets a refund below → excluded from earnings
        host.SeedPendingClawback(nurse, prior, 2_000_000);

        var result = await Gen(host).Handle(GenCmd, CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(PayoutBatchStatus.Draft, result.Result.Batch.Status);
        var payout = Assert.Single(result.Result.Payouts);
        Assert.Equal("8500000", payout.GrossEarningsIrr);
        Assert.Equal("2000000", payout.ClawbackAppliedIrr);
        Assert.Equal("6500000", payout.NetAmountIrr);
        Assert.Equal("6500000", payout.Amount);
        Assert.Equal(1, payout.BookingCount);
        Assert.Equal("6500000", result.Result.Batch.TotalAmount);
        Assert.Equal(1, result.Result.Batch.PayoutCount);
        Assert.EndsWith("0123", payout.MaskedIban);
        Assert.Contains("•", payout.MaskedIban);
        Assert.Empty(result.Result.Skipped);
    }

    [Fact]
    public async Task Generate_skips_nurse_without_verified_primary_iban_with_reason()
    {
        using var host = new PayoutsTestHost();
        var nurseOk = host.SeedNurse();
        var nurseNoIban = host.SeedNurse(verifiedIban: null);
        host.SeedCompletedBooking(nurseOk, Past);
        host.SeedCompletedBooking(nurseNoIban, Past);

        var result = await Gen(host).Handle(GenCmd, CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Single(result.Result.Payouts);
        var skipped = Assert.Single(result.Result.Skipped);
        Assert.Equal(nurseNoIban, skipped.NurseId);
        Assert.Equal("no_verified_primary_iban", skipped.Reason);
    }

    [Fact]
    public async Task Double_pay_guard_second_generate_does_not_reselect_linked_bookings()
    {
        using var host = new PayoutsTestHost();
        var nurse = host.SeedNurse();
        host.SeedCompletedBooking(nurse, Past);

        var first = await Gen(host).Handle(GenCmd, CancellationToken.None);
        Assert.True(first.IsSuccess);

        var second = await Gen(host).Handle(GenCmd, CancellationToken.None);
        Assert.False(second.IsSuccess); // the booking_id UNIQUE link excludes it — nothing left to pay
    }

    [Fact]
    public async Task Execute_posts_balanced_payout_ledger_and_drains_payable()
    {
        using var host = new PayoutsTestHost();
        var nurse = host.SeedNurse();
        host.SeedCompletedBooking(nurse, Past, gross: 10_000_000, commission: 1_500_000);

        var gen = await Gen(host).Handle(GenCmd, CancellationToken.None);
        var batchId = gen.Result.Batch.Id;
        var before = host.NursePayableBalance(nurse);

        var exec = await Exec(host).Handle(new ExecutePayoutBatchCommand(batchId), CancellationToken.None);

        Assert.True(exec.IsSuccess);
        Assert.Equal(PayoutBatchStatus.Completed, exec.Result.Status);
        Assert.Equal(1, exec.Result.PaidCount);

        var after = host.NursePayableBalance(nurse);
        Assert.Equal(8_500_000, before - after);

        var legs = PayoutLegs(host);
        Assert.Equal(8_500_000, Leg(legs, LedgerAccountType.NursePayable, LedgerDirection.Debit));
        Assert.Equal(8_500_000, Leg(legs, LedgerAccountType.EscrowHeld, LedgerDirection.Credit));
        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 payout = host.Db.Set<NursePayout>().AsNoTracking().Single();
        Assert.Equal(PayoutStatus.Paid, payout.Status);
        Assert.NotNull(payout.TransferReference);
        Assert.NotNull(payout.PaidAt);
    }

    [Fact]
    public async Task Execute_recovers_clawback_and_posts_recovery_leg()
    {
        using var host = new PayoutsTestHost();
        var nurse = host.SeedNurse();
        host.SeedCompletedBooking(nurse, Past, gross: 10_000_000, commission: 1_500_000);
        var prior = host.SeedCompletedBooking(nurse, Past);
        var clawbackId = host.SeedPendingClawback(nurse, prior, 2_000_000);

        var gen = await Gen(host).Handle(GenCmd, CancellationToken.None);
        await Exec(host).Handle(new ExecutePayoutBatchCommand(gen.Result.Batch.Id), CancellationToken.None);

        var recovery = host.Db.Set<LedgerEntry>().AsNoTracking()
            .Where(l => l.SourceRefType == LedgerSourceRefType.Clawback && l.SourceRefId == clawbackId).ToList();
        Assert.Equal(2_000_000, Leg(recovery, LedgerAccountType.NursePayable, LedgerDirection.Debit));
        Assert.Equal(2_000_000, Leg(recovery, LedgerAccountType.NurseClawbackReceivable, LedgerDirection.Credit));

        var clawback = host.Db.Set<NurseClawback>().AsNoTracking().Single(c => c.Id == clawbackId);
        Assert.Equal(ClawbackStatus.Recovered, clawback.Status);
        Assert.NotNull(clawback.RecoveredInPayoutId);
    }

    [Fact]
    public async Task Reprocess_is_idempotent_no_second_ledger_group()
    {
        using var host = new PayoutsTestHost();
        var nurse = host.SeedNurse();
        host.SeedCompletedBooking(nurse, Past);

        var gen = await Gen(host).Handle(GenCmd, CancellationToken.None);
        var batchId = gen.Result.Batch.Id;

        await Exec(host).Handle(new ExecutePayoutBatchCommand(batchId), CancellationToken.None);
        var countAfterFirst = PayoutLegs(host).Count;

        var second = await Exec(host).Handle(new ExecutePayoutBatchCommand(batchId), CancellationToken.None);
        Assert.True(second.IsSuccess);
        Assert.Equal(PayoutBatchStatus.Completed, second.Result.Status);
        Assert.Equal(countAfterFirst, PayoutLegs(host).Count);
    }

    [Fact]
    public async Task Holiday_shifts_period_end_and_processing_date()
    {
        using var host = new PayoutsTestHost();
        var nurse = host.SeedNurse();
        host.SeedCompletedBooking(nurse, Past);

        var shiftedEnd = new DateOnly(2026, 7, 2);
        var shiftedProcessing = new DateOnly(2026, 7, 4);
        var holidays = host.Holidays(
            (PeriodEnd, shiftedEnd),
            (DateOnly.FromDateTime(Now.UtcDateTime), shiftedProcessing));

        var result = await Gen(host, holidays).Handle(GenCmd, CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(shiftedEnd, result.Result.Batch.PeriodEnd);
        Assert.Equal(shiftedProcessing, result.Result.Batch.ProcessingDate);
    }

    [Fact]
    public async Task Partial_failure_then_retry_completes_the_batch()
    {
        using var host = new PayoutsTestHost();
        var nurseOk = host.SeedNurse("IR000000000000000000000001");
        var nurseFail = host.SeedNurse("IR000000000000000000000999");
        host.SeedCompletedBooking(nurseOk, Past);
        host.SeedCompletedBooking(nurseFail, Past);

        var gen = await Gen(host).Handle(GenCmd, CancellationToken.None);
        var batchId = gen.Result.Batch.Id;

        var failingBank = host.Bank(failIban: "IR000000000000000000000999");
        var exec = await Exec(host, failingBank).Handle(new ExecutePayoutBatchCommand(batchId), CancellationToken.None);

        Assert.Equal(PayoutBatchStatus.PartiallyFailed, exec.Result.Status);
        Assert.Equal(1, exec.Result.PaidCount);
        Assert.Equal(1, exec.Result.FailedCount);

        var failedPayout = host.Db.Set<NursePayout>().AsNoTracking().Single(p => p.Status == PayoutStatus.Failed);

        var retry = new RetryFailedPayoutCommandHandler(
            host.UnitOfWork, host.Lock(), host.Bank(), host.Holidays(), host.Config(), host.Clock(Now));
        var retried = await retry.Handle(new RetryFailedPayoutCommand(failedPayout.Id), CancellationToken.None);

        Assert.True(retried.IsSuccess);
        var batch = await host.Db.Set<NursePayoutBatch>().AsNoTracking().FirstAsync(b => b.Id == batchId);
        Assert.Equal(PayoutBatchStatus.Completed, batch.Status);
    }

    private static List<LedgerEntry> PayoutLegs(PayoutsTestHost host)
        => host.Db.Set<LedgerEntry>().AsNoTracking()
            .Where(l => l.SourceRefType == LedgerSourceRefType.NursePayout).ToList();

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