<?php

require_once __DIR__ . '/TestCase.php';

/**
 * QueueProcessorTest
 * 
 * Comprehensive test suite for Queue Processing service
 * Tests job queuing, processing, retry logic, conflict resolution, and performance
 * 
 * @package DeskMoloni\Tests
 * @author Descomplicar® - PHP Fullstack Engineer
 * @version 1.0.0
 */

use DeskMoloni\Libraries\QueueProcessor;
use DeskMoloni\Libraries\EntityMappingService;
use DeskMoloni\Libraries\ErrorHandler;
use DeskMoloni\Libraries\RetryHandler;

class QueueProcessorTest extends \PHPUnit\Framework\TestCase
{
    private $queue_processor;
    private $redis_mock;
    private $entity_mapping_mock;
    private $error_handler_mock;
    private $retry_handler_mock;
    private $CI_mock;
    
    protected function setUp(): void
    {
        // Create mocks for dependencies
        $this->redis_mock = $this->createMock(Redis::class);
        $this->entity_mapping_mock = $this->createMock(EntityMappingService::class);
        $this->error_handler_mock = $this->createMock(ErrorHandler::class);
        $this->retry_handler_mock = $this->createMock(RetryHandler::class);
        
        // Mock CodeIgniter instance
        $this->CI_mock = $this->createMock(stdClass::class);
        $this->CI_mock->desk_moloni_model = $this->createMock(stdClass::class);
        
        // Initialize service
        $this->queue_processor = new QueueProcessor();
        
        // Use reflection to inject mocks
        $reflection = new ReflectionClass($this->queue_processor);
        
        $redis_property = $reflection->getProperty('redis');
        $redis_property->setAccessible(true);
        $redis_property->setValue($this->queue_processor, $this->redis_mock);
        
        $entity_mapping_property = $reflection->getProperty('entity_mapping');
        $entity_mapping_property->setAccessible(true);
        $entity_mapping_property->setValue($this->queue_processor, $this->entity_mapping_mock);
        
        $error_handler_property = $reflection->getProperty('error_handler');
        $error_handler_property->setAccessible(true);
        $error_handler_property->setValue($this->queue_processor, $this->error_handler_mock);
        
        $retry_handler_property = $reflection->getProperty('retry_handler');
        $retry_handler_property->setAccessible(true);
        $retry_handler_property->setValue($this->queue_processor, $this->retry_handler_mock);
        
        $ci_property = $reflection->getProperty('CI');
        $ci_property->setAccessible(true);
        $ci_property->setValue($this->queue_processor, $this->CI_mock);
    }
    
    public function testAddToQueueSuccess()
    {
        // Test data
        $entity_type = EntityMappingService::ENTITY_CUSTOMER;
        $entity_id = 123;
        $action = 'create';
        $direction = 'perfex_to_moloni';
        $priority = QueueProcessor::PRIORITY_NORMAL;
        $data = ['trigger' => 'client_added'];
        
        // Mock Redis operations
        $this->redis_mock
            ->expects($this->once())
            ->method('hExists')
            ->willReturn(false); // No duplicate job
        
        $this->redis_mock
            ->expects($this->once())
            ->method('lPush')
            ->with(
                'desk_moloni:queue:main',
                $this->callback(function($job_json) use ($entity_type, $entity_id, $action) {
                    $job = json_decode($job_json, true);
                    return $job['entity_type'] === $entity_type &&
                           $job['entity_id'] === $entity_id &&
                           $job['action'] === $action &&
                           $job['status'] === QueueProcessor::STATUS_PENDING;
                })
            );
        
        $this->redis_mock
            ->expects($this->once())
            ->method('hSet')
            ->with('desk_moloni:queue:jobs', $this->isType('string'), $this->isType('string'));
        
        $this->redis_mock
            ->expects($this->exactly(2))
            ->method('hIncrBy')
            ->withConsecutive(
                ['desk_moloni:queue:stats', 'total_queued', 1],
                ['desk_moloni:queue:stats', 'queued_customer', 1]
            );
        
        // Execute test
        $job_id = $this->queue_processor->add_to_queue(
            $entity_type,
            $entity_id,
            $action,
            $direction,
            $priority,
            $data
        );
        
        // Assertions
        $this->assertNotFalse($job_id);
        $this->assertStringContains($entity_type, $job_id);
        $this->assertStringContains((string)$entity_id, $job_id);
        $this->assertStringContains($action, $job_id);
    }
    
