from datetime import timedelta import pytest from django.urls import reverse from django.utils import timezone from apps.accounts.models import User, UserRole from apps.bookings.models import Booking, BookingStatus from apps.salons.models import Salon, Service, StaffAvailability, StaffProfile @pytest.fixture def base_entities(): owner = User.objects.create_user(email="owner@example.com", password="pass", role=UserRole.MANAGER) customer = User.objects.create_user(email="customer@example.com", password="pass") staff_user = User.objects.create_user(email="staff@example.com", password="pass", role=UserRole.STAFF) salon = Salon.objects.create( owner=owner, name="Main Salon", description="", address="123 King Rd", city="Riyadh", phone_number="0512345678", ) service = Service.objects.create( salon=salon, name="Haircut", description="", duration_minutes=60, price_amount=120, currency="SAR", ) staff = StaffProfile.objects.create(user=staff_user, salon=salon) return customer, staff, service def booking_payload(service, staff, start_time, end_time, notes=""): return { "service": service.id, "staff": staff.id if staff else None, "start_time": start_time.isoformat(), "end_time": end_time.isoformat(), "notes": notes, } def next_day_at(hour, minute=0): now = timezone.now() target = now + timedelta(days=1) return target.replace(hour=hour, minute=minute, second=0, microsecond=0) @pytest.mark.django_db def test_requires_staff_assignment(client, base_entities): customer, staff, service = base_entities client.force_login(customer) start_time = next_day_at(9) end_time = start_time + timedelta(minutes=service.duration_minutes) payload = booking_payload(service, None, start_time, end_time) response = client.post( reverse("booking-list"), payload, content_type="application/json", ) assert response.status_code == 400 assert "staff" in response.json() @pytest.mark.django_db def test_rejects_end_before_start(client, base_entities): customer, staff, service = base_entities client.force_login(customer) start_time = next_day_at(9) end_time = start_time - timedelta(minutes=service.duration_minutes) payload = booking_payload(service, staff, start_time, end_time) response = client.post( reverse("booking-list"), payload, content_type="application/json", ) assert response.status_code == 400 assert "end_time" in response.json() @pytest.mark.django_db def test_rejects_duration_mismatch(client, base_entities): customer, staff, service = base_entities client.force_login(customer) start_time = next_day_at(9) end_time = start_time + timedelta(minutes=30) payload = booking_payload(service, staff, start_time, end_time) response = client.post( reverse("booking-list"), payload, content_type="application/json", ) assert response.status_code == 400 assert "end_time" in response.json() @pytest.mark.django_db def test_rejects_outside_availability_when_defined(client, base_entities): customer, staff, service = base_entities client.force_login(customer) start_time = next_day_at(8) end_time = start_time + timedelta(minutes=service.duration_minutes) StaffAvailability.objects.create( staff=staff, day_of_week=start_time.weekday(), start_time=timezone.datetime(2000, 1, 1, 9, 0).time(), end_time=timezone.datetime(2000, 1, 1, 17, 0).time(), ) payload = booking_payload(service, staff, start_time, end_time) response = client.post( reverse("booking-list"), payload, content_type="application/json", ) assert response.status_code == 400 assert "start_time" in response.json() @pytest.mark.django_db def test_allows_booking_without_availability_records(client, base_entities): customer, staff, service = base_entities client.force_login(customer) start_time = next_day_at(10) end_time = start_time + timedelta(minutes=service.duration_minutes) payload = booking_payload(service, staff, start_time, end_time) response = client.post( reverse("booking-list"), payload, content_type="application/json", ) assert response.status_code == 201 @pytest.mark.django_db def test_rejects_overlapping_pending_or_confirmed_bookings(client, base_entities): customer, staff, service = base_entities client.force_login(customer) start_time = next_day_at(9) end_time = start_time + timedelta(minutes=service.duration_minutes) Booking.objects.create( salon=service.salon, customer=customer, service=service, staff=staff, start_time=start_time, end_time=end_time, status=BookingStatus.CONFIRMED, price_amount=service.price_amount, currency=service.currency, notes="", ) overlap_start = start_time + timedelta(minutes=30) overlap_end = overlap_start + timedelta(minutes=service.duration_minutes) payload = booking_payload(service, staff, overlap_start, overlap_end) response = client.post( reverse("booking-list"), payload, content_type="application/json", ) assert response.status_code == 400 assert "start_time" in response.json() @pytest.mark.django_db def test_allows_overlap_with_cancelled_or_completed(client, base_entities): customer, staff, service = base_entities client.force_login(customer) start_time = next_day_at(9) end_time = start_time + timedelta(minutes=service.duration_minutes) Booking.objects.create( salon=service.salon, customer=customer, service=service, staff=staff, start_time=start_time, end_time=end_time, status=BookingStatus.CANCELLED, price_amount=service.price_amount, currency=service.currency, notes="", ) overlap_start = start_time + timedelta(minutes=30) overlap_end = overlap_start + timedelta(minutes=service.duration_minutes) payload = booking_payload(service, staff, overlap_start, overlap_end) response = client.post( reverse("booking-list"), payload, content_type="application/json", ) assert response.status_code == 201