using Baya.Application.Contracts.Common; using Baya.Application.Contracts.Configuration; using Baya.Application.Contracts.Persistence; using Baya.Application.Features.Booking.Commands.CreateBookingRequest; using Baya.Application.Models.Booking; using Baya.Application.Models.Identity; using Baya.Domain.Entities.Booking; using Baya.Domain.Entities.Catalog; using Baya.Domain.Entities.Identity; using Baya.Domain.Entities.User; using NSubstitute; namespace Baya.Test.Foundation.Booking; public class CreateBookingRequestHandlerTests { private readonly ICurrentUser _currentUser = Substitute.For(); private readonly IUnitOfWork _unitOfWork = Substitute.For(); private readonly IPlatformConfig _config = Substitute.For(); private readonly IDateTimeProvider _clock = Substitute.For(); private readonly INotificationDispatcher _notifications = Substitute.For(); private readonly ICustomerProfileRepository _customers = Substitute.For(); private readonly IPatientRepository _patients = Substitute.For(); private readonly ICustomerAddressRepository _addresses = Substitute.For(); private readonly INurseServiceVariantRepository _variants = Substitute.For(); private readonly INurseProfileRepository _nurses = Substitute.For(); private readonly IBookingRequestRepository _requests = Substitute.For(); private const int CustomerUserId = 7; private const long CustomerId = 100; private const long NurseId = 42; private const int NurseUserId = 9; private const long PatientId = 5; private const long AddressId = 6; private const long VariantId = 8; private static readonly DateTimeOffset Now = new(2026, 7, 6, 10, 0, 0, TimeSpan.Zero); public CreateBookingRequestHandlerTests() { _currentUser.UserId.Returns(CustomerUserId); _currentUser.Roles.Returns([RoleNames.Customer]); _clock.UtcNow.Returns(Now); _unitOfWork.CustomerProfileRepository.Returns(_customers); _unitOfWork.PatientRepository.Returns(_patients); _unitOfWork.CustomerAddressRepository.Returns(_addresses); _unitOfWork.NurseServiceVariantRepository.Returns(_variants); _unitOfWork.NurseProfileRepository.Returns(_nurses); _unitOfWork.BookingRequestRepository.Returns(_requests); _customers.GetProfileIdByUserIdAsync(CustomerUserId, Arg.Any()).Returns(CustomerId); _patients.GetOwnedAsync(PatientId, CustomerId, Arg.Any()) .Returns(new Patient { CustomerId = CustomerId, DisplayName = "پدر", Gender = "male" }); _addresses.GetOwnedAsync(AddressId, CustomerId, Arg.Any()) .Returns(new CustomerAddress { CustomerId = CustomerId }); _variants.GetOwnedAsync(VariantId, NurseId, Arg.Any()) .Returns(new NurseServiceVariant { NurseId = NurseId, IsActive = true }); _nurses.GetBookingContextByIdAsync(NurseId, Arg.Any()) .Returns(new NurseBookingContext(NurseUserId, "female", IsVerified: true, IsAcceptingBookings: true)); _config.GetConfig("nurse_response_deadline_hours", Arg.Any()).Returns(24); _requests.GetDetailAsync(Arg.Any(), Arg.Any()).Returns(DetailStub()); } private CreateBookingRequestCommandHandler Handler() => new(_currentUser, _unitOfWork, _config, _clock, _notifications); private static CreateBookingRequestCommand Command(string gender = "female") => new(NurseId, VariantId, PatientId, AddressId, new DateOnly(2026, 8, 1), new TimeOnly(9, 0), new TimeOnly(13, 0), gender, "Careful with the IV."); [Fact] public async Task Create_Valid_FreezesResponseDeadlineFromConfigAndNotifiesNurse() { var result = await Handler().Handle(Command(), CancellationToken.None); Assert.True(result.IsSuccess); await _requests.Received(1).AddAsync( Arg.Is(r => r.CustomerId == CustomerId && r.NurseId == NurseId && r.PatientId == PatientId && r.Status == BookingRequestStatus.PendingNurseResponse && r.PaymentDeadlineAt == null && r.NurseResponseDeadlineAt == Now.AddHours(24).UtcDateTime), Arg.Any()); await _unitOfWork.Received(1).CommitAsync(); await _notifications.Received(1).DispatchAsync( Arg.Is(n => n.RecipientUserId == NurseUserId && n.Type == "booking_request_received"), Arg.Any()); } [Fact] public async Task Create_AnyGender_MatchesRegardlessOfNurseGender() { var result = await Handler().Handle(Command("any"), CancellationToken.None); Assert.True(result.IsSuccess); } [Fact] public async Task Create_SameGenderMismatch_FailsNamingTheConflict() { // Nurse is female; a male requirement must not match. var result = await Handler().Handle(Command("male"), CancellationToken.None); Assert.False(result.IsSuccess); Assert.False(result.IsNotFound); await _requests.DidNotReceive().AddAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Create_CrossCustomerPatient_IsNotFoundAndCreatesNothing() { _patients.GetOwnedAsync(PatientId, CustomerId, Arg.Any()).Returns((Patient)null); var result = await Handler().Handle(Command(), CancellationToken.None); Assert.True(result.IsNotFound); await _requests.DidNotReceive().AddAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Create_AddressNotOwned_IsNotFound() { _addresses.GetOwnedAsync(AddressId, CustomerId, Arg.Any()).Returns((CustomerAddress)null); var result = await Handler().Handle(Command(), CancellationToken.None); Assert.True(result.IsNotFound); } [Fact] public async Task Create_VariantNotThisNurses_IsNotFound() { _variants.GetOwnedAsync(VariantId, NurseId, Arg.Any()).Returns((NurseServiceVariant)null); var result = await Handler().Handle(Command(), CancellationToken.None); Assert.True(result.IsNotFound); } [Fact] public async Task Create_InactiveVariant_Fails() { _variants.GetOwnedAsync(VariantId, NurseId, Arg.Any()) .Returns(new NurseServiceVariant { NurseId = NurseId, IsActive = false }); var result = await Handler().Handle(Command(), CancellationToken.None); Assert.False(result.IsSuccess); await _requests.DidNotReceive().AddAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Create_NurseNotAcceptingOrUnverified_Fails() { _nurses.GetBookingContextByIdAsync(NurseId, Arg.Any()) .Returns(new NurseBookingContext(NurseUserId, "female", IsVerified: true, IsAcceptingBookings: false)); var result = await Handler().Handle(Command(), CancellationToken.None); Assert.False(result.IsSuccess); } [Fact] public async Task Create_PastDate_Fails() { var command = Command() with { RequestedDate = new DateOnly(2026, 7, 5) }; var result = await Handler().Handle(command, CancellationToken.None); Assert.False(result.IsSuccess); } [Fact] public async Task Create_NonCustomer_IsForbidden() { _currentUser.Roles.Returns([RoleNames.Nurse]); var result = await Handler().Handle(Command(), CancellationToken.None); Assert.True(result.IsForbidden); } private static BookingRequestDetailProjection DetailStub() => new( 1, BookingRequestStatus.PendingNurseResponse, CustomerId, NurseId, "پرستار", 0m, 0, PatientId, "پدر", VariantId, "خدمت", "per_day", AddressId, "خانه", 1, "تهران", "Tehran", null, null, null, "line", "postal", "recipient", "phone", "female", new DateOnly(2026, 8, 1), new TimeOnly(9, 0), new TimeOnly(13, 0), "notes", Now.AddHours(24).UtcDateTime, null, null, Now); }