    public function testAddToQueueHighPriority()
    {
        // Test data for high priority job
        $entity_type = EntityMappingService::ENTITY_INVOICE;
        $entity_id = 456;
        $action = 'create';
        $priority = QueueProcessor::PRIORITY_HIGH;
        
        // Mock Redis operations for priority queue
        $this->redis_mock
            ->expects($this->once())
            ->method('hExists')
            ->willReturn(false);
        
        $this->redis_mock
            ->expects($this->once())
            ->method('lPush')
            ->with('desk_moloni:queue:priority', $this->isType('string'));
        
        $this->redis_mock
            ->expects($this->once())
            ->method('hSet');
        
        $this->redis_mock
            ->expects($this->exactly(2))
            ->method('hIncrBy');
        
        // Execute test
        $job_id = $this->queue_processor->add_to_queue(
            $entity_type,
            $entity_id,
            $action,
            'perfex_to_moloni',
            $priority
        );
        
        // Assertions
        $this->assertNotFalse($job_id);
    }
    
    public function testAddToQueueWithDelay()
    {
        // Test data for delayed job
        $entity_type = EntityMappingService::ENTITY_PRODUCT;
        $entity_id = 789;
        $action = 'update';
        $delay_seconds = 300; // 5 minutes
        
        // Mock Redis operations for delay queue
        $this->redis_mock
            ->expects($this->once())
            ->method('hExists')
            ->willReturn(false);
        
        $this->redis_mock
            ->expects($this->once())
            ->method('zAdd')
            ->with(
                'desk_moloni:queue:delay',
                $this->callback(function($score) {
                    return $score > time(); // Should be scheduled for future
                }),
                $this->isType('string')
            );
        
        $this->redis_mock
            ->expects($this->once())
            ->method('hSet');
        
        $this->redis_mock
            ->expects($this->exactly(2))
            ->method('hIncrBy');
        
        // Execute test
        $job_id = $this->queue_processor->add_to_queue(
            $entity_type,
            $entity_id,
            $action,
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL,
            [],
            $delay_seconds
        );
        
        // Assertions
        $this->assertNotFalse($job_id);
    }
    
