from datetime import timedelta import pytest from django.urls import reverse from django.utils import timezone from rest_framework.test import APIClient 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_local = timezone.localtime(timezone.now()) target = now_local + timedelta(days=1) return target.replace(hour=hour, minute=minute, second=0, microsecond=0) @pytest.mark.django_db def test_requires_authentication(base_entities): _, staff, service = base_entities client = APIClient() 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 == 401 @pytest.mark.django_db def test_requires_staff_assignment(base_entities): customer, staff, service = base_entities client = APIClient() client.force_authenticate(user=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(base_entities): customer, staff, service = base_entities client = APIClient() client.force_authenticate(user=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(base_entities): customer, staff, service = base_entities client = APIClient() client.force_authenticate(user=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(base_entities): customer, staff, service = base_entities client = APIClient() client.force_authenticate(user=customer) start_time = next_day_at(8) end_time = start_time + timedelta(minutes=service.duration_minutes) start_local = timezone.localtime(start_time) StaffAvailability.objects.create( staff=staff, day_of_week=start_local.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(base_entities): customer, staff, service = base_entities client = APIClient() client.force_authenticate(user=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(base_entities): customer, staff, service = base_entities client = APIClient() client.force_authenticate(user=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(base_entities): customer, staff, service = base_entities client = APIClient() client.force_authenticate(user=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