""" Unit tests for Smart Scheduling Engine (Phase 10) Tests for SmartScheduler class and scoring algorithms """ import pytest from datetime import datetime, timedelta, time from django.test import TestCase from django.utils import timezone from django.contrib.auth import get_user_model from appointments.scheduling.smart_scheduler import SmartScheduler from appointments.models import ( AppointmentRequest, SchedulingPreference, AppointmentPriorityRule, SchedulingMetrics, AppointmentTemplate ) from patients.models import PatientProfile from core.models import Tenant User = get_user_model() class SmartSchedulerTestCase(TestCase): """Test cases for SmartScheduler class""" def setUp(self): """Set up test data""" # Create tenant self.tenant = Tenant.objects.create( name="Test Hospital", display_name="Test Hospital", address_line1="123 Test St", city="Test City", state="Test State", postal_code="12345", phone_number="+966501234567", email="test@hospital.com" ) # Create provider self.provider = User.objects.create_user( email="provider@test.com", first_name="Test", last_name="Provider", tenant=self.tenant, role="PHYSICIAN" ) # Create patient self.patient = PatientProfile.objects.create( tenant=self.tenant, first_name="Test", last_name="Patient", date_of_birth=datetime(1990, 1, 1).date(), gender="M", mrn="TEST001" ) # Create appointment template self.appointment_type = AppointmentTemplate.objects.create( tenant=self.tenant, name="General Consultation", appointment_type="CONSULTATION", specialty="FAMILY_MEDICINE", duration_minutes=30, is_active=True ) # Create scheduling preference for patient self.preference = SchedulingPreference.objects.create( tenant=self.tenant, patient=self.patient, preferred_days_of_week=[1, 2, 3], # Mon, Tue, Wed preferred_time_of_day="MORNING", no_show_rate=5.0, completion_rate=95.0 ) # Initialize scheduler self.scheduler = SmartScheduler(self.tenant) def test_scheduler_initialization(self): """Test SmartScheduler initialization""" self.assertIsNotNone(self.scheduler) self.assertEqual(self.scheduler.tenant, self.tenant) def test_calculate_provider_availability_score(self): """Test provider availability scoring""" # Create a slot slot_datetime = timezone.now() + timedelta(days=1, hours=10) # Test with available slot score = self.scheduler._calculate_provider_availability_score( self.provider, slot_datetime, 30 ) # Score should be between 0 and 1 self.assertGreaterEqual(score, 0.0) self.assertLessEqual(score, 1.0) def test_calculate_patient_priority_score(self): """Test patient priority scoring""" # Create priority rule priority_rule = AppointmentPriorityRule.objects.create( tenant=self.tenant, name="Urgent Care", priority_level="URGENT", icd10_codes=["R07.9"], # Chest pain urgency_multiplier=2.0, max_wait_days=7 ) # Test priority scoring score = self.scheduler._calculate_patient_priority_score( self.patient, self.appointment_type, icd10_codes=["R07.9"] ) # Score should be elevated due to priority rule self.assertGreater(score, 0.5) def test_calculate_no_show_risk_score(self): """Test no-show risk calculation""" # Test with patient who has low no-show rate score = self.scheduler._calculate_no_show_risk_score( self.patient, timezone.now() + timedelta(days=1, hours=10) ) # Score should be high (low risk) self.assertGreater(score, 0.8) # Update preference with high no-show rate self.preference.no_show_rate = 50.0 self.preference.save() # Recalculate score = self.scheduler._calculate_no_show_risk_score( self.patient, timezone.now() + timedelta(days=1, hours=10) ) # Score should be lower (higher risk) self.assertLess(score, 0.6) def test_calculate_geographic_score(self): """Test geographic proximity scoring""" # Set patient location self.preference.patient_latitude = 24.7136 self.preference.patient_longitude = 46.6753 self.preference.save() # Test with nearby location score = self.scheduler._calculate_geographic_score( self.patient, 24.7200, # Nearby latitude 46.6800 # Nearby longitude ) # Score should be high (close proximity) self.assertGreater(score, 0.7) def test_calculate_patient_preference_score(self): """Test patient preference scoring""" # Test with preferred day and time preferred_datetime = timezone.now().replace( hour=9, minute=0, second=0, microsecond=0 ) # Make it a Monday days_ahead = (0 - preferred_datetime.weekday()) % 7 if days_ahead == 0: days_ahead = 7 preferred_datetime += timedelta(days=days_ahead) score = self.scheduler._calculate_patient_preference_score( self.patient, preferred_datetime ) # Score should be high (matches preferences) self.assertGreater(score, 0.7) # Test with non-preferred day (Sunday) non_preferred_datetime = preferred_datetime + timedelta(days=6) score = self.scheduler._calculate_patient_preference_score( self.patient, non_preferred_datetime ) # Score should be lower self.assertLess(score, 0.5) def test_calculate_slot_score(self): """Test overall slot scoring""" slot_datetime = timezone.now() + timedelta(days=1, hours=10) score = self.scheduler._calculate_slot_score( patient=self.patient, provider=self.provider, slot_datetime=slot_datetime, appointment_type=self.appointment_type, duration_minutes=30 ) # Score should be a dictionary with all components self.assertIn('total_score', score) self.assertIn('provider_availability', score) self.assertIn('patient_priority', score) self.assertIn('no_show_risk', score) self.assertIn('geographic_proximity', score) self.assertIn('patient_preference', score) # Total score should be between 0 and 100 self.assertGreaterEqual(score['total_score'], 0) self.assertLessEqual(score['total_score'], 100) def test_find_optimal_slots_basic(self): """Test basic optimal slot finding""" # Create date range start_date = timezone.now() + timedelta(days=1) date_range = [start_date + timedelta(days=i) for i in range(7)] # Find slots slots = self.scheduler.find_optimal_slots( patient=self.patient, provider=self.provider, appointment_type=self.appointment_type, preferred_dates=date_range, duration_minutes=30, max_results=5 ) # Should return a list self.assertIsInstance(slots, list) # Each slot should have required fields if slots: slot = slots[0] self.assertIn('datetime', slot) self.assertIn('score', slot) self.assertIn('score_breakdown', slot) def test_find_optimal_slots_with_constraints(self): """Test slot finding with time constraints""" start_date = timezone.now() + timedelta(days=1) date_range = [start_date + timedelta(days=i) for i in range(3)] # Find slots with morning preference slots = self.scheduler.find_optimal_slots( patient=self.patient, provider=self.provider, appointment_type=self.appointment_type, preferred_dates=date_range, duration_minutes=30, time_preferences=['MORNING'], max_results=10 ) # All slots should be in morning hours (before 12:00) for slot in slots: slot_time = slot['datetime'].time() self.assertLess(slot_time.hour, 12) def test_scoring_weights(self): """Test that scoring weights sum to 1.0""" weights = self.scheduler.scoring_weights total_weight = sum(weights.values()) # Weights should sum to 1.0 (with small tolerance for floating point) self.assertAlmostEqual(total_weight, 1.0, places=2) def test_no_show_prediction_time_factors(self): """Test no-show prediction with time-based factors""" # Early morning appointment (should have higher risk) early_morning = timezone.now().replace(hour=7, minute=0) score_early = self.scheduler._calculate_no_show_risk_score( self.patient, early_morning ) # Mid-morning appointment (should have lower risk) mid_morning = timezone.now().replace(hour=10, minute=0) score_mid = self.scheduler._calculate_no_show_risk_score( self.patient, mid_morning ) # Early morning should have higher risk (lower score) self.assertLess(score_early, score_mid) def test_priority_routing(self): """Test priority-based routing for urgent appointments""" # Create urgent priority rule urgent_rule = AppointmentPriorityRule.objects.create( tenant=self.tenant, name="Emergency", priority_level="EMERGENCY", icd10_codes=["I21.9"], # Acute MI urgency_multiplier=5.0, max_wait_days=1 ) # Calculate priority score score = self.scheduler._calculate_patient_priority_score( self.patient, self.appointment_type, icd10_codes=["I21.9"] ) # Should have very high priority score self.assertGreater(score, 0.9) class ScoringAlgorithmTestCase(TestCase): """Test cases for individual scoring algorithms""" def setUp(self): """Set up test data""" self.tenant = Tenant.objects.create( name="Test Hospital", display_name="Test Hospital", address_line1="123 Test St", city="Test City", state="Test State", postal_code="12345", phone_number="+966501234567", email="test@hospital.com" ) self.scheduler = SmartScheduler(self.tenant) def test_time_of_day_scoring(self): """Test time of day preference scoring""" # Morning preference morning_time = time(9, 0) afternoon_time = time(14, 0) evening_time = time(18, 0) # Test morning preference score_morning = self.scheduler._score_time_of_day(morning_time, "MORNING") score_afternoon = self.scheduler._score_time_of_day(afternoon_time, "MORNING") # Morning time should score higher for morning preference self.assertGreater(score_morning, score_afternoon) def test_day_of_week_scoring(self): """Test day of week preference scoring""" # Preferred days: Monday, Wednesday, Friday (0, 2, 4) preferred_days = [0, 2, 4] # Monday (preferred) score_preferred = self.scheduler._score_day_of_week(0, preferred_days) # Tuesday (not preferred) score_not_preferred = self.scheduler._score_day_of_week(1, preferred_days) # Preferred day should score higher self.assertGreater(score_preferred, score_not_preferred) def test_distance_scoring(self): """Test geographic distance scoring""" # Test various distances score_0km = self.scheduler._score_distance(0) score_5km = self.scheduler._score_distance(5) score_20km = self.scheduler._score_distance(20) score_50km = self.scheduler._score_distance(50) # Closer distances should score higher self.assertGreater(score_0km, score_5km) self.assertGreater(score_5km, score_20km) self.assertGreater(score_20km, score_50km) def test_no_show_rate_scoring(self): """Test no-show rate scoring""" # Low no-show rate (good) score_low = self.scheduler._score_no_show_rate(5.0) # High no-show rate (bad) score_high = self.scheduler._score_no_show_rate(50.0) # Lower no-show rate should score higher self.assertGreater(score_low, score_high) @pytest.mark.django_db class SmartSchedulerPerformanceTest(TestCase): """Performance tests for SmartScheduler""" def setUp(self): """Set up test data""" self.tenant = Tenant.objects.create( name="Test Hospital", display_name="Test Hospital", address_line1="123 Test St", city="Test City", state="Test State", postal_code="12345", phone_number="+966501234567", email="test@hospital.com" ) self.scheduler = SmartScheduler(self.tenant) # Create multiple providers self.providers = [] for i in range(5): provider = User.objects.create_user( email=f"provider{i}@test.com", first_name=f"Provider{i}", last_name="Test", tenant=self.tenant, role="PHYSICIAN" ) self.providers.append(provider) # Create patient self.patient = PatientProfile.objects.create( tenant=self.tenant, first_name="Test", last_name="Patient", date_of_birth=datetime(1990, 1, 1).date(), gender="M", mrn="TEST001" ) # Create appointment template self.appointment_type = AppointmentTemplate.objects.create( tenant=self.tenant, name="General Consultation", appointment_type="CONSULTATION", specialty="FAMILY_MEDICINE", duration_minutes=30, is_active=True ) def test_slot_finding_performance(self): """Test performance of slot finding for 30-day range""" import time # Create 30-day date range start_date = timezone.now() + timedelta(days=1) date_range = [start_date + timedelta(days=i) for i in range(30)] # Measure time start_time = time.time() slots = self.scheduler.find_optimal_slots( patient=self.patient, provider=self.providers[0], appointment_type=self.appointment_type, preferred_dates=date_range, duration_minutes=30, max_results=20 ) end_time = time.time() execution_time = end_time - start_time # Should complete in less than 2 seconds self.assertLess(execution_time, 2.0) print(f"\nSlot finding for 30 days completed in {execution_time:.3f} seconds") def test_scoring_performance(self): """Test performance of scoring algorithm""" import time slot_datetime = timezone.now() + timedelta(days=1, hours=10) # Measure time for 100 score calculations start_time = time.time() for _ in range(100): self.scheduler._calculate_slot_score( patient=self.patient, provider=self.providers[0], slot_datetime=slot_datetime, appointment_type=self.appointment_type, duration_minutes=30 ) end_time = time.time() execution_time = end_time - start_time # 100 calculations should complete in less than 1 second self.assertLess(execution_time, 1.0) print(f"\n100 score calculations completed in {execution_time:.3f} seconds")