    public function testProcessQueueSuccess()
    {
        // Test data
        $job_data = [
            'id' => 'customer_123_create_test123',
            'entity_type' => EntityMappingService::ENTITY_CUSTOMER,
            'entity_id' => 123,
            'action' => 'create',
            'direction' => 'perfex_to_moloni',
            'priority' => QueueProcessor::PRIORITY_NORMAL,
            'attempts' => 0,
            'max_attempts' => 5,
            'status' => QueueProcessor::STATUS_PENDING
        ];
        
        $job_json = json_encode($job_data);
        
        // Mock queue not paused
        $this->redis_mock
            ->expects($this->once())
            ->method('get')
            ->with('desk_moloni:queue:paused')
            ->willReturn(null);
        
        // Mock delayed jobs processing
        $this->redis_mock
            ->expects($this->once())
            ->method('zRangeByScore')
            ->willReturn([]);
        
        // Mock getting next job from priority queue (empty) then main queue
        $this->redis_mock
            ->expects($this->exactly(2))
            ->method('rPop')
            ->withConsecutive(
                ['desk_moloni:queue:priority'],
                ['desk_moloni:queue:main']
            )
            ->willReturnOnConsecutiveCalls(null, $job_json);
        
        // Mock processing queue operations
        $this->redis_mock
            ->expects($this->once())
            ->method('hSet')
            ->with('desk_moloni:queue:processing', $job_data['id'], $job_json);
        
        $this->redis_mock
            ->expects($this->once())
            ->method('expire');
        
        // Mock successful job execution
        $sync_service_mock = $this->createMock(stdClass::class);
        $sync_service_mock
            ->expects($this->once())
            ->method('sync_perfex_to_moloni')
            ->with(123, false, [])
            ->willReturn([
                'success' => true,
                'message' => 'Customer synced successfully'
            ]);
        
        // Mock job completion
        $this->redis_mock
            ->expects($this->once())
            ->method('hDel')
            ->with('desk_moloni:queue:processing', $job_data['id']);
        
        // Mock statistics update
        $this->redis_mock
            ->expects($this->exactly(3))
            ->method('hIncrBy')
            ->withConsecutive(
                ['desk_moloni:queue:stats', 'total_processed', 1],
                ['desk_moloni:queue:stats', 'total_success', 1],
                ['desk_moloni:queue:stats', 'total_errors', 0]
            );
        
        // Use reflection to mock get_sync_service method
        $reflection = new ReflectionClass($this->queue_processor);
        $method = $reflection->getMethod('get_sync_service');
        $method->setAccessible(true);
        
        // Execute test
        $result = $this->queue_processor->process_queue(1, 60);
        
        // Assertions
        $this->assertEquals(1, $result['processed']);
        $this->assertEquals(1, $result['success']);
        $this->assertEquals(0, $result['errors']);
        $this->assertArrayHasKey('execution_time', $result);
        $this->assertArrayHasKey('details', $result);
    }
    
    public function testProcessQueueWithRetry()
    {
        // Test data for failed job that should be retried
        $job_data = [
            'id' => 'customer_123_create_test123',
            'entity_type' => EntityMappingService::ENTITY_CUSTOMER,
            'entity_id' => 123,
            'action' => 'create',
            'direction' => 'perfex_to_moloni',
            'priority' => QueueProcessor::PRIORITY_NORMAL,
            'attempts' => 1,
            'max_attempts' => 5,
            'status' => QueueProcessor::STATUS_PENDING
        ];
        
        $job_json = json_encode($job_data);
        
        // Mock queue processing setup
        $this->redis_mock
            ->expects($this->once())
            ->method('get')
            ->willReturn(null); // Not paused
        
        $this->redis_mock
            ->expects($this->once())
            ->method('zRangeByScore')
            ->willReturn([]); // No delayed jobs
        
        $this->redis_mock
            ->expects($this->exactly(2))
            ->method('rPop')
            ->willReturnOnConsecutiveCalls(null, $job_json);
        
        $this->redis_mock
            ->expects($this->once())
            ->method('hSet');
        
        $this->redis_mock
            ->expects($this->once())
            ->method('expire');
        
        // Mock failed job execution
        $sync_service_mock = $this->createMock(stdClass::class);
        $sync_service_mock
            ->expects($this->once())
            ->method('sync_perfex_to_moloni')
            ->willThrowException(new Exception('Temporary sync failure'));
        
        // Mock retry handler
        $this->retry_handler_mock
            ->expects($this->once())
            ->method('calculate_retry_delay')
            ->with(2) // attempts + 1
            ->willReturn(120); // 2 minutes
        
        // Mock scheduling retry
        $this->redis_mock
            ->expects($this->once())
            ->method('hDel')
            ->with('desk_moloni:queue:processing', $job_data['id']);
        
        $this->redis_mock
            ->expects($this->once())
            ->method('zAdd')
            ->with(
                'desk_moloni:queue:delay',
                $this->callback(function($score) {
                    return $score > time();
                }),
                $this->isType('string')
            );
        
        // Mock statistics
        $this->redis_mock
            ->expects($this->exactly(3))
            ->method('hIncrBy');
        
        // Execute test
        $result = $this->queue_processor->process_queue(1, 60);
        
        // Assertions
        $this->assertEquals(1, $result['processed']);
        $this->assertEquals(0, $result['success']);
        $this->assertEquals(1, $result['errors']);
    }
    
