using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Holidays;
using Baya.Application.Contracts.Payments;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Catalog;
using Baya.Domain.Entities.Geography;
using Baya.Domain.Entities.Identity;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.Payouts;
using Baya.Domain.Entities.Refunds;
using Baya.Domain.Entities.User;
using Baya.Infrastructure.CrossCutting.Seams;
using Baya.Infrastructure.Persistence;
using Baya.Infrastructure.Persistence.Repositories.Common;
using Baya.Tests.Setup.Setups;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using NSubstitute;
using BookingEntity = Baya.Domain.Entities.Booking.Booking;

namespace Baya.Test.Foundation.Payouts;

/// <summary>
/// A self-contained SQLite host exercising the real EF model (schema, the net-split CHECK, the booking_id UNIQUE
/// link, encrypted iban_snapshot, query filters) for the b13 payout engine. Seeds a customer + bookable nurses and
/// can create completed, dispute-window-closed bookings, verified primary bank accounts, and pending clawbacks so
/// a test can drive the real handlers against the real <see cref="UnitOfWork"/> with substituted seams.
/// </summary>
public sealed class PayoutsTestHost : IDisposable
{
    private readonly SqliteConnection _connection;
    public ApplicationDbContext Db { get; }
    public UnitOfWork UnitOfWork { get; }

    public long CustomerId { get; }
    public int AdminUserId { get; }
    private readonly long _cityId;
    private readonly long _categoryId;
    private readonly long _patientId;
    private readonly long _addressId;
    private int _phoneSeq = 100;

    public PayoutsTestHost()
    {
        _connection = new SqliteConnection("DataSource=:memory:");
        _connection.Open();

        var options = new DbContextOptionsBuilder<ApplicationDbContext>().UseSqlite(_connection).Options;
        Db = new ApplicationDbContext(options, TestFieldEncryptor.Instance);
        Db.Database.EnsureCreated();
        UnitOfWork = new UnitOfWork(Db);

        var province = new Province { NameFa = "تهران", NameEn = "Tehran", SortOrder = 1, IsActive = true };
        Db.Set<Province>().Add(province);
        Db.SaveChanges();
        var city = new City { ProvinceId = province.Id, NameFa = "تهران", NameEn = "Tehran", SortOrder = 1, IsActive = true };
        Db.Set<City>().Add(city);
        var category = new ServiceCategory { NameFa = "سالمند", NameEn = "Elderly", SortOrder = 1, IsActive = true };
        Db.Set<ServiceCategory>().Add(category);
        Db.SaveChanges();
        _cityId = city.Id;
        _categoryId = category.Id;

        var adminUser = new User { UserName = "admin1", PhoneNumber = NextPhone(), Gender = "male", Name = "ادمین", FamilyName = "سیستم", IsActive = true };
        Db.Users.Add(adminUser);
        Db.SaveChanges();
        AdminUserId = adminUser.Id;

        var customerUser = new User { UserName = "cust1", PhoneNumber = NextPhone(), Gender = "male", Name = "علی", FamilyName = "رضایی", IsActive = true };
        Db.Users.Add(customerUser);
        Db.SaveChanges();
        var customer = new CustomerProfile { UserId = customerUser.Id };
        Db.Set<CustomerProfile>().Add(customer);
        Db.SaveChanges();
        CustomerId = customer.Id;

        var patient = new Patient { CustomerId = customer.Id, DisplayName = "پدر", FirstName = "حسن", LastName = "رضایی", Gender = "male", IsActive = true };
        Db.Set<Patient>().Add(patient);
        var address = new CustomerAddress
        {
            CustomerId = customer.Id, CityId = city.Id, Title = "خانه", AddressLine = "خیابان اول",
            PostalCode = "1111111111", RecipientName = "علی", RecipientPhone = "09120000001",
            Latitude = 35.6892m, Longitude = 51.3890m, IsPrimary = true
        };
        Db.Set<CustomerAddress>().Add(address);
        Db.SaveChanges();
        _patientId = patient.Id;
        _addressId = address.Id;
    }

    private string NextPhone() => $"0912000{++_phoneSeq:0000}";

