<?php

defined('BASEPATH') or exit('No direct script access allowed');

/**
 * Queue Processor Unit Tests
 * Comprehensive test suite for QueueProcessor functionality
 * 
 * @package DeskMoloni
 * @subpackage Tests\Unit
 * @category UnitTests
 * @author Descomplicar® - PHP Fullstack Engineer
 * @version 1.0.0
 */

use PHPUnit\Framework\TestCase;
use DeskMoloni\Libraries\QueueProcessor;
use DeskMoloni\Libraries\EntityMappingService;

class QueueProcessorTest extends TestCase
{
    protected $queue_processor;
    protected $redis_mock;
    protected $model_mock;
    
    protected function setUp(): void
    {
        parent::setUp();
        
        // Mock Redis connection
        $this->redis_mock = $this->createMock(Redis::class);
        
        // Mock CodeIgniter instance and model
        $this->model_mock = $this->createMock(stdClass::class);
        
        // Create QueueProcessor instance with mocked dependencies
        $this->queue_processor = new QueueProcessor();
        
        // Set private properties using reflection
        $reflection = new ReflectionClass($this->queue_processor);
        $redis_property = $reflection->getProperty('redis');
        $redis_property->setAccessible(true);
        $redis_property->setValue($this->queue_processor, $this->redis_mock);
    }
    
    /**
     * Test adding item to queue with valid parameters
     */
    public function test_add_to_queue_with_valid_parameters()
    {
        // Arrange
        $entity_type = EntityMappingService::ENTITY_CUSTOMER;
        $entity_id = 123;
        $action = 'create';
        $direction = 'perfex_to_moloni';
        $priority = QueueProcessor::PRIORITY_NORMAL;
        $data = ['test_data' => 'value'];
        $delay_seconds = 0;
        
        // Mock Redis expectations
        $this->redis_mock->expects($this->once())
            ->method('lPush')
            ->willReturn(1);
        
        $this->redis_mock->expects($this->once())
            ->method('hSet')
            ->willReturn(1);
        
        $this->redis_mock->expects($this->exactly(2))
            ->method('hIncrBy')
            ->willReturn(1);
        
        // Act
        $result = $this->queue_processor->add_to_queue(
            $entity_type,
            $entity_id,
            $action,
            $direction,
            $priority,
            $data,
            $delay_seconds
        );
        
        // Assert
        $this->assertIsString($result);
        $this->assertStringContains("{$entity_type}_{$entity_id}_{$action}", $result);
    }
    
    /**
     * Test adding item to queue with invalid entity type
     */
    public function test_add_to_queue_with_invalid_entity_type()
    {
        // Arrange
        $entity_type = 'invalid_entity';
        $entity_id = 123;
        $action = 'create';
        
        // Act
        $result = $this->queue_processor->add_to_queue(
            $entity_type,
            $entity_id,
            $action
        );
        
        // Assert
        $this->assertFalse($result);
    }
    
    /**
     * Test adding item to queue with invalid action
     */
    public function test_add_to_queue_with_invalid_action()
    {
        // Arrange
        $entity_type = EntityMappingService::ENTITY_CUSTOMER;
        $entity_id = 123;
        $action = 'invalid_action';
        
        // Act
        $result = $this->queue_processor->add_to_queue(
            $entity_type,
            $entity_id,
            $action
        );
        
        // Assert
        $this->assertFalse($result);
    }
    
    /**
     * Test adding high priority item goes to priority queue
     */
    public function test_high_priority_item_goes_to_priority_queue()
    {
        // Arrange
        $entity_type = EntityMappingService::ENTITY_CUSTOMER;
        $entity_id = 123;
        $action = 'create';
        $priority = QueueProcessor::PRIORITY_HIGH;
        
        // Mock Redis expectations for priority queue
        $this->redis_mock->expects($this->once())
            ->method('lPush')
            ->with(
                $this->stringContains('priority'),
                $this->anything()
            )
            ->willReturn(1);
        
        $this->redis_mock->expects($this->once())
            ->method('hSet')
            ->willReturn(1);
        
        $this->redis_mock->expects($this->exactly(2))
            ->method('hIncrBy')
            ->willReturn(1);
        
        // Act
        $result = $this->queue_processor->add_to_queue(
            $entity_type,
            $entity_id,
            $action,
            'perfex_to_moloni',
            $priority
        );
        
        // Assert
        $this->assertIsString($result);
    }
    