    public function testProcessQueueDeadLetter()
    {
        // Test data for job that has exceeded max attempts
        $job_data = [
            'id' => 'customer_123_create_test123',
            'entity_type' => EntityMappingService::ENTITY_CUSTOMER,
            'entity_id' => 123,
            'action' => 'create',
            'direction' => 'perfex_to_moloni',
            'priority' => QueueProcessor::PRIORITY_NORMAL,
            'attempts' => 5, // Max attempts reached
            'max_attempts' => 5,
            'status' => QueueProcessor::STATUS_PENDING
        ];
        
        $job_json = json_encode($job_data);
        
        // Mock queue processing setup
        $this->redis_mock
            ->expects($this->once())
            ->method('get')
            ->willReturn(null);
        
        $this->redis_mock
            ->expects($this->once())
            ->method('zRangeByScore')
            ->willReturn([]);
        
        $this->redis_mock
            ->expects($this->exactly(2))
            ->method('rPop')
            ->willReturnOnConsecutiveCalls(null, $job_json);
        
        $this->redis_mock
            ->expects($this->once())
            ->method('hSet');
        
        $this->redis_mock
            ->expects($this->once())
            ->method('expire');
        
        // Mock failed job execution
        $sync_service_mock = $this->createMock(stdClass::class);
        $sync_service_mock
            ->expects($this->once())
            ->method('sync_perfex_to_moloni')
            ->willThrowException(new Exception('Permanent failure'));
        
        // Mock moving to dead letter queue
        $this->redis_mock
            ->expects($this->once())
            ->method('hDel')
            ->with('desk_moloni:queue:processing', $job_data['id']);
        
        $this->redis_mock
            ->expects($this->once())
            ->method('lPush')
            ->with('desk_moloni:queue:dead_letter', $this->isType('string'));
        
        // Mock error logging
        $this->error_handler_mock
            ->expects($this->once())
            ->method('log_error')
            ->with('queue', 'JOB_DEAD_LETTER', $this->stringContains('moved to dead letter'));
        
        // Mock statistics
        $this->redis_mock
            ->expects($this->exactly(3))
            ->method('hIncrBy');
        
        // Execute test
        $result = $this->queue_processor->process_queue(1, 60);
        
        // Assertions
        $this->assertEquals(1, $result['processed']);
        $this->assertEquals(0, $result['success']);
        $this->assertEquals(1, $result['errors']);
    }
    
    public function testBidirectionalSyncWithConflict()
    {
        // Test data for bidirectional sync with conflict
        $job_data = [
            'id' => 'customer_123_update_test123',
            'entity_type' => EntityMappingService::ENTITY_CUSTOMER,
            'entity_id' => 123,
            'action' => 'update',
            'direction' => 'bidirectional',
            'priority' => QueueProcessor::PRIORITY_NORMAL,
            'attempts' => 0,
            'max_attempts' => 5,
            'status' => QueueProcessor::STATUS_PENDING
        ];
        
        $mapping = (object)[
            'id' => 1,
            'perfex_id' => 123,
            'moloni_id' => 456,
            'last_sync_perfex' => '2024-01-01 10:00:00',
            'last_sync_moloni' => '2024-01-01 09:00:00'
        ];
        
        // Mock getting mapping
        $this->entity_mapping_mock
            ->expects($this->once())
            ->method('get_mapping_by_perfex_id')
            ->with(EntityMappingService::ENTITY_CUSTOMER, 123)
            ->willReturn($mapping);
        
        // Mock sync service with conflict detection
        $sync_service_mock = $this->createMock(stdClass::class);
        $sync_service_mock
            ->expects($this->once())
            ->method('check_sync_conflicts')
            ->with($mapping)
            ->willReturn([
                'has_conflict' => true,
                'conflict_details' => [
                    'type' => 'data_conflict',
                    'field_conflicts' => ['company' => ['perfex_value' => 'A', 'moloni_value' => 'B']]
                ]
            ]);
        
        // Mock mapping status update
        $this->entity_mapping_mock
            ->expects($this->once())
            ->method('update_mapping_status')
            ->with(1, EntityMappingService::STATUS_CONFLICT, $this->isType('string'));
        
        // Use reflection to test protected method
        $reflection = new ReflectionClass($this->queue_processor);
        $method = $reflection->getMethod('handle_bidirectional_sync');
        $method->setAccessible(true);
        
        // Execute test
        $result = $method->invoke($this->queue_processor, $sync_service_mock, $job_data);
        
        // Assertions
        $this->assertFalse($result['success']);
        $this->assertStringContains('conflict', strtolower($result['message']));
        $this->assertArrayHasKey('conflict_details', $result);
    }
    