    /// <summary>Seeds a bookable nurse. When <paramref name="verifiedIban"/> is set, also seeds a verified primary
    /// bank account (is_primary + is_verified + matched_national_id) with the given IBAN.</summary>
    public long SeedNurse(string? verifiedIban = "IR000000000000000000000123")
    {
        var nurseUser = new User { UserName = $"nurse_{Guid.NewGuid():N}", PhoneNumber = NextPhone(), Gender = "female", Name = "زهرا", FamilyName = "احمدی", IsActive = true };
        Db.Users.Add(nurseUser);
        Db.SaveChanges();
        var nurse = new NurseProfile { UserId = nurseUser.Id };
        nurse.MarkVerified();
        nurse.SetAcceptingBookings(true);
        Db.Set<NurseProfile>().Add(nurse);
        Db.SaveChanges();

        if (verifiedIban is not null)
        {
            var account = new NurseBankAccount
            {
                NurseId = nurse.Id, BankName = "بانک ملی", AccountHolderName = "زهرا احمدی",
                Iban = verifiedIban, IbanHash = $"hash-{nurse.Id}", IsPrimary = true, IsVerified = true,
                MatchedNationalId = true, AccountHolderFromBank = "زهرا احمدی", OwnershipVendorRef = "mock"
            };
            Db.Set<NurseBankAccount>().Add(account);
            Db.SaveChanges();
        }

        return nurse.Id;
    }

