<?php
/**
 * Appointment Model Unit Tests
 *
 * Tests for appointment scheduling, validation, conflict detection and business logic
 *
 * @package Care_API\Tests\Unit\Models
 * @version 1.0.0
 * @author  Descomplicar® <dev@descomplicar.pt>
 * @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
    }
}