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.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 PaymentsApiTests(BayaApiFactory factory) : IClassFixture<BayaApiFactory>
{
    [Fact]
    public async Task Initiate_Unauthenticated_Returns401()
    {
        var client = factory.CreateClient();
        var response = await client.PostAsync("/api/v1/bookings/1/payments", null);
        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }

    [Fact]
    public async Task Initiate_InvalidId_Returns400()
    {
        var client = factory.CreateClient();
        await ProfileTestClient.AuthenticateAsync(factory, client, "09131900201", "customer");

        var response = await client.PostAsync("/api/v1/bookings/0/payments", null);
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }

    [Fact]
    public async Task Full_flow_initiate_then_webhook_confirms_booking_and_accrues_nurse_payable()
    {
        var client = factory.CreateClient();
        const string phone = "09131900202";
        await ProfileTestClient.AuthenticateAsync(factory, client, phone, "customer");

        var (requestId, nurseId) = await SeedAcceptedRequestAsync(phone, price: 23_300_000);

        // 1. Initiate the card payment → pending transaction + a redirect + the gateway reference.
        var initiate = await client.PostAsync($"/api/v1/bookings/{requestId}/payments", null);
        Assert.Equal(HttpStatusCode.OK, initiate.StatusCode);
        var initData = await AuthTestClient.ReadDataAsync(initiate);
        var reference = initData.GetProperty("gatewayReferenceCode").GetString()!;
        Assert.False(string.IsNullOrEmpty(initData.GetProperty("redirectUrl").GetString()));

        // 2. A signature-authenticated webhook confirms it (anonymous to the auth pipeline).
        var webhook = await PostWebhookAsync(client, "zarinpal", SuccessBody(reference, "evt-flow-1"));
        Assert.Equal(HttpStatusCode.OK, webhook.StatusCode);
        var webhookData = await AuthTestClient.ReadDataAsync(webhook);
        Assert.Equal(WebhookProcessingStatus.Processed, webhookData.GetProperty("processingStatus").GetString());

        // 3. The booking converted/confirmed and the balanced capture group posted.
        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(3, legs.Count);
        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 payable = legs.Single(l => l.AccountType == LedgerAccountType.NursePayable);
        Assert.Equal(nurseId, payable.NurseId);
        Assert.Equal(19_805_000, payable.AmountIrr);
    }

    [Fact]
    public async Task Webhook_duplicate_replay_is_idempotent()
    {
        var client = factory.CreateClient();
        const string phone = "09131900203";
        await ProfileTestClient.AuthenticateAsync(factory, client, phone, "customer");

        var (requestId, _) = await SeedAcceptedRequestAsync(phone, price: 5_000_000);
        var initiate = await client.PostAsync($"/api/v1/bookings/{requestId}/payments", null);
        var reference = (await AuthTestClient.ReadDataAsync(initiate)).GetProperty("gatewayReferenceCode").GetString()!;

        var body = SuccessBody(reference, "evt-dup-1");
        var first = await PostWebhookAsync(client, "zarinpal", body);
        var replay = await PostWebhookAsync(client, "zarinpal", 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>();
        // Exactly one capture group for this booking, and one webhook event for the replayed key (the DB is
        // shared across the class fixture, so scope both assertions to this test's own rows).
        var booking = db.Set<BookingEntity>().AsNoTracking().Single(b => b.BookingRequestId == requestId);
        Assert.Equal(3, db.Set<LedgerEntry>().AsNoTracking().Count(l => l.BookingId == booking.Id));
        Assert.Equal(1, db.Set<PaymentWebhookEvent>().AsNoTracking().Count(e => e.ExternalEventId == "evt-dup-1"));
    }

    private static string SuccessBody(string reference, string eventId)
        => $"{{\"external_event_id\":\"{eventId}\",\"event_type\":\"payment.succeeded\",\"gateway_reference_code\":\"{reference}\"}}";

    private static Task<HttpResponseMessage> PostWebhookAsync(HttpClient client, string provider, string body)
        => client.PostAsync($"/api/v1/webhooks/payments/{provider}", new StringContent(body, Encoding.UTF8, "application/json"));

    /// <summary>Seeds the reference data + a bookable nurse + an accepted request owned by the authenticated
    /// customer, plus an active standard gateway. Returns the request id and nurse profile id.</summary>
    private async Task<(long RequestId, long NurseId)> SeedAcceptedRequestAsync(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.Standard && g.IsActive))
        {
            db.Set<PaymentGateway>().Add(new PaymentGateway
            {
                ProviderCode = "zarinpal", Type = PaymentGatewayType.Standard, DisplayName = "ZarinPal",
                ConfigJson = "{\"merchantId\":\"test\",\"sandbox\":true}", IsActive = true, Priority = 0
            });
        }

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