* @since 1.0.0 */ namespace Care_API\Tests\Unit\Models; use Care_API\Models\Appointment; use Care_API\Models\Doctor; use Care_API\Models\Patient; use Care_API\Models\Clinic; class AppointmentTest extends \Care_API_Test_Case { /** * Mock wpdb for database operations */ private $mock_wpdb; /** * Setup before each test */ public function setUp(): void { parent::setUp(); // Mock wpdb global global $wpdb; $this->mock_wpdb = $this->createMock('wpdb'); $wpdb = $this->mock_wpdb; // Set table prefix $wpdb->prefix = 'wp_'; } /** * Test appointment scheduling validation with valid data * * @covers Appointment::create * @covers Appointment::validate_appointment_data * @covers Appointment::check_availability */ public function test_appointment_scheduling_validation() { // Arrange $valid_appointment_data = array( 'appointment_start_date' => '2024-02-15', 'appointment_start_time' => '14:30:00', 'appointment_end_time' => '15:00:00', 'visit_type' => 'consultation', 'clinic_id' => 1, 'doctor_id' => 100, 'patient_id' => 200, 'description' => 'Consulta de cardiologia de rotina' ); // Mock entity existence checks $this->mock_entities_exist(true, true, true); // Mock no conflicts $this->mock_wpdb->expects($this->once()) ->method('get_results') ->willReturn(array()); // No conflicting appointments // Mock successful insert $this->mock_wpdb->expects($this->once()) ->method('insert') ->willReturn(1); $this->mock_wpdb->insert_id = 123; // Act $result = Appointment::create($valid_appointment_data); // Assert $this->assertIsInt($result, 'Appointment creation should return appointment ID'); $this->assertEquals(123, $result, 'Should return the inserted appointment ID'); } /** * Test appointment scheduling with missing required fields * * @covers Appointment::validate_appointment_data */ public function test_appointment_scheduling_missing_fields() { // Arrange $invalid_appointment_data = array( 'appointment_start_date' => '2024-02-15', // Missing required fields: start_time, end_time, clinic_id, doctor_id, patient_id 'description' => 'Test appointment' ); // Act $result = Appointment::create($invalid_appointment_data); // Assert $this->assertInstanceOf('WP_Error', $result, 'Should return WP_Error for missing fields'); $this->assertEquals('appointment_validation_failed', $result->get_error_code()); $error_data = $result->get_error_data(); $this->assertArrayHasKey('errors', $error_data); $this->assertContains("Field 'appointment_start_time' is required", $error_data['errors']); $this->assertContains("Field 'appointment_end_time' is required", $error_data['errors']); $this->assertContains("Field 'clinic_id' is required", $error_data['errors']); $this->assertContains("Field 'doctor_id' is required", $error_data['errors']); $this->assertContains("Field 'patient_id' is required", $error_data['errors']); } /** * Test appointment scheduling with invalid date format * * @covers Appointment::validate_appointment_data */ public function test_appointment_scheduling_invalid_date_format() { // Arrange $appointment_data_invalid_date = array( 'appointment_start_date' => '15/02/2024', // Invalid format (should be Y-m-d) 'appointment_start_time' => '14:30:00', 'appointment_end_time' => '15:00:00', 'clinic_id' => 1, 'doctor_id' => 100, 'patient_id' => 200 ); // Act $result = Appointment::create($appointment_data_invalid_date); // Assert $this->assertInstanceOf('WP_Error', $result); $error_data = $result->get_error_data(); $this->assertContains('Invalid start date format. Use YYYY-MM-DD', $error_data['errors']); } /** * Test appointment scheduling with invalid time format * * @covers Appointment::validate_appointment_data */ public function test_appointment_scheduling_invalid_time_format() { // Arrange $appointment_data_invalid_time = array( 'appointment_start_date' => '2024-02-15', 'appointment_start_time' => '25:00:00', // Invalid hour 'appointment_end_time' => '15:70:00', // Invalid minutes 'clinic_id' => 1, 'doctor_id' => 100, 'patient_id' => 200 ); // Act $result = Appointment::create($appointment_data_invalid_time); // Assert $this->assertInstanceOf('WP_Error', $result); $error_data = $result->get_error_data(); $this->assertContains('Invalid appointment_start_time format. Use HH:MM or HH:MM:SS', $error_data['errors']); $this->assertContains('Invalid appointment_end_time format. Use HH:MM or HH:MM:SS', $error_data['errors']); } /** * Test appointment scheduling with end time before start time * * @covers Appointment::validate_appointment_data */ public function test_appointment_scheduling_end_before_start() { // Arrange $appointment_data_invalid_times = array( 'appointment_start_date' => '2024-02-15', 'appointment_start_time' => '15:00:00', 'appointment_end_time' => '14:30:00', // End before start 'clinic_id' => 1, 'doctor_id' => 100, 'patient_id' => 200 ); // Act $result = Appointment::create($appointment_data_invalid_times); // Assert $this->assertInstanceOf('WP_Error', $result); $error_data = $result->get_error_data(); $this->assertContains('End time must be after start time', $error_data['errors']); } /** * Test appointment conflict detection * * @covers Appointment::check_availability * @covers Appointment::times_overlap */ public function test_appointment_conflict_detection() { // Arrange $new_appointment_data = array( 'appointment_start_date' => '2024-02-15', 'appointment_start_time' => '14:30:00', 'appointment_end_time' => '15:00:00', 'clinic_id' => 1, 'doctor_id' => 100, 'patient_id' => 200 ); // Mock entity existence checks $this->mock_entities_exist(true, true, true); // Mock conflicting appointment exists $conflicting_appointments = array( array( 'id' => 456, 'appointment_start_time' => '14:00:00', 'appointment_end_time' => '14:45:00' // Overlaps with new appointment ) ); $this->mock_wpdb->expects($this->once()) ->method('get_results') ->willReturn($conflicting_appointments); // Act $result = Appointment::create($new_appointment_data); // Assert $this->assertInstanceOf('WP_Error', $result, 'Should return WP_Error for conflicting appointment'); $this->assertEquals('appointment_conflict', $result->get_error_code()); $error_data = $result->get_error_data(); $this->assertEquals(456, $error_data['conflicting_appointment_id']); } /** * Test appointment conflict detection with non-overlapping times * * @covers Appointment::check_availability * @covers Appointment::times_overlap */ public function test_appointment_no_conflict() { // Arrange $new_appointment_data = array( 'appointment_start_date' => '2024-02-15', 'appointment_start_time' => '15:30:00', 'appointment_end_time' => '16:00:00', 'clinic_id' => 1, 'doctor_id' => 100, 'patient_id' => 200 ); // Mock entity existence checks $this->mock_entities_exist(true, true, true); // Mock non-conflicting appointment exists $existing_appointments = array( array( 'id' => 456, 'appointment_start_time' => '14:00:00', 'appointment_end_time' => '14:30:00' // Does not overlap ) ); $this->mock_wpdb->expects($this->once()) ->method('get_results') ->willReturn($existing_appointments); // Mock successful insert $this->mock_wpdb->expects($this->once()) ->method('insert') ->willReturn(1); $this->mock_wpdb->insert_id = 789; // Act $result = Appointment::create($new_appointment_data); // Assert $this->assertIsInt($result, 'Should successfully create appointment without conflict'); $this->assertEquals(789, $result); } /** * Test available time slots generation * * @covers Appointment::get_available_slots * @covers Appointment::generate_time_slots */ public function test_get_available_slots() { // Arrange $availability_args = array( 'doctor_id' => 100, 'clinic_id' => 1, 'date' => '2024-02-15', 'duration' => 30 // 30 minutes slots ); // Mock doctor working hours for Thursday global $wp_test_expectations; $working_hours = array( 'thursday' => array( 'start_time' => '09:00', 'end_time' => '17:00' ) ); $wp_test_expectations['get_user_meta'] = wp_json_encode($working_hours); // Mock existing appointments $existing_appointments = array( array( 'appointment_start_time' => '10:00:00', 'appointment_end_time' => '10:30:00' ), array( 'appointment_start_time' => '14:30:00', 'appointment_end_time' => '15:00:00' ) ); $this->mock_wpdb->expects($this->once()) ->method('get_results') ->willReturn($existing_appointments); // Act $result = Appointment::get_available_slots($availability_args); // Assert $this->assertIsArray($result, 'Should return array of availability data'); $this->assertArrayHasKey('available_slots', $result); $this->assertArrayHasKey('working_hours', $result); $this->assertArrayHasKey('total_slots', $result); $this->assertArrayHasKey('booked_slots', $result); $this->assertEquals('2024-02-15', $result['date']); $this->assertEquals(100, $result['doctor_id']); $this->assertEquals('09:00', $result['working_hours']['start']); $this->assertEquals('17:00', $result['working_hours']['end']); $this->assertEquals(30, $result['slot_duration']); $this->assertEquals(2, $result['booked_slots']); // 2 existing appointments // Verify available slots don't include booked times $available_times = array_column($result['available_slots'], 'start_time'); $this->assertNotContains('10:00', $available_times, 'Booked slot should not be available'); $this->assertNotContains('14:30', $available_times, 'Booked slot should not be available'); } /** * Test appointment statistics calculation * * @covers Appointment::get_statistics */ public function test_appointment_statistics() { // Arrange $filters = array( 'clinic_id' => 1, 'doctor_id' => 100 ); // Mock database queries for different statistics $this->mock_wpdb->expects($this->exactly(8)) ->method('get_var') ->willReturnOnConsecutiveCalls( 50, // total_appointments 35, // scheduled_appointments 12, // completed_appointments 2, // cancelled_appointments 1, // no_show_appointments 3, // appointments_today 8, // appointments_this_week 25 // appointments_this_month ); // Act $statistics = Appointment::get_statistics($filters); // Assert $this->assertIsArray($statistics, 'Statistics should be an array'); $this->assertArrayHasKey('total_appointments', $statistics); $this->assertArrayHasKey('scheduled_appointments', $statistics); $this->assertArrayHasKey('completed_appointments', $statistics); $this->assertArrayHasKey('cancelled_appointments', $statistics); $this->assertArrayHasKey('no_show_appointments', $statistics); $this->assertArrayHasKey('appointments_today', $statistics); $this->assertArrayHasKey('appointments_this_week', $statistics); $this->assertArrayHasKey('appointments_this_month', $statistics); $this->assertEquals(50, $statistics['total_appointments']); $this->assertEquals(35, $statistics['scheduled_appointments']); $this->assertEquals(12, $statistics['completed_appointments']); $this->assertEquals(2, $statistics['cancelled_appointments']); $this->assertEquals(1, $statistics['no_show_appointments']); $this->assertEquals(3, $statistics['appointments_today']); $this->assertEquals(8, $statistics['appointments_this_week']); $this->assertEquals(25, $statistics['appointments_this_month']); } /** * Test appointment update with availability check * * @covers Appointment::update * @covers Appointment::check_availability */ public function test_appointment_update_with_availability_check() { // Arrange $appointment_id = 123; $update_data = array( 'appointment_start_time' => '16:00:00', 'appointment_end_time' => '16:30:00', 'description' => 'Updated appointment time' ); // Mock appointment exists $this->mock_wpdb->expects($this->once()) ->method('get_var') ->willReturn(1); // Appointment exists // Mock current appointment data $current_appointment = array( 'id' => 123, 'appointment_start_date' => '2024-02-15', 'appointment_start_time' => '14:30:00', 'appointment_end_time' => '15:00:00', 'doctor_id' => 100, 'patient_id' => 200, 'clinic_id' => 1 ); // Mock get appointment data $this->mock_wpdb->expects($this->once()) ->method('get_row') ->willReturn($current_appointment); // Mock no conflicts for new time $this->mock_wpdb->expects($this->once()) ->method('get_results') ->willReturn(array()); // No conflicts // Mock successful update $this->mock_wpdb->expects($this->once()) ->method('update') ->willReturn(1); // Act $result = Appointment::update($appointment_id, $update_data); // Assert $this->assertTrue($result, 'Appointment should be successfully updated'); } /** * Test times overlap detection * * @covers Appointment::times_overlap (private method tested via check_availability) */ public function test_times_overlap_detection() { // Test cases for time overlap scenarios $overlap_cases = array( // Case 1: Complete overlap array( 'time1' => array('start' => '10:00:00', 'end' => '11:00:00'), 'time2' => array('start' => '10:30:00', 'end' => '11:30:00'), 'should_overlap' => true ), // Case 2: No overlap - sequential array( 'time1' => array('start' => '10:00:00', 'end' => '11:00:00'), 'time2' => array('start' => '11:00:00', 'end' => '12:00:00'), 'should_overlap' => false ), // Case 3: Partial overlap at start array( 'time1' => array('start' => '10:30:00', 'end' => '11:30:00'), 'time2' => array('start' => '10:00:00', 'end' => '11:00:00'), 'should_overlap' => true ), // Case 4: No overlap - gap between array( 'time1' => array('start' => '10:00:00', 'end' => '11:00:00'), 'time2' => array('start' => '12:00:00', 'end' => '13:00:00'), 'should_overlap' => false ) ); foreach ($overlap_cases as $index => $case) { // Arrange $appointment_data = array( 'appointment_start_date' => '2024-02-15', 'appointment_start_time' => $case['time1']['start'], 'appointment_end_time' => $case['time1']['end'], 'clinic_id' => 1, 'doctor_id' => 100, 'patient_id' => 200 ); // Mock entity existence $this->mock_entities_exist(true, true, true); // Mock existing appointment $existing_appointments = array( array( 'id' => 999, 'appointment_start_time' => $case['time2']['start'], 'appointment_end_time' => $case['time2']['end'] ) ); $this->mock_wpdb->expects($this->once()) ->method('get_results') ->willReturn($existing_appointments); if ($case['should_overlap']) { // Mock no insert should happen due to conflict $this->mock_wpdb->expects($this->never()) ->method('insert'); } else { // Mock successful insert when no conflict $this->mock_wpdb->expects($this->once()) ->method('insert') ->willReturn(1); $this->mock_wpdb->insert_id = 100 + $index; } // Act $result = Appointment::create($appointment_data); // Assert if ($case['should_overlap']) { $this->assertInstanceOf('WP_Error', $result, "Case {$index}: Should detect overlap and return error"); $this->assertEquals('appointment_conflict', $result->get_error_code()); } else { $this->assertIsInt($result, "Case {$index}: Should successfully create appointment without overlap"); } // Reset mock wpdb for next iteration $this->setUp(); } } /** * Helper method to mock entity existence checks */ private function mock_entities_exist($clinic_exists, $doctor_exists, $patient_exists) { // Mock static method calls would require more complex mocking // For now, we'll assume entities exist in valid test cases // This would typically be handled by test doubles or dependency injection } }