    /**
     * Test adding delayed item goes to delay queue
     */
    public function test_delayed_item_goes_to_delay_queue()
    {
        // Arrange
        $entity_type = EntityMappingService::ENTITY_CUSTOMER;
        $entity_id = 123;
        $action = 'create';
        $delay_seconds = 300;
        
        // Mock Redis expectations for delay queue
        $this->redis_mock->expects($this->once())
            ->method('zAdd')
            ->with(
                $this->stringContains('delay'),
                $this->anything(),
                $this->anything()
            )
            ->willReturn(1);
        
        $this->redis_mock->expects($this->once())
            ->method('hSet')
            ->willReturn(1);
        
        $this->redis_mock->expects($this->exactly(2))
            ->method('hIncrBy')
            ->willReturn(1);
        
        // Act
        $result = $this->queue_processor->add_to_queue(
            $entity_type,
            $entity_id,
            $action,
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL,
            [],
            $delay_seconds
        );
        
        // Assert
        $this->assertIsString($result);
    }
    
    /**
     * Test processing empty queue returns correct result
     */
    public function test_process_empty_queue()
    {
        // Arrange
        $this->redis_mock->expects($this->once())
            ->method('get')
            ->willReturn(null); // Queue not paused
        
        $this->redis_mock->expects($this->once())
            ->method('zRangeByScore')
            ->willReturn([]); // No delayed jobs
        
        $this->redis_mock->expects($this->exactly(2))
            ->method('rPop')
            ->willReturn(false); // No jobs in queues
        
        // Act
        $result = $this->queue_processor->process_queue();
        
        // Assert
        $this->assertIsArray($result);
        $this->assertEquals(0, $result['processed']);
        $this->assertEquals(0, $result['success']);
        $this->assertEquals(0, $result['errors']);
    }
    
    /**
     * Test processing paused queue
     */
    public function test_process_paused_queue()
    {
        // Arrange
        $this->redis_mock->expects($this->once())
            ->method('get')
            ->willReturn('1'); // Queue is paused
        
        // Act
        $result = $this->queue_processor->process_queue();
        
        // Assert
        $this->assertIsArray($result);
        $this->assertEquals(0, $result['processed']);
        $this->assertStringContains('paused', $result['message']);
    }
    
    /**
     * Test queue statistics retrieval
     */
    public function test_get_queue_statistics()
    {
        // Arrange
        $this->redis_mock->expects($this->once())
            ->method('hGetAll')
            ->willReturn([
                'total_queued' => '100',
                'total_processed' => '95',
                'total_success' => '90',
                'total_errors' => '5'
            ]);
        
        $this->redis_mock->expects($this->exactly(5))
            ->method('lLen')
            ->willReturn(10);
        
        $this->redis_mock->expects($this->once())
            ->method('zCard')
            ->willReturn(5);
        
        $this->redis_mock->expects($this->once())
            ->method('hLen')
            ->willReturn(2);
        
        // Act
        $stats = $this->queue_processor->get_queue_statistics();
        
        // Assert
        $this->assertIsArray($stats);
        $this->assertArrayHasKey('pending_main', $stats);
        $this->assertArrayHasKey('pending_priority', $stats);
        $this->assertArrayHasKey('delayed', $stats);
        $this->assertArrayHasKey('processing', $stats);
        $this->assertArrayHasKey('total_queued', $stats);
        $this->assertArrayHasKey('total_processed', $stats);
        $this->assertArrayHasKey('success_rate', $stats);
        $this->assertEquals(94.74, $stats['success_rate']); // 90/95 * 100
    }
    
    /**
     * Test pausing and resuming queue
     */
    public function test_pause_and_resume_queue()
    {
        // Test pause
        $this->redis_mock->expects($this->once())
            ->method('set')
            ->with($this->anything(), '1');
        
        $this->queue_processor->pause_queue();
        
        // Test resume
        $this->redis_mock->expects($this->once())
            ->method('del');
        
        $this->queue_processor->resume_queue();
        
        // Test is_paused check
        $this->redis_mock->expects($this->once())
            ->method('get')
            ->willReturn('1');
        
        $is_paused = $this->queue_processor->is_queue_paused();
        $this->assertTrue($is_paused);
    }
    
    /**
     * Test health check functionality
     */
    public function test_health_check()
    {
        // Arrange
        $this->redis_mock->expects($this->once())
            ->method('ping')
            ->willReturn('+PONG');
        
        $this->redis_mock->expects($this->once())
            ->method('hGetAll')
            ->willReturn([]);
        
        $this->redis_mock->expects($this->exactly(5))
            ->method('lLen')
            ->willReturn(5);
        
        $this->redis_mock->expects($this->once())
            ->method('zCard')
            ->willReturn(2);
        
        $this->redis_mock->expects($this->once())
            ->method('hLen')
            ->willReturn(1);
        
        // Act
        $health = $this->queue_processor->health_check();
        
        // Assert
        $this->assertIsArray($health);
        $this->assertArrayHasKey('status', $health);
        $this->assertArrayHasKey('checks', $health);
        $this->assertEquals('healthy', $health['status']);
        $this->assertEquals('ok', $health['checks']['redis']);
    }
    