    /// <summary>Seeds a completed booking with the given dispute-window close. Amounts satisfy
    /// <c>gross = commission + payout</c>. When <paramref name="disputed"/> is set the booking is moved to
    /// <c>disputed</c> (payout-ineligible).</summary>
    public long SeedCompletedBooking(
        long nurseId, DateTime disputeWindowEndsAt, long gross = 10_000_000, long commission = 1_500_000, bool disputed = false)
    {
        var variant = new NurseServiceVariant
        {
            NurseId = nurseId, ServiceCategoryId = _categoryId, Price = gross, PriceUnit = "per_day",
            SessionCount = null, DisplayName = "مراقبت روزانه", OptionSetHash = $"hash-{Guid.NewGuid():N}", IsActive = true
        };
        Db.Set<NurseServiceVariant>().Add(variant);
        Db.SaveChanges();

        var request = new BookingRequest
        {
            CustomerId = CustomerId, NurseId = nurseId, PatientId = _patientId, VariantId = variant.Id,
            CustomerAddressId = _addressId, RequiredCaregiverGender = CaregiverGender.Any,
            RequestedDate = new DateOnly(2026, 6, 1), RequestedTimeStart = new TimeOnly(9, 0), RequestedTimeEnd = new TimeOnly(13, 0),
            CustomerNotes = "note", NurseResponseDeadlineAt = new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc)
        };
        Db.Set<BookingRequest>().Add(request);
        Db.SaveChanges();
        request.Accept(new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc));
        Db.SaveChanges();

        var confirmedAt = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc);
        var booking = new BookingEntity
        {
            BookingRequestId = request.Id, CustomerId = CustomerId, NurseId = nurseId, PatientId = _patientId,
            VariantId = variant.Id, CustomerAddressId = _addressId, VariantSnapshotJson = "{}", AddressSnapshotJson = "{}",
            GrossPriceIrr = gross, BalinyaarCommissionIrr = commission, PlatformFeeRate = 0.15m,
            NursePayoutAmount = gross - commission, SessionCount = 1,
            ScheduledDate = new DateOnly(2026, 6, 1), ScheduledTimeStart = new TimeOnly(9, 0), ScheduledTimeEnd = new TimeOnly(13, 0)
        };
        booking.TransitionTo(BookingStatus.Confirmed, confirmedAt);
        booking.TransitionTo(BookingStatus.InProgress, confirmedAt);
        booking.TransitionTo(BookingStatus.Completed, confirmedAt);
        booking.SetDisputeWindow(disputeWindowEndsAt);
        if (disputed)
            booking.TransitionTo(BookingStatus.Disputed, confirmedAt);
        Db.Set<BookingEntity>().Add(booking);
        Db.SaveChanges();

        // The capture accrual so nurse_payable starts positive and the payout drains it to zero.
        var legs = LedgerPosting.CardCapture(booking.Id, nurseId, gross, commission, gross - commission, 0, confirmedAt);
        Db.Set<LedgerEntry>().AddRange(legs);
        Db.SaveChanges();

        return booking.Id;
    }

    /// <summary>Seeds a pending clawback the nurse owes back (opened by a prior post-payout refund in b11).</summary>
    public long SeedPendingClawback(long nurseId, long bookingId, long amount)
    {
        // A minimal refund row to satisfy the clawback's required refund_id FK.
        var refund = new Refund
        {
            PaymentTransactionId = SeedThrowawayTransaction(bookingId), BookingId = bookingId, RequestedByCustomerId = CustomerId,
            Amount = amount, PlatformFeeRefundedIrr = 0, NursePayoutRefundedIrr = amount, RefundPercentage = 1m,
            RefundChannel = RefundChannel.PspCard
        };
        Db.Set<Refund>().Add(refund);
        Db.SaveChanges();

        var clawback = new NurseClawback { NurseId = nurseId, BookingId = bookingId, RefundId = refund.Id, AmountIrr = amount };
        Db.Set<NurseClawback>().Add(clawback);
        Db.SaveChanges();
        return clawback.Id;
    }

    private long SeedThrowawayTransaction(long bookingId)
    {
        var gateway = new PaymentGateway { ProviderCode = "zarinpal", Type = PaymentGatewayType.Standard, DisplayName = "GW", ConfigJson = "{}", IsActive = true, Priority = 0 };
        Db.Set<PaymentGateway>().Add(gateway);
        Db.SaveChanges();
        var nurseBookingRequestId = Db.Set<BookingEntity>().Where(b => b.Id == bookingId).Select(b => b.BookingRequestId).First();
        var txn = new PaymentTransaction
        {
            BookingRequestId = nurseBookingRequestId, CustomerId = CustomerId, GatewayId = gateway.Id,
            Amount = 10_000_000, GatewayReferenceCode = $"ref-{Guid.NewGuid():N}"
        };
        txn.MarkSucceeded(bookingId, "ok", null);
        Db.Set<PaymentTransaction>().Add(txn);
        Db.SaveChanges();
        return txn.Id;
    }

    public ICurrentUser AsAdmin(int? userId = null)
    {
        var u = Substitute.For<ICurrentUser>();
        u.UserId.Returns(userId ?? AdminUserId);
        u.Roles.Returns(new[] { RoleNames.Admin });
        return u;
    }

    public IDateTimeProvider Clock(DateTimeOffset now)
    {
        var c = Substitute.For<IDateTimeProvider>();
        c.UtcNow.Returns(now);
        return c;
    }

    /// <summary>Identity holiday calendar (no shift) unless <paramref name="shifts"/> maps a closed day to its
    /// next business day.</summary>
    public IHolidayCalendar Holidays(params (DateOnly Closed, DateOnly Next)[] shifts)
    {
        var h = Substitute.For<IHolidayCalendar>();
        h.NextBusinessDay(Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
            .Returns(ci =>
            {
                var d = ci.Arg<DateOnly>();
                foreach (var (closed, next) in shifts)
                    if (closed == d) return next;
                return d;
            });
        h.IsBankClosed(Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
            .Returns(ci => shifts.Any(s => s.Closed == ci.Arg<DateOnly>()));
        return h;
    }

    public IPlatformConfig Config(long satnaThresholdIrr = 1_000_000_000, bool requireBnplSettlement = false)
    {
        var cfg = Substitute.For<IPlatformConfig>();
        cfg.GetConfig<decimal>("payout_satna_threshold_irr", Arg.Any<CancellationToken>()).Returns(satnaThresholdIrr);
        cfg.GetConfig<bool>("require_bnpl_settlement_for_payout", Arg.Any<CancellationToken>()).Returns(requireBnplSettlement);
        return cfg;
    }

    public IBankTransferProvider Bank(bool forceFailure = false, string failIban = "")
        => new MockBankTransferProvider(Options.Create(new SeamOptions
        {
            BankTransfer = new BankTransferOptions { ForceFailure = forceFailure, FailIban = failIban }
        }));

    public IDistributedLock Lock() => new NoOpLock();

    public IReadOnlyList<LedgerEntry> LedgerFor(long? bookingId, long? nurseId = null)
        => Db.Set<LedgerEntry>().AsNoTracking()
            .Where(l => (bookingId == null || l.BookingId == bookingId) && (nurseId == null || l.NurseId == nurseId))
            .OrderBy(l => l.Id).ToList();

    public long NursePayableBalance(long nurseId)
        => Db.Set<LedgerEntry>().AsNoTracking()
            .Where(l => l.AccountType == LedgerAccountType.NursePayable && l.NurseId == nurseId)
            .Sum(l => l.Direction == LedgerDirection.Credit ? l.AmountIrr : -l.AmountIrr);

    public void Dispose()
    {
        Db.Dispose();
        _connection.Dispose();
    }

    private sealed class NoOpLock : IDistributedLock
    {
        public ValueTask<IAsyncDisposable> AcquireAsync(string key, CancellationToken cancellationToken = default)
            => ValueTask.FromResult<IAsyncDisposable>(new Handle());

        private sealed class Handle : IAsyncDisposable
        {
            public ValueTask DisposeAsync() => ValueTask.CompletedTask;
        }
    }
}