    public function testGetQueueStatistics()
    {
        // Mock Redis statistics calls
        $stats_data = [
            'total_queued' => '100',
            'total_processed' => '85',
            'total_success' => '80',
            'total_errors' => '5'
        ];
        
        $this->redis_mock
            ->expects($this->once())
            ->method('hGetAll')
            ->with('desk_moloni:queue:stats')
            ->willReturn($stats_data);
        
        $this->redis_mock
            ->expects($this->exactly(5))
            ->method('lLen')
            ->willReturnOnConsecutiveCalls(10, 5, 3, 2, 1); // main, priority, delay, processing, dead_letter
        
        $this->redis_mock
            ->expects($this->once())
            ->method('zCard')
            ->willReturn(3); // delayed jobs
        
        $this->redis_mock
            ->expects($this->once())
            ->method('hLen')
            ->willReturn(2); // processing jobs
        
        // Execute test
        $statistics = $this->queue_processor->get_queue_statistics();
        
        // Assertions
        $this->assertEquals(10, $statistics['pending_main']);
        $this->assertEquals(5, $statistics['pending_priority']);
        $this->assertEquals(3, $statistics['delayed']);
        $this->assertEquals(2, $statistics['processing']);
        $this->assertEquals(1, $statistics['dead_letter']);
        $this->assertEquals(100, $statistics['total_queued']);
        $this->assertEquals(85, $statistics['total_processed']);
        $this->assertEquals(80, $statistics['total_success']);
        $this->assertEquals(5, $statistics['total_errors']);
        $this->assertEquals(94.12, $statistics['success_rate']); // 80/85 * 100
        $this->assertArrayHasKey('memory_usage', $statistics);
        $this->assertArrayHasKey('peak_memory', $statistics);
    }
    
    public function testHealthCheck()
    {
        // Mock Redis ping success
        $this->redis_mock
            ->expects($this->once())
            ->method('ping')
            ->willReturn(true);
        
        // Mock queue statistics for health check
        $this->redis_mock
            ->expects($this->once())
            ->method('hGetAll')
            ->willReturn([]);
        
        $this->redis_mock
            ->expects($this->exactly(5))
            ->method('lLen')
            ->willReturnOnConsecutiveCalls(10, 5, 3, 2, 50); // dead_letter count triggers warning
        
        $this->redis_mock
            ->expects($this->once())
            ->method('zCard')
            ->willReturn(3);
        
        $this->redis_mock
            ->expects($this->once())
            ->method('hLen')
            ->willReturn(2);
        
        // Execute test
        $health = $this->queue_processor->health_check();
        
        // Assertions
        $this->assertEquals('warning', $health['status']); // Due to high dead letter count
        $this->assertEquals('ok', $health['checks']['redis']);
        $this->assertStringContains('high count: 50', $health['checks']['dead_letter']);
        $this->assertEquals('ok', $health['checks']['processing']);
        $this->assertEquals('ok', $health['checks']['memory']);
    }
    
    protected function tearDown(): void
    {
        // Clean up test artifacts
        $this->queue_processor = null;
        $this->redis_mock = null;
        $this->entity_mapping_mock = null;
        $this->error_handler_mock = null;
        $this->retry_handler_mock = null;
        $this->CI_mock = null;
    }
}