using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Features.Booking.Commands.AcceptBookingRequest;
using Baya.Application.Features.Booking.Commands.CancelBookingRequest;
using Baya.Application.Features.Booking.Commands.RejectBookingRequest;
using Baya.Application.Models.Booking;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Identity;
using Baya.Domain.Entities.User;
using NSubstitute;

namespace Baya.Test.Foundation.Booking;

public class RespondBookingRequestHandlerTests
{
    private readonly ICurrentUser _currentUser = Substitute.For<ICurrentUser>();
    private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
    private readonly IPlatformConfig _config = Substitute.For<IPlatformConfig>();
    private readonly IDateTimeProvider _clock = Substitute.For<IDateTimeProvider>();
    private readonly INotificationDispatcher _notifications = Substitute.For<INotificationDispatcher>();
    private readonly INurseProfileRepository _nurses = Substitute.For<INurseProfileRepository>();
    private readonly ICustomerProfileRepository _customers = Substitute.For<ICustomerProfileRepository>();
    private readonly IBookingRequestRepository _requests = Substitute.For<IBookingRequestRepository>();

    private const int NurseUserId = 9;
    private const long NurseId = 42;
    private const int CustomerUserId = 7;
    private const long CustomerId = 100;
    private const int CustomerAccountUserId = 5;
    private const long RequestId = 1;
    private static readonly DateTimeOffset Now = new(2026, 7, 6, 10, 0, 0, TimeSpan.Zero);

    public RespondBookingRequestHandlerTests()
    {
        _clock.UtcNow.Returns(Now);
        _unitOfWork.NurseProfileRepository.Returns(_nurses);
        _unitOfWork.CustomerProfileRepository.Returns(_customers);
        _unitOfWork.BookingRequestRepository.Returns(_requests);
        _nurses.GetProfileIdByUserIdAsync(NurseUserId, Arg.Any<CancellationToken>()).Returns(NurseId);
        _customers.GetProfileIdByUserIdAsync(CustomerUserId, Arg.Any<CancellationToken>()).Returns(CustomerId);
        _config.GetConfig<int>("booking_payment_deadline_minutes", Arg.Any<CancellationToken>()).Returns(30);
        _requests.GetDetailAsync(Arg.Any<long>(), Arg.Any<CancellationToken>()).Returns(DetailStub(BookingRequestStatus.PendingNurseResponse));
    }

    private static BookingRequest PendingRequest(DateTimeOffset responseDeadline)
        => new()
        {
            CustomerId = CustomerId,
            NurseId = NurseId,
            NurseResponseDeadlineAt = responseDeadline.UtcDateTime,
            Customer = new CustomerProfile { UserId = CustomerAccountUserId }
        };

    // ---- Accept ----

