<?php

declare(strict_types=1);

namespace DeskMoloni\Tests\Unit;

/**
 * Descomplicar® Crescimento Digital
 * https://descomplicar.pt
 */

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\DataProvider;
use DeskMoloni\Tests\TestCase as DeskMoloniTestCase;
use ReflectionClass;

/**
 * QueueProcessorTest
 * 
 * Unit tests for QueueProcessor class
 * Tests queue operations, job processing, and priority handling
 * 
 * @package DeskMoloni\Tests\Unit
 * @author Development Helper
 * @version 1.0.0
 */
#[CoversClass('QueueProcessor')]
class QueueProcessorTest extends DeskMoloniTestCase
{
    private $queue_processor;
    private $redis_mock;
    
    protected function setUp(): void
    {
        parent::setUp();
        
        // Create Redis mock
        $this->redis_mock = $this->createMock(\Redis::class);
        
        // Load QueueProcessor
        require_once 'modules/desk_moloni/libraries/QueueProcessor.php';
        $this->queue_processor = new QueueProcessor();
        
        // Inject Redis mock if possible
        $reflection = new ReflectionClass($this->queue_processor);
        if ($reflection->hasProperty('redis')) {
            $redis_property = $reflection->getProperty('redis');
            $redis_property->setAccessible(true);
            $redis_property->setValue($this->queue_processor, $this->redis_mock);
        }
    }
    
    #[Test]
    #[Group('unit')]
    public function testQueueProcessorInitialization(): void
    {
        $this->assertInstanceOf(QueueProcessor::class, $this->queue_processor);
        
        // Test priority constants
        $this->assertEquals(1, QueueProcessor::PRIORITY_LOW);
        $this->assertEquals(2, QueueProcessor::PRIORITY_NORMAL);
        $this->assertEquals(3, QueueProcessor::PRIORITY_HIGH);
        $this->assertEquals(4, QueueProcessor::PRIORITY_CRITICAL);
    }
    
    #[Test]
    #[Group('unit')]
    public function testAddJobToQueue(): void
    {
        $job_data = [
            'entity_type' => 'customer',
            'entity_id' => 123,
            'action' => 'create',
            'direction' => 'perfex_to_moloni',
            'priority' => QueueProcessor::PRIORITY_NORMAL,
            'payload' => ['test_data' => 'value']
        ];
        
        // Mock Redis operations
        $this->redis_mock
            ->expects($this->once())
            ->method('zadd')
            ->willReturn(1);
            
        $this->redis_mock
            ->expects($this->once())
            ->method('hset')
            ->willReturn(1);
        
        $job_id = $this->queue_processor->add_to_queue(
            $job_data['entity_type'],
            $job_data['entity_id'],
            $job_data['action'],
            $job_data['direction'],
            $job_data['priority'],
            $job_data['payload']
        );
        
        $this->assertIsString($job_id);
        $this->assertNotEmpty($job_id);
    }
    
    #[Test]
    #[Group('unit')]
    #[DataProvider('priorityProvider')]
    public function testQueuePriorityHandling(int $priority, string $expected_queue): void
    {
        $job_data = [
            'entity_type' => 'customer',
            'entity_id' => 123,
            'action' => 'create',
            'direction' => 'perfex_to_moloni',
            'priority' => $priority,
            'payload' => []
        ];
        
        // Mock Redis to capture which queue is used
        $this->redis_mock
            ->expects($this->once())
            ->method('zadd')
            ->with($expected_queue, $this->anything(), $this->anything())
            ->willReturn(1);
            
        $this->redis_mock
            ->expects($this->once())
            ->method('hset')
            ->willReturn(1);
        
        $job_id = $this->queue_processor->add_to_queue(
            $job_data['entity_type'],
            $job_data['entity_id'],
            $job_data['action'],
            $job_data['direction'],
            $job_data['priority'],
            $job_data['payload']
        );
        
        $this->assertNotFalse($job_id);
    }
    
    public static function priorityProvider(): array
    {
        return [
            'Low Priority' => [QueueProcessor::PRIORITY_LOW, 'desk_moloni:queue:main'],
            'Normal Priority' => [QueueProcessor::PRIORITY_NORMAL, 'desk_moloni:queue:main'],
            'High Priority' => [QueueProcessor::PRIORITY_HIGH, 'desk_moloni:queue:priority'],
            'Critical Priority' => [QueueProcessor::PRIORITY_CRITICAL, 'desk_moloni:queue:priority']
        ];
    }
    
