using System.Net;
using System.Net.Http.Json;
using System.Text;
using Baya.Application.Contracts.Identity;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Bnpl;
using Baya.Domain.Entities.Catalog;
using Baya.Domain.Entities.Geography;
using Baya.Domain.Entities.Identity;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.User;
using Baya.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using BookingEntity = Baya.Domain.Entities.Booking.Booking;

namespace Baya.Test.Api;

public class BnplApiTests(BayaApiFactory factory) : IClassFixture<BayaApiFactory>
{
    [Fact]
    public async Task Eligibility_Unauthenticated_Returns401()
    {
        var client = factory.CreateClient();
        var response = await client.PostAsJsonAsync("/api/v1/checkout_bnpl/eligibility", new { bookingRequestId = 1L, providerCode = "snapppay" });
        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }

    [Fact]
    public async Task Eligibility_InvalidProvider_Returns400()
    {
        var client = factory.CreateClient();
        await ProfileTestClient.AuthenticateAsync(factory, client, "09131902101", "customer");
        var response = await client.PostAsJsonAsync("/api/v1/checkout_bnpl/eligibility", new { bookingRequestId = 1L, providerCode = "not_a_provider" });
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }

    [Fact]
    public async Task Full_flow_eligibility_initiate_settle_posts_net_of_fee_ledger_and_invariant_payout()
    {
        var client = factory.CreateClient();
        const string phone = "09131902102";
        await ProfileTestClient.AuthenticateAsync(factory, client, phone, "customer");
        var (requestId, nurseId) = await SeedAcceptedBnplRequestAsync(phone, price: 10_000_000);

        // 1. Eligibility → eligible + a bnpl_transactions row in status 'eligible'.
        var eligibility = await client.PostAsJsonAsync("/api/v1/checkout_bnpl/eligibility",
            new { bookingRequestId = requestId, providerCode = BnplProviderCodes.SnappPay });
        Assert.Equal(HttpStatusCode.OK, eligibility.StatusCode);
        Assert.True((await AuthTestClient.ReadDataAsync(eligibility)).GetProperty("isEligible").GetBoolean());

        // 2. Initiate → token_issued + a token/redirect.
        var initiate = await client.PostAsJsonAsync("/api/v1/checkout_bnpl/initiate",
            new { bookingRequestId = requestId, providerCode = BnplProviderCodes.SnappPay });
        Assert.Equal(HttpStatusCode.OK, initiate.StatusCode);
        var initData = await AuthTestClient.ReadDataAsync(initiate);
        var token = initData.GetProperty("externalPaymentToken").GetString()!;
        Assert.Equal("token_issued", initData.GetProperty("status").GetString());

        // 3. Provider callbacks verify then settle (anonymous, signature-authenticated).
        Assert.Equal(HttpStatusCode.OK, (await PostCallbackAsync(client, CallbackBody("evt-verify-1", "order.verified", token))).StatusCode);
        var settle = await PostCallbackAsync(client, CallbackBody("evt-settle-1", "order.settled", token));
        Assert.Equal(HttpStatusCode.OK, settle.StatusCode);
        Assert.Equal(WebhookProcessingStatus.Processed, (await AuthTestClient.ReadDataAsync(settle)).GetProperty("processingStatus").GetString());

        using var scope = factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        var booking = db.Set<BookingEntity>().AsNoTracking().Single(b => b.BookingRequestId == requestId);
        Assert.Equal(BookingStatus.Confirmed, booking.Status);

        var legs = db.Set<LedgerEntry>().AsNoTracking().Where(l => l.BookingId == booking.Id).ToList();
        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));

        // Net escrow = settled amount (gross − provider commission), and the bnpl_fee_expense leg exists.
        Assert.Equal(1_000_000, legs.Where(l => l.AccountType == LedgerAccountType.BnplFeeExpense).Sum(l => l.AmountIrr)); // 10%
        var escrowNet = legs.Where(l => l.AccountType == LedgerAccountType.EscrowHeld && l.Direction == LedgerDirection.Debit).Sum(l => l.AmountIrr)
                        - legs.Where(l => l.AccountType == LedgerAccountType.EscrowHeld && l.Direction == LedgerDirection.Credit).Sum(l => l.AmountIrr);
        Assert.Equal(9_000_000, escrowNet);

        // Payout invariant to method — identical to the card path (gross − 15% commission).
        var payable = legs.Single(l => l.AccountType == LedgerAccountType.NursePayable);
        Assert.Equal(nurseId, payable.NurseId);
        Assert.Equal(8_500_000, payable.AmountIrr);

        var settledTxn = db.Set<PaymentTransaction>().AsNoTracking().Single(t => t.BookingRequestId == requestId && t.Status == PaymentTransactionStatus.Succeeded);
        var bnpl = db.Set<BnplTransaction>().AsNoTracking().Single(b => b.PaymentTransactionId == settledTxn.Id);
        Assert.Equal(BnplStatus.Settled, bnpl.Status);
        Assert.Equal(9_000_000, bnpl.SettledAmountIrr);
        Assert.Equal(1_000_000, bnpl.BnplCommissionIrr);
    }

    [Fact]
    public async Task Replayed_settle_callback_is_idempotent_no_second_ledger()
    {
        var client = factory.CreateClient();
        const string phone = "09131902103";
        await ProfileTestClient.AuthenticateAsync(factory, client, phone, "customer");
        var (requestId, _) = await SeedAcceptedBnplRequestAsync(phone, price: 5_000_000);

        await client.PostAsJsonAsync("/api/v1/checkout_bnpl/eligibility", new { bookingRequestId = requestId, providerCode = BnplProviderCodes.SnappPay });
        var initiate = await client.PostAsJsonAsync("/api/v1/checkout_bnpl/initiate", new { bookingRequestId = requestId, providerCode = BnplProviderCodes.SnappPay });
        var token = (await AuthTestClient.ReadDataAsync(initiate)).GetProperty("externalPaymentToken").GetString()!;

        await PostCallbackAsync(client, CallbackBody("evt-verify-2", "order.verified", token));
        var body = CallbackBody("evt-settle-2", "order.settled", token);
        var first = await PostCallbackAsync(client, body);
        var replay = await PostCallbackAsync(client, body);

        Assert.Equal(HttpStatusCode.OK, first.StatusCode);
        Assert.Equal(HttpStatusCode.OK, replay.StatusCode);
        Assert.True((await AuthTestClient.ReadDataAsync(replay)).GetProperty("duplicate").GetBoolean());

        using var scope = factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        var booking = db.Set<BookingEntity>().AsNoTracking().Single(b => b.BookingRequestId == requestId);
        Assert.Equal(5, db.Set<LedgerEntry>().AsNoTracking().Count(l => l.BookingId == booking.Id));
        Assert.Equal(1, db.Set<PaymentWebhookEvent>().AsNoTracking().Count(e => e.ExternalEventId == "evt-settle-2"));
    }

    [Fact]
    public async Task Admin_revert_reverses_a_settled_order_and_opens_a_bnpl_revert_refund()
    {
        var client = factory.CreateClient();
        const string phone = "09131902104";
        await ProfileTestClient.AuthenticateAsync(factory, client, phone, "customer");
        var (requestId, _) = await SeedAcceptedBnplRequestAsync(phone, price: 8_000_000);

        await client.PostAsJsonAsync("/api/v1/checkout_bnpl/eligibility", new { bookingRequestId = requestId, providerCode = BnplProviderCodes.SnappPay });
        var initiate = await client.PostAsJsonAsync("/api/v1/checkout_bnpl/initiate", new { bookingRequestId = requestId, providerCode = BnplProviderCodes.SnappPay });
        var token = (await AuthTestClient.ReadDataAsync(initiate)).GetProperty("externalPaymentToken").GetString()!;
        await PostCallbackAsync(client, CallbackBody("evt-verify-3", "order.verified", token));
        await PostCallbackAsync(client, CallbackBody("evt-settle-3", "order.settled", token));

        long bnplId;
        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            var pt = db.Set<PaymentTransaction>().AsNoTracking().Single(t => t.GatewayReferenceCode == token);
            bnplId = db.Set<BnplTransaction>().AsNoTracking().Single(b => b.PaymentTransactionId == pt.Id).Id;
        }

        // A customer may not revert (admin-only) — the dynamic-permission policy forbids it.
        var forbidden = await client.PostAsJsonAsync($"/api/v1/admin_bnpl/{bnplId}/revert", new { });
        Assert.Equal(HttpStatusCode.Forbidden, forbidden.StatusCode);

        var admin = factory.CreateClient();
        await AdminTestClient.AuthenticateAsync(factory, admin, "09131902199");
        var revert = await admin.PostAsJsonAsync($"/api/v1/admin_bnpl/{bnplId}/revert", new { });
        Assert.Equal(HttpStatusCode.OK, revert.StatusCode);
        var revertData = await AuthTestClient.ReadDataAsync(revert);
        Assert.Equal(BnplStatus.Reverted, revertData.GetProperty("status").GetString());
        Assert.Equal("8000000", revertData.GetProperty("revertedAmountIrr").GetString());

        var status = await admin.GetAsync($"/api/v1/admin_bnpl/{bnplId}");
        Assert.Equal(HttpStatusCode.OK, status.StatusCode);
        var statusData = await AuthTestClient.ReadDataAsync(status);
        Assert.Equal(BnplStatus.Reverted, statusData.GetProperty("status").GetString());
        Assert.False(statusData.GetProperty("refundChannel").GetString() is null);

        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            var pt = db.Set<PaymentTransaction>().AsNoTracking().Single(t => t.GatewayReferenceCode == token);
            var refund = db.Set<Baya.Domain.Entities.Refunds.Refund>().AsNoTracking().Single(r => r.PaymentTransactionId == pt.Id);
            Assert.Equal(Baya.Domain.Entities.Refunds.RefundChannel.BnplRevert, refund.RefundChannel);
            Assert.NotNull(refund.ExpectedCustomerRefundEta);
        }
    }

    private static string CallbackBody(string eventId, string eventType, string token)
        => $"{{\"external_event_id\":\"{eventId}\",\"event_type\":\"{eventType}\",\"gateway_reference_code\":\"{token}\"}}";

    private static Task<HttpResponseMessage> PostCallbackAsync(HttpClient client, string body)
        => client.PostAsync("/api/v1/webhooks_bnpl/snapppay", new StringContent(body, Encoding.UTF8, "application/json"));

    /// <summary>Seeds reference data + a bookable nurse + an accepted request owned by the authenticated customer,
    /// plus an active BNPL gateway. Returns the request id and nurse profile id.</summary>
    private async Task<(long RequestId, long NurseId)> SeedAcceptedBnplRequestAsync(string customerPhone, long price)
    {
        using var scope = factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        var userManager = scope.ServiceProvider.GetRequiredService<IAppUserManager>();

        var customerUser = await userManager.GetUserByPhoneNumber(customerPhone);
        var customer = db.Set<CustomerProfile>().FirstOrDefault(c => c.UserId == customerUser!.Id);
        if (customer is null)
        {
            customer = new CustomerProfile { UserId = customerUser!.Id };
            db.Set<CustomerProfile>().Add(customer);
            db.SaveChanges();
        }

        if (!db.Set<PaymentGateway>().Any(g => g.Type == PaymentGatewayType.Bnpl && g.IsActive))
        {
            db.Set<PaymentGateway>().Add(new PaymentGateway
            {
                ProviderCode = BnplProviderCodes.SnappPay, Type = PaymentGatewayType.Bnpl, DisplayName = "SnappPay",
                ConfigJson = "{\"merchantId\":\"test\",\"sandbox\":true}", IsActive = true, Priority = 0
            });
            db.SaveChanges();
        }

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

        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 = customerPhone,
            Latitude = 35.6892m, Longitude = 51.3890m, IsPrimary = true
        };
        db.Set<CustomerAddress>().Add(address);

        var nurseUser = new User { UserName = $"nurse_{Guid.NewGuid():N}", PhoneNumber = $"0912{Random.Shared.Next(1000000, 9999999)}", 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();

        var variant = new NurseServiceVariant
        {
            NurseId = nurse.Id, ServiceCategoryId = category.Id, Price = price, 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 = customer.Id, NurseId = nurse.Id, PatientId = patient.Id, VariantId = variant.Id,
            CustomerAddressId = address.Id, 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();

        return (request.Id, nurse.Id);
    }
}