    [Fact]
    public async Task Accept_Pending_SetsThirtyMinuteWindowAndNotifiesCustomer()
    {
        _currentUser.UserId.Returns(NurseUserId);
        _currentUser.Roles.Returns([RoleNames.Nurse]);
        var request = PendingRequest(Now.AddHours(2));
        _requests.GetTrackedForNurseAsync(RequestId, NurseId, Arg.Any<CancellationToken>()).Returns(request);

        var handler = new AcceptBookingRequestCommandHandler(_currentUser, _unitOfWork, _config, _clock, _notifications);
        var result = await handler.Handle(new AcceptBookingRequestCommand(RequestId), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(BookingRequestStatus.AcceptedAwaitingPayment, request.Status);
        Assert.Equal(Now.AddMinutes(30).UtcDateTime, request.PaymentDeadlineAt);
        await _unitOfWork.Received(1).CommitAsync();
        await _notifications.Received(1).DispatchAsync(
            Arg.Is<Notification>(n => n.RecipientUserId == CustomerAccountUserId && n.Type == "booking_request_accepted"),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Accept_AfterResponseDeadline_IsConflict()
    {
        _currentUser.UserId.Returns(NurseUserId);
        _currentUser.Roles.Returns([RoleNames.Nurse]);
        var request = PendingRequest(Now.AddHours(-1));
        _requests.GetTrackedForNurseAsync(RequestId, NurseId, Arg.Any<CancellationToken>()).Returns(request);

        var handler = new AcceptBookingRequestCommandHandler(_currentUser, _unitOfWork, _config, _clock, _notifications);
        var result = await handler.Handle(new AcceptBookingRequestCommand(RequestId), CancellationToken.None);

        Assert.True(result.IsConflict);
        await _unitOfWork.DidNotReceive().CommitAsync();
    }

    [Fact]
    public async Task Accept_NonPending_IsConflict()
    {
        _currentUser.UserId.Returns(NurseUserId);
        _currentUser.Roles.Returns([RoleNames.Nurse]);
        var request = PendingRequest(Now.AddHours(2));
        request.Reject("already declined");
        _requests.GetTrackedForNurseAsync(RequestId, NurseId, Arg.Any<CancellationToken>()).Returns(request);

        var handler = new AcceptBookingRequestCommandHandler(_currentUser, _unitOfWork, _config, _clock, _notifications);
        var result = await handler.Handle(new AcceptBookingRequestCommand(RequestId), CancellationToken.None);

        Assert.True(result.IsConflict);
    }

    [Fact]
    public async Task Accept_NotAssignedNurse_IsNotFound()
    {
        _currentUser.UserId.Returns(NurseUserId);
        _currentUser.Roles.Returns([RoleNames.Nurse]);
        _requests.GetTrackedForNurseAsync(RequestId, NurseId, Arg.Any<CancellationToken>()).Returns((BookingRequest)null);

        var handler = new AcceptBookingRequestCommandHandler(_currentUser, _unitOfWork, _config, _clock, _notifications);
        var result = await handler.Handle(new AcceptBookingRequestCommand(RequestId), CancellationToken.None);

        Assert.True(result.IsNotFound);
    }

    // ---- Reject ----

    [Fact]
    public async Task Reject_Pending_StoresReasonAndNotifies()
    {
        _currentUser.UserId.Returns(NurseUserId);
        _currentUser.Roles.Returns([RoleNames.Nurse]);
        var request = PendingRequest(Now.AddHours(2));
        _requests.GetTrackedForNurseAsync(RequestId, NurseId, Arg.Any<CancellationToken>()).Returns(request);

        var handler = new RejectBookingRequestCommandHandler(_currentUser, _unitOfWork, _notifications);
        var result = await handler.Handle(new RejectBookingRequestCommand("Fully booked", RequestId), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(BookingRequestStatus.RejectedByNurse, request.Status);
        Assert.Equal("Fully booked", request.NurseRejectionReason);
        await _notifications.Received(1).DispatchAsync(
            Arg.Is<Notification>(n => n.Type == "booking_request_rejected"), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Reject_NonPending_IsConflict()
    {
        _currentUser.UserId.Returns(NurseUserId);
        _currentUser.Roles.Returns([RoleNames.Nurse]);
        var request = PendingRequest(Now.AddHours(2));
        request.Accept(Now.AddMinutes(30).UtcDateTime);
        _requests.GetTrackedForNurseAsync(RequestId, NurseId, Arg.Any<CancellationToken>()).Returns(request);

        var handler = new RejectBookingRequestCommandHandler(_currentUser, _unitOfWork, _notifications);
        var result = await handler.Handle(new RejectBookingRequestCommand("late", RequestId), CancellationToken.None);

        Assert.True(result.IsConflict);
    }

    // ---- Cancel ----

    [Fact]
    public async Task Cancel_FromAcceptedAwaitingPayment_Succeeds()
    {
        _currentUser.UserId.Returns(CustomerUserId);
        _currentUser.Roles.Returns([RoleNames.Customer]);
        var request = PendingRequest(Now.AddHours(2));
        request.Accept(Now.AddMinutes(30).UtcDateTime);
        _requests.GetTrackedForCustomerAsync(RequestId, CustomerId, Arg.Any<CancellationToken>()).Returns(request);
        _requests.GetDetailAsync(Arg.Any<long>(), Arg.Any<CancellationToken>())
            .Returns(DetailStub(BookingRequestStatus.CancelledByCustomer));

        var handler = new CancelBookingRequestCommandHandler(_currentUser, _unitOfWork);
        var result = await handler.Handle(new CancelBookingRequestCommand(RequestId), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(BookingRequestStatus.CancelledByCustomer, request.Status);
    }

    [Fact]
    public async Task Cancel_FromTerminal_IsConflict()
    {
        _currentUser.UserId.Returns(CustomerUserId);
        _currentUser.Roles.Returns([RoleNames.Customer]);
        var request = PendingRequest(Now.AddHours(2));
        request.Reject("declined");
        _requests.GetTrackedForCustomerAsync(RequestId, CustomerId, Arg.Any<CancellationToken>()).Returns(request);

        var handler = new CancelBookingRequestCommandHandler(_currentUser, _unitOfWork);
        var result = await handler.Handle(new CancelBookingRequestCommand(RequestId), CancellationToken.None);

        Assert.True(result.IsConflict);
        await _unitOfWork.DidNotReceive().CommitAsync();
    }

    private static BookingRequestDetailProjection DetailStub(string status)
        => new(
            RequestId, status, CustomerId, NurseId,
            "پرستار", 0m, 0, 5, "پدر", 8, "خدمت", "per_day",
            6, "خانه", 1, "تهران", "Tehran", null, null, null,
            "line", "postal", "recipient", "phone",
            "any", new DateOnly(2026, 8, 1), new TimeOnly(9, 0), new TimeOnly(13, 0),
            null, Now.AddHours(24).UtcDateTime, null, null, Now,
            1_000_000, null, null);
}
