using Baya.Application.Common;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.SupportAlerts;
using Baya.Application.Features.Bookings.Commands.CheckInVisit;
using Baya.Application.Features.Bookings.Commands.CheckOutVisit;
using Baya.Application.Features.Bookings.Commands.ConvertRequestToBooking;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.SupportAlerts;
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using BookingEntity = Baya.Domain.Entities.Booking.Booking;

namespace Baya.Test.Foundation.Bookings;

public class BookingEvvTests
{
    private static readonly DateTimeOffset Now = new(2026, 8, 1, 9, 5, 0, TimeSpan.Zero);

    private static async Task<(long BookingId, long SessionId)> ConvertSingleAsync(BookingsTestHost host)
    {
        var variantId = host.AddVariant(sessionCount: 1);
        var requestId = host.AddAcceptedRequest(variantId);
        var handler = new ConvertRequestToBookingCommandHandler(
            host.AsCustomer(), host.UnitOfWork, host.Config(), host.Clock(Now), host.Capture(),
            new VariantSnapshotSerializer(), Substitute.For<INotificationDispatcher>());
        var result = await handler.Handle(new ConvertRequestToBookingCommand(requestId), CancellationToken.None);
        return (result.Result.Id, result.Result.Sessions[0].Id);
    }

    private IGeocoder GeocoderAt(BookingsTestHost host)
    {
        var g = Substitute.For<IGeocoder>();
        g.GeocodeAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(new GeocodeResult(host.AddressLat, host.AddressLng, "formatted", 0.9));
        return g;
    }

    [Fact]
    public async Task CheckIn_in_range_then_checkout_completes_booking_and_sets_dispute_window()
    {
        using var host = new BookingsTestHost();
        var (bookingId, sessionId) = await ConvertSingleAsync(host);
        var alerts = Substitute.For<ISupportAlertService>();

        var checkIn = new CheckInVisitCommandHandler(host.AsNurse(), host.UnitOfWork, host.Config(), host.Clock(Now),
            GeocoderAt(host), alerts, Substitute.For<INotificationDispatcher>());
        var inResult = await checkIn.Handle(new CheckInVisitCommand(host.AddressLat, host.AddressLng, sessionId), CancellationToken.None);

        Assert.True(inResult.IsSuccess);
        Assert.True(inResult.Result.CheckInAddressMatch); // in range
        Assert.Equal(BookingStatus.InProgress, host.StatusOf(bookingId));
        await alerts.DidNotReceive().RaiseAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
            Arg.Any<long?>(), Arg.Any<long?>(), Arg.Any<CancellationToken>());

        var checkOutTime = new DateTimeOffset(2026, 8, 1, 13, 0, 0, TimeSpan.Zero);
        var checkOut = new CheckOutVisitCommandHandler(host.AsNurse(), host.UnitOfWork, host.Config(), host.Clock(checkOutTime));
        var outResult = await checkOut.Handle(new CheckOutVisitCommand(host.AddressLat, host.AddressLng, sessionId), CancellationToken.None);

        Assert.True(outResult.IsSuccess);
        Assert.Equal(VisitVerificationStatus.Completed, outResult.Result.Status);
        Assert.Equal(BookingStatus.Completed, host.StatusOf(bookingId));

        var booking = host.Db.Set<BookingEntity>().Include(b => b.Sessions).AsNoTracking().Single(b => b.Id == bookingId);
        Assert.Equal(checkOutTime.UtcDateTime.AddHours(72), booking.DisputeWindowEndsAt);
        Assert.Equal(checkOutTime.UtcDateTime.AddHours(72), booking.Sessions.Single().PayoutEligibleAt);
    }

    [Fact]
    public async Task CheckIn_out_of_range_raises_alert_without_blocking()
    {
        using var host = new BookingsTestHost();
        var (bookingId, sessionId) = await ConvertSingleAsync(host);
        var alerts = Substitute.For<ISupportAlertService>();

        var checkIn = new CheckInVisitCommandHandler(host.AsNurse(), host.UnitOfWork, host.Config(), host.Clock(Now),
            GeocoderAt(host), alerts, Substitute.For<INotificationDispatcher>());

        // Far-away GPS (Gulf of Guinea) → well beyond tolerance.
        var result = await checkIn.Handle(new CheckInVisitCommand(0m, 0m, sessionId), CancellationToken.None);

        Assert.True(result.IsSuccess);                       // never blocked
        Assert.False(result.Result.CheckInAddressMatch);     // flagged
        Assert.Equal(BookingStatus.InProgress, host.StatusOf(bookingId)); // visit proceeds
        await alerts.Received(1).RaiseAsync(
            SupportAlertType.EvvLocationMismatch, "booking_session", sessionId.ToString(),
            SupportAlertSeverity.Medium, bookingId, Arg.Any<long?>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task CheckOut_without_open_checkin_fails()
    {
        using var host = new BookingsTestHost();
        var (_, sessionId) = await ConvertSingleAsync(host);

        var checkOut = new CheckOutVisitCommandHandler(host.AsNurse(), host.UnitOfWork, host.Config(), host.Clock(Now));
        var result = await checkOut.Handle(new CheckOutVisitCommand(host.AddressLat, host.AddressLng, sessionId), CancellationToken.None);

        Assert.False(result.IsSuccess);
    }
}