    #[Test]
    #[Group('unit')]
    public function testProcessSingleJob(): void
    {
        $job_id = 'test_job_123';
        $job_data = [
            'id' => $job_id,
            'entity_type' => 'customer',
            'entity_id' => 456,
            'action' => 'create',
            'direction' => 'perfex_to_moloni',
            'payload' => ['company' => 'Test Company'],
            'attempts' => 0,
            'max_attempts' => 3,
            'created_at' => time()
        ];
        
        // Mock Redis operations for job retrieval
        $this->redis_mock
            ->expects($this->once())
            ->method('zpopmin')
            ->willReturn([$job_id => time()]);
            
        $this->redis_mock
            ->expects($this->once())
            ->method('hget')
            ->with('desk_moloni:jobs', $job_id)
            ->willReturn(json_encode($job_data));
            
        // Mock successful job processing
        $this->redis_mock
            ->expects($this->once())
            ->method('hdel')
            ->with('desk_moloni:jobs', $job_id)
            ->willReturn(1);
        
        $result = $this->queue_processor->process_queue(1, 30);
        
        $this->assertEquals(1, $result['processed']);
        $this->assertEquals(1, $result['success']);
        $this->assertEquals(0, $result['errors']);
    }
    
    #[Test]
    #[Group('unit')]
    public function testJobRetryMechanism(): void
    {
        $job_id = 'retry_test_job';
        $job_data = [
            'id' => $job_id,
            'entity_type' => 'customer',
            'entity_id' => 789,
            'action' => 'create',
            'direction' => 'perfex_to_moloni',
            'payload' => [],
            'attempts' => 1,
            'max_attempts' => 3,
            'created_at' => time()
        ];
        
        // Mock job failure that should trigger retry
        $this->redis_mock
            ->expects($this->once())
            ->method('zpopmin')
            ->willReturn([$job_id => time()]);
            
        $this->redis_mock
            ->expects($this->once())
            ->method('hget')
            ->willReturn(json_encode($job_data));
        
        // Mock retry scheduling
        $this->redis_mock
            ->expects($this->once())
            ->method('zadd')
            ->with('desk_moloni:queue:delayed', $this->anything(), $job_id)
            ->willReturn(1);
            
        $this->redis_mock
            ->expects($this->once())
            ->method('hset')
            ->willReturn(1);
        
        // Simulate job processing with failure
        $result = $this->queue_processor->process_queue(1, 30);
        
        $this->assertEquals(1, $result['processed']);
        $this->assertEquals(0, $result['success']);
        $this->assertEquals(1, $result['errors']);
    }
    
    #[Test]
    #[Group('unit')]
    public function testJobMaxRetriesExceeded(): void
    {
        $job_id = 'max_retries_job';
        $job_data = [
            'id' => $job_id,
            'entity_type' => 'customer',
            'entity_id' => 999,
            'action' => 'create',
            'direction' => 'perfex_to_moloni',
            'payload' => [],
            'attempts' => 3,
            'max_attempts' => 3,
            'created_at' => time()
        ];
        
        // Mock job that has exceeded max retries
        $this->redis_mock
            ->expects($this->once())
            ->method('zpopmin')
            ->willReturn([$job_id => time()]);
            
        $this->redis_mock
            ->expects($this->once())
            ->method('hget')
            ->willReturn(json_encode($job_data));
        
        // Should move to dead letter queue
        $this->redis_mock
            ->expects($this->once())
            ->method('zadd')
            ->with('desk_moloni:queue:dead_letter', $this->anything(), $job_id)
            ->willReturn(1);
        
        $result = $this->queue_processor->process_queue(1, 30);
        
        $this->assertEquals(1, $result['processed']);
        $this->assertEquals(0, $result['success']);
        $this->assertEquals(1, $result['errors']);
    }
    
    #[Test]
    #[Group('unit')]
    public function testQueueStatistics(): void
    {
        // Mock Redis responses for statistics
        $this->redis_mock
            ->expects($this->exactly(5))
            ->method('zcard')
            ->willReturnOnConsecutiveCalls(10, 5, 2, 1, 3); // main, priority, delayed, processing, dead_letter
            
        $this->redis_mock
            ->expects($this->once())
            ->method('hlen')
            ->willReturn(21); // total jobs
            
        $this->redis_mock
            ->expects($this->exactly(3))
            ->method('get')
            ->willReturnOnConsecutiveCalls('100', '95', '5'); // total_processed, total_success, total_errors
        
        $stats = $this->queue_processor->get_queue_statistics();
        
        $this->assertIsArray($stats);
        $this->assertArrayHasKey('pending_main', $stats);
        $this->assertArrayHasKey('pending_priority', $stats);
        $this->assertArrayHasKey('delayed', $stats);
        $this->assertArrayHasKey('processing', $stats);
        $this->assertArrayHasKey('dead_letter', $stats);
        $this->assertArrayHasKey('total_queued', $stats);
        $this->assertArrayHasKey('total_processed', $stats);
        $this->assertArrayHasKey('total_success', $stats);
        $this->assertArrayHasKey('total_errors', $stats);
        $this->assertArrayHasKey('success_rate', $stats);
        
        $this->assertEquals(10, $stats['pending_main']);
        $this->assertEquals(5, $stats['pending_priority']);
        $this->assertEquals(95.0, $stats['success_rate']);
    }
    
