using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Payments;
using Baya.Application.Features.Messaging.Commands.AutoCreateCoordinationTicket;
using Baya.Application.Features.Messaging.Commands.OpenTicket;
using Baya.Application.Models.Common;
using Baya.Application.Models.Messaging;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Catalog;
using Baya.Domain.Entities.Geography;
using Baya.Domain.Entities.Identity;
using Baya.Domain.Entities.Messaging;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.User;
using Baya.Infrastructure.Persistence;
using Baya.Infrastructure.Persistence.Repositories.Common;
using Baya.Tests.Setup.Setups;
using Mediator;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using BookingEntity = Baya.Domain.Entities.Booking.Booking;

namespace Baya.Test.Foundation.Refunds;

/// <summary>
/// A self-contained SQLite host exercising the real EF model (schema, CHECK, filtered indexes, query filters,
/// the invoice-number counter seed) for the b11 refund/clawback/invoice engine. Seeds one bookable nurse + one
/// customer, and can create a confirmed booking with a captured (succeeded) card/BNPL transaction so a test can
/// drive the real <see cref="UnitOfWork"/> against the real handlers with substituted seams.
/// </summary>
public sealed class RefundsTestHost : IDisposable
{
    private readonly SqliteConnection _connection;
    public ApplicationDbContext Db { get; }
    public UnitOfWork UnitOfWork { get; }

    public long CustomerId { get; }
    public int CustomerUserId { get; }
    public long NurseId { get; }

    /// <summary>A real seeded ticket row so the <c>refunds.ticket_id</c> FK (refinement-phase-6) is satisfied when
    /// the auto-anchor hook returns this id.</summary>
    public long TicketId { get; }
    private readonly long _cityId;
    private readonly long _categoryId;
    private readonly long _patientId;
    private readonly long _addressId;

    public RefundsTestHost()
    {
        _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 customerUser = new User { UserName = "cust1", PhoneNumber = "09120000001", Gender = "male", Name = "علی", FamilyName = "رضایی", IsActive = true };
        Db.Users.Add(customerUser);
        Db.SaveChanges();
        CustomerUserId = customerUser.Id;
        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;

        var nurseUser = new User { UserName = "nurse1", PhoneNumber = "09120000002", 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();
        NurseId = nurse.Id;

        var ticket = new Ticket
        {
            ReferenceCode = "TKT-TEST01", Category = TicketCategory.Refund, OpenedById = CustomerUserId
        };
        Db.Set<Ticket>().Add(ticket);
        Db.SaveChanges();
        TicketId = ticket.Id;
    }

    /// <summary>Seeds a confirmed booking + its captured (succeeded) transaction. Amounts satisfy
    /// <c>gross = commission + payout</c>. <paramref name="disputeWindowEndsAt"/> sets the paid-proxy window.</summary>
    public (long BookingId, long TransactionId) SeedCapturedBooking(
        long gross = 10_000_000, long commission = 1_500_000, string gatewayType = PaymentGatewayType.Standard,
        DateTime? disputeWindowEndsAt = null)
    {
        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, 8, 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 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, 8, 1), ScheduledTimeStart = new TimeOnly(9, 0), ScheduledTimeEnd = new TimeOnly(13, 0)
        };
        booking.TransitionTo(BookingStatus.Confirmed, new DateTime(2026, 8, 1, 0, 0, 0, DateTimeKind.Utc));
        if (disputeWindowEndsAt is { } d)
            booking.SetDisputeWindow(d);
        Db.Set<BookingEntity>().Add(booking);
        Db.SaveChanges();

        var gateway = new PaymentGateway
        {
            ProviderCode = gatewayType == PaymentGatewayType.Bnpl ? "snapppay" : "zarinpal",
            Type = gatewayType, DisplayName = "GW", ConfigJson = "{}", IsActive = true, Priority = 0
        };
        Db.Set<PaymentGateway>().Add(gateway);
        Db.SaveChanges();

        var txn = new PaymentTransaction
        {
            BookingRequestId = request.Id, CustomerId = CustomerId, GatewayId = gateway.Id,
            Amount = gross, GatewayReferenceCode = $"ref-{Guid.NewGuid():N}"
        };
        txn.MarkSucceeded(booking.Id, "ok", null);
        Db.Set<PaymentTransaction>().Add(txn);
        Db.SaveChanges();

        return (booking.Id, txn.Id);
    }

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

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

    public IPlatformConfig Config(decimal vatRate = 0.10m, int bnplEtaDays = 10)
    {
        var cfg = Substitute.For<IPlatformConfig>();
        cfg.GetConfig<decimal>("vat_rate", Arg.Any<CancellationToken>()).Returns(vatRate);
        cfg.GetConfig<decimal>("platform_fee_rate", Arg.Any<CancellationToken>()).Returns(0.15m);
        cfg.GetConfig<int>("bnpl_refund_eta_business_days", Arg.Any<CancellationToken>()).Returns(bnplEtaDays);
        return cfg;
    }

    public INursePayoutStatus PayoutStatus(bool paid)
    {
        var s = Substitute.For<INursePayoutStatus>();
        s.IsNursePaidForBookingAsync(Arg.Any<long>(), Arg.Any<CancellationToken>()).Returns(paid);
        return s;
    }

    public IPaymentProvider Card()
    {
        var p = Substitute.For<IPaymentProvider>();
        p.RefundAsync(Arg.Any<string>(), Arg.Any<long>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(ci => new PaymentRefundResult(PaymentProviderStatus.Succeeded, $"card-refund-{ci.ArgAt<string>(0)}"));
        return p;
    }

    public IBnplProvider Bnpl()
    {
        var p = Substitute.For<IBnplProvider>();
        p.RevertAsync(Arg.Any<string>(), Arg.Any<long>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(new BnplRevertResult(PaymentProviderStatus.Succeeded, "bnpl-revert", null));
        p.UpdateAsync(Arg.Any<string>(), Arg.Any<long>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(new BnplRevertResult(PaymentProviderStatus.Succeeded, "bnpl-update", null));
        return p;
    }

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

    /// <summary>An <see cref="ISender"/> whose refund ticket-anchor hook returns the <b>real</b> seeded
    /// <see cref="TicketId"/>, so the refund row satisfies the <c>refunds.ticket_id</c> FK.</summary>
    public ISender Senders()
    {
        var sender = Substitute.For<ISender>();
        sender.Send(Arg.Any<OpenTicketCommand>(), Arg.Any<CancellationToken>())
            .Returns(ValueTask.FromResult(OperationResult<OpenTicketResult>.SuccessResult(
                new OpenTicketResult(TicketId, "TKT-TEST01", "open", "refund"))));
        sender.Send(Arg.Any<AutoCreateCoordinationTicketCommand>(), Arg.Any<CancellationToken>())
            .Returns(ValueTask.FromResult(OperationResult<bool>.SuccessResult(true)));
        return sender;
    }

    public IReadOnlyList<LedgerEntry> LedgerFor(long bookingId)
        => Db.Set<LedgerEntry>().AsNoTracking().Where(l => l.BookingId == bookingId).OrderBy(l => l.Id).ToList();

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