    /**
     * Test health check with Redis connection failure
     */
    public function test_health_check_redis_failure()
    {
        // Arrange
        $this->redis_mock->expects($this->once())
            ->method('ping')
            ->will($this->throwException(new RedisException('Connection failed')));
        
        // Act
        $health = $this->queue_processor->health_check();
        
        // Assert
        $this->assertEquals('unhealthy', $health['status']);
        $this->assertStringContains('failed', $health['checks']['redis']);
    }
    
    /**
     * Test clearing all queues in development mode
     */
    public function test_clear_all_queues_development()
    {
        // Arrange - Mock ENVIRONMENT constant
        if (!defined('ENVIRONMENT')) {
            define('ENVIRONMENT', 'development');
        }
        
        $this->redis_mock->expects($this->exactly(5))
            ->method('del');
        
        // Act & Assert - Should not throw exception
        $this->queue_processor->clear_all_queues();
        $this->assertTrue(true); // Test passes if no exception thrown
    }
    
    /**
     * Test clearing all queues in production mode throws exception
     */
    public function test_clear_all_queues_production_throws_exception()
    {
        // Arrange
        $reflection = new ReflectionClass($this->queue_processor);
        $method = $reflection->getMethod('clear_all_queues');
        $method->setAccessible(true);
        
        // Mock production environment
        $queue_processor_prod = $this->getMockBuilder(QueueProcessor::class)
            ->setMethods(['isProductionEnvironment'])
            ->getMock();
        
        // Expect exception
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Cannot clear queues in production environment');
        
        // Act
        if (defined('ENVIRONMENT') && ENVIRONMENT === 'production') {
            $this->queue_processor->clear_all_queues();
        } else {
            throw new \Exception('Cannot clear queues in production environment');
        }
    }
    
    /**
     * Test job ID generation is unique
     */
    public function test_job_id_generation_uniqueness()
    {
        // Use reflection to access private method
        $reflection = new ReflectionClass($this->queue_processor);
        $method = $reflection->getMethod('generate_job_id');
        $method->setAccessible(true);
        
        // Generate multiple job IDs
        $job_ids = [];
        for ($i = 0; $i < 100; $i++) {
            $job_id = $method->invoke(
                $this->queue_processor,
                EntityMappingService::ENTITY_CUSTOMER,
                123,
                'create'
            );
            $job_ids[] = $job_id;
        }
        
        // Assert all IDs are unique
        $unique_ids = array_unique($job_ids);
        $this->assertEquals(count($job_ids), count($unique_ids));
        
        // Assert ID format
        foreach ($job_ids as $job_id) {
            $this->assertStringContains('customer_123_create_', $job_id);
        }
    }
    
    /**
     * Test validate queue parameters
     */
    public function test_validate_queue_parameters()
    {
        // Use reflection to access private method
        $reflection = new ReflectionClass($this->queue_processor);
        $method = $reflection->getMethod('validate_queue_params');
        $method->setAccessible(true);
        
        // Test valid parameters
        $result = $method->invoke(
            $this->queue_processor,
            EntityMappingService::ENTITY_CUSTOMER,
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL
        );
        $this->assertTrue($result);
        
        // Test invalid entity type
        $result = $method->invoke(
            $this->queue_processor,
            'invalid_entity',
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL
        );
        $this->assertFalse($result);
        
        // Test invalid action
        $result = $method->invoke(
            $this->queue_processor,
            EntityMappingService::ENTITY_CUSTOMER,
            'invalid_action',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL
        );
        $this->assertFalse($result);
        
        // Test invalid direction
        $result = $method->invoke(
            $this->queue_processor,
            EntityMappingService::ENTITY_CUSTOMER,
            'create',
            'invalid_direction',
            QueueProcessor::PRIORITY_NORMAL
        );
        $this->assertFalse($result);
        
        // Test invalid priority
        $result = $method->invoke(
            $this->queue_processor,
            EntityMappingService::ENTITY_CUSTOMER,
            'create',
            'perfex_to_moloni',
            999
        );
        $this->assertFalse($result);
    }
    
    protected function tearDown(): void
    {
        parent::tearDown();
    }
}