    #[Test]
    #[Group('unit')]
    public function testHealthCheck(): void
    {
        // Mock Redis connection test
        $this->redis_mock
            ->expects($this->once())
            ->method('ping')
            ->willReturn('+PONG');
            
        // Mock queue counts
        $this->redis_mock
            ->expects($this->exactly(2))
            ->method('zcard')
            ->willReturnOnConsecutiveCalls(0, 1); // dead_letter, processing
            
        $health = $this->queue_processor->health_check();
        
        $this->assertIsArray($health);
        $this->assertArrayHasKey('status', $health);
        $this->assertArrayHasKey('checks', $health);
        $this->assertArrayHasKey('redis', $health['checks']);
        $this->assertArrayHasKey('dead_letter', $health['checks']);
        $this->assertArrayHasKey('processing', $health['checks']);
        $this->assertArrayHasKey('memory', $health['checks']);
        
        $this->assertEquals('healthy', $health['status']);
        $this->assertTrue($health['checks']['redis']['status']);
    }
    
    #[Test]
    #[Group('unit')]
    public function testClearAllQueues(): void
    {
        // Mock Redis operations for clearing queues
        $this->redis_mock
            ->expects($this->exactly(5))
            ->method('del')
            ->willReturn(1);
            
        $this->redis_mock
            ->expects($this->once())
            ->method('flushdb')
            ->willReturn(true);
        
        $result = $this->queue_processor->clear_all_queues();
        
        $this->assertTrue($result);
    }
    
    #[Test]
    #[Group('unit')]
    public function testJobValidation(): void
    {
        // Test invalid entity type
        $result = $this->queue_processor->add_to_queue(
            'invalid_entity',
            123,
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL
        );
        
        $this->assertFalse($result);
        
        // Test invalid action
        $result = $this->queue_processor->add_to_queue(
            'customer',
            123,
            'invalid_action',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL
        );
        
        $this->assertFalse($result);
        
        // Test invalid direction
        $result = $this->queue_processor->add_to_queue(
            'customer',
            123,
            'create',
            'invalid_direction',
            QueueProcessor::PRIORITY_NORMAL
        );
        
        $this->assertFalse($result);
    }
    
    #[Test]
    #[Group('unit')]
    public function testBatchJobProcessing(): void
    {
        $batch_size = 5;
        $job_ids = [];
        
        // Mock multiple jobs in queue
        for ($i = 0; $i < $batch_size; $i++) {
            $job_ids[] = "batch_job_{$i}";
        }
        
        // Mock Redis returning batch of jobs
        $this->redis_mock
            ->expects($this->once())
            ->method('zpopmin')
            ->willReturn(array_combine($job_ids, array_fill(0, $batch_size, time())));
        
        // Mock job data retrieval
        $this->redis_mock
            ->expects($this->exactly($batch_size))
            ->method('hget')
            ->willReturnCallback(function($key, $job_id) {
                return json_encode([
                    'id' => $job_id,
                    'entity_type' => 'customer',
                    'entity_id' => rand(100, 999),
                    'action' => 'create',
                    'direction' => 'perfex_to_moloni',
                    'payload' => [],
                    'attempts' => 0,
                    'max_attempts' => 3
                ]);
            });
        
        // Mock successful processing
        $this->redis_mock
            ->expects($this->exactly($batch_size))
            ->method('hdel')
            ->willReturn(1);
        
        $result = $this->queue_processor->process_queue($batch_size, 60);
        
        $this->assertEquals($batch_size, $result['processed']);
        $this->assertEquals($batch_size, $result['success']);
        $this->assertEquals(0, $result['errors']);
    }
    
    #[Test]
    #[Group('unit')]
    public function testJobTimeout(): void
    {
        $timeout = 1; // 1 second timeout for testing
        
        $job_data = [
            'id' => 'timeout_job',
            'entity_type' => 'customer',
            'entity_id' => 123,
            'action' => 'create',
            'direction' => 'perfex_to_moloni',
            'payload' => [],
            'attempts' => 0,
            'max_attempts' => 3
        ];
        
        // Mock job retrieval
        $this->redis_mock
            ->expects($this->once())
            ->method('zpopmin')
            ->willReturn(['timeout_job' => time()]);
            
        $this->redis_mock
            ->expects($this->once())
            ->method('hget')
            ->willReturn(json_encode($job_data));
        
        // Process with very short timeout
        $start_time = microtime(true);
        $result = $this->queue_processor->process_queue(1, $timeout);
        $execution_time = microtime(true) - $start_time;
        
        // Should respect timeout
        $this->assertLessThanOrEqual($timeout + 0.5, $execution_time); // Allow small margin
    }
    
    protected function tearDown(): void
    {
        $this->queue_processor = null;
        $this->redis_mock = null;
        
        parent::tearDown();
    }
}