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

<?php

require_once __DIR__ . '/TestCase.php';

/**
 * IntegrationTest
 * 
 * End-to-end integration tests for the complete Desk-Moloni synchronization system
 * Tests full workflow from hooks to queue processing to API calls
 * 
 * @package DeskMoloni\Tests
 * @author Descomplicar® - PHP Fullstack Engineer
 * @version 1.0.0
 */

use DeskMoloni\Libraries\ClientSyncService;
use DeskMoloni\Libraries\ProductSyncService;
use DeskMoloni\Libraries\InvoiceSyncService;
use DeskMoloni\Libraries\EstimateSyncService;
use DeskMoloni\Libraries\QueueProcessor;
use DeskMoloni\Libraries\PerfexHooks;
use DeskMoloni\Libraries\EntityMappingService;

class IntegrationTest extends \PHPUnit\Framework\TestCase
{
    private $client_sync;
    private $product_sync;
    private $invoice_sync;
    private $estimate_sync;
    private $queue_processor;
    private $perfex_hooks;
    private $entity_mapping;
    
    protected function setUp(): void
    {
        parent::setUp(); // This will call initializeCodeIgniter() from TestCase.php

        // Define mock for get_option if it doesn't exist
        if (!function_exists('get_option')) {
            function get_option($name, $default = '') {
                $options = [
                    'desk_moloni_redis_host' => '127.0.0.1',
                    'desk_moloni_redis_port' => 6379,
                    'desk_moloni_redis_password' => '',
                    'desk_moloni_redis_db' => 1,
                    'desk_moloni_sync_enabled' => '1',
                    'desk_moloni_sync_customers' => '1',
                    'desk_moloni_sync_invoices' => '1',
                    'desk_moloni_sync_products' => '1',
                    'desk_moloni_sync_estimates' => '1',
                    'desk_moloni_sync_payments' => '1',
                ];
                return $options[$name] ?? $default;
            }
        }
        
        // These services don't have complex dependencies in their constructors yet
        $this->entity_mapping = new EntityMappingService();
        $this->client_sync = new ClientSyncService();
        $this->product_sync = new ProductSyncService();
        $this->invoice_sync = new InvoiceSyncService();
        $this->estimate_sync = new EstimateSyncService();

        // Instantiate PerfexHooks, which will create the QueueProcessor with all its dependencies
        $this->perfex_hooks = new PerfexHooks();

        // Get the QueueProcessor instance from the hooks class for test assertions
        $reflection = new \ReflectionClass($this->perfex_hooks);
        $queue_processor_property = $reflection->getProperty('queue_processor');
        $queue_processor_property->setAccessible(true);
        $this->queue_processor = $queue_processor_property->getValue($this->perfex_hooks);

        // Clean up any existing test data
        $this->cleanupTestData();
    }
    
    public function testCompleteCustomerSyncWorkflow()
    {
        // Test complete customer synchronization workflow
        
        // Step 1: Create customer in Perfex (simulates user action)
        $perfex_client_data = [
            'company' => 'Integration Test Company Ltd',
            'vat' => 'PT999888777',
            'email' => 'integration@test.com',
            'phonenumber' => '+351999888777',
            'billing_street' => 'Test Integration Street 123',
            'billing_city' => 'Porto',
            'billing_zip' => '4000-999',
            'billing_country' => 'PT',
            'active' => 1
        ];
        
        // Mock Perfex client creation
        $perfex_client_id = 9999; // Simulated ID
        
        // Step 2: Hook triggers queue job
        $job_id = $this->queue_processor->add_to_queue(
            EntityMappingService::ENTITY_CUSTOMER,
            $perfex_client_id,
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL,
            ['trigger' => 'integration_test']
        );
        
        $this->assertNotFalse($job_id, 'Job should be queued successfully');
        
        // Step 3: Process queue (simulates background processing)
        $result = $this->queue_processor->process_queue(1, 60);
        
        $this->assertEquals(1, $result['processed'], 'One job should be processed');
        $this->assertEquals(1, $result['success'], 'Job should complete successfully');
        $this->assertEquals(0, $result['errors'], 'No errors should occur');
        
        // Step 4: Verify mapping was created
        $mapping = $this->entity_mapping->get_mapping_by_perfex_id(
            EntityMappingService::ENTITY_CUSTOMER,
            $perfex_client_id
        );
        
        $this->assertNotNull($mapping, 'Entity mapping should be created');
        $this->assertEquals(EntityMappingService::STATUS_SYNCED, $mapping->sync_status);
        $this->assertEquals(EntityMappingService::DIRECTION_PERFEX_TO_MOLONI, $mapping->sync_direction);
        
        return [
            'perfex_client_id' => $perfex_client_id,
            'moloni_customer_id' => $mapping->moloni_id,
            'mapping_id' => $mapping->id
        ];
    }
    
    public function testCompleteInvoiceWorkflowWithDependencies()
    {
        // Test complete invoice sync with customer dependency
        
        // Step 1: Ensure customer exists and is synced
        $customer_data = $this->testCompleteCustomerSyncWorkflow();
        
        // Step 2: Create invoice in Perfex
        $perfex_invoice_data = [
            'clientid' => $customer_data['perfex_client_id'],
            'number' => 'INV-TEST-2024-001',
            'date' => date('Y-m-d'),
            'duedate' => date('Y-m-d', strtotime('+30 days')),
            'subtotal' => 100.00,
            'total_tax' => 23.00,
            'total' => 123.00,
            'status' => 1, // Draft
            'currency' => 1
        ];
        
        $perfex_invoice_id = 8888; // Simulated ID
        
        // Step 3: Queue invoice sync
        $job_id = $this->queue_processor->add_to_queue(
            EntityMappingService::ENTITY_INVOICE,
            $perfex_invoice_id,
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_HIGH,
            ['trigger' => 'invoice_added']
        );
        
        $this->assertNotFalse($job_id, 'Invoice job should be queued');
        
        // Step 4: Process queue
        $result = $this->queue_processor->process_queue(1, 60);
        
        $this->assertEquals(1, $result['processed'], 'Invoice job should be processed');
        
        // Step 5: Verify invoice mapping
        $invoice_mapping = $this->entity_mapping->get_mapping_by_perfex_id(
            EntityMappingService::ENTITY_INVOICE,
            $perfex_invoice_id
        );
        
        $this->assertNotNull($invoice_mapping, 'Invoice mapping should be created');
        $this->assertEquals(EntityMappingService::STATUS_SYNCED, $invoice_mapping->sync_status);
        
        return [
            'perfex_invoice_id' => $perfex_invoice_id,
            'moloni_invoice_id' => $invoice_mapping->moloni_id,
            'customer_data' => $customer_data
        ];
    }
    
    public function testBidirectionalSyncConflictResolution()
    {
        // Test conflict detection and resolution in bidirectional sync
        
        // Step 1: Create initial sync
        $customer_data = $this->testCompleteCustomerSyncWorkflow();
        
        // Step 2: Simulate concurrent modifications
        // Update in Perfex
        $perfex_update_data = [
            'company' => 'Updated Company Name - Perfex',
            'email' => 'updated.perfex@test.com'
        ];
        
        // Update in Moloni (simulated)
        $moloni_update_data = [
            'name' => 'Updated Company Name - Moloni',
            'email' => 'updated.moloni@test.com'
        ];
        
        // Step 3: Trigger bidirectional sync
        $job_id = $this->queue_processor->add_to_queue(
            EntityMappingService::ENTITY_CUSTOMER,
            $customer_data['perfex_client_id'],
            'update',
            'bidirectional',
            QueueProcessor::PRIORITY_NORMAL,
            ['trigger' => 'conflict_test']
        );
        
        // Step 4: Process should detect conflict
        $result = $this->queue_processor->process_queue(1, 60);
        
        // Step 5: Verify conflict handling
        $mapping = $this->entity_mapping->get_mapping_by_perfex_id(
            EntityMappingService::ENTITY_CUSTOMER,
            $customer_data['perfex_client_id']
        );
        
        // Depending on conflict resolution strategy, mapping should be marked as conflict
        // or resolved according to the configured strategy
        $this->assertNotNull($mapping);
        
        // If manual resolution is configured, status should be CONFLICT
        if (get_option('desk_moloni_conflict_strategy', 'manual') === 'manual') {
            $this->assertEquals(EntityMappingService::STATUS_CONFLICT, $mapping->sync_status);
        }
    }
    
    public function testQueuePriorityAndOrdering()
    {
        // Test queue priority system and job ordering
        
        $jobs = [];
        
        // Add jobs with different priorities
        $jobs['low'] = $this->queue_processor->add_to_queue(
            EntityMappingService::ENTITY_PRODUCT,
            1001,
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_LOW,
            ['test' => 'low_priority']
        );
        
        $jobs['critical'] = $this->queue_processor->add_to_queue(
            EntityMappingService::ENTITY_INVOICE,
            1002,
            'update',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_CRITICAL,
            ['test' => 'critical_priority']
        );
        
        $jobs['normal'] = $this->queue_processor->add_to_queue(
            EntityMappingService::ENTITY_CUSTOMER,
            1003,
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL,
            ['test' => 'normal_priority']
        );
        
        $jobs['high'] = $this->queue_processor->add_to_queue(
            EntityMappingService::ENTITY_INVOICE,
            1004,
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_HIGH,
            ['test' => 'high_priority']
        );
        
        // All jobs should be added successfully
        foreach ($jobs as $priority => $job_id) {
            $this->assertNotFalse($job_id, "Job with {$priority} priority should be queued");
        }
        
        // Process all jobs
        $result = $this->queue_processor->process_queue(4, 120);
        
        $this->assertEquals(4, $result['processed'], 'All 4 jobs should be processed');
        
        // Verify that high priority jobs were processed first
        // This would require additional tracking in the actual implementation
        $this->assertGreaterThan(0, $result['success']);
    }
    
    public function testRetryMechanismWithExponentialBackoff()
    {
        // Test retry mechanism for failed jobs
        
        // Create a job that will fail initially
        $job_id = $this->queue_processor->add_to_queue(
            EntityMappingService::ENTITY_CUSTOMER,
            9998, // Non-existent customer to trigger failure
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL,
            ['test' => 'retry_mechanism']
        );
        
        $this->assertNotFalse($job_id);
        
        // First processing attempt should fail and schedule retry
        $result = $this->queue_processor->process_queue(1, 30);
        
        $this->assertEquals(1, $result['processed']);
        $this->assertEquals(0, $result['success']);
        $this->assertEquals(1, $result['errors']);
        
        // Check queue statistics to verify retry was scheduled
        $stats = $this->queue_processor->get_queue_statistics();
        $this->assertGreaterThan(0, $stats['delayed']); // Job should be in delay queue
        
        // Simulate time passing and process delayed jobs
        // In real scenario, this would be handled by cron job
        $delayed_result = $this->queue_processor->process_queue(1, 30);
        
        // Job should be attempted again
        $this->assertGreaterThanOrEqual(0, $delayed_result['processed']);
    }
    
    public function testBulkSynchronizationPerformance()
    {
        // Test bulk synchronization performance
        
        $start_time = microtime(true);
        $job_count = 10;
        $jobs = [];
        
        // Queue multiple jobs
        for ($i = 1; $i <= $job_count; $i++) {
            $jobs[] = $this->queue_processor->add_to_queue(
                EntityMappingService::ENTITY_CUSTOMER,
                7000 + $i,
                'create',
                'perfex_to_moloni',
                QueueProcessor::PRIORITY_NORMAL,
                ['test' => 'bulk_sync', 'batch_id' => $i]
            );
        }
        
        $queue_time = microtime(true) - $start_time;
        
        // All jobs should be queued successfully
        $this->assertCount($job_count, array_filter($jobs));
        
        // Process all jobs in batch
        $process_start = microtime(true);
        $result = $this->queue_processor->process_queue($job_count, 300);
        $process_time = microtime(true) - $process_start;
        
        // Performance assertions
        $this->assertEquals($job_count, $result['processed']);
        $this->assertLessThan(5.0, $queue_time, 'Queuing should be fast');
        $this->assertLessThan(30.0, $process_time, 'Processing should complete within reasonable time');
        
        // Memory usage should be reasonable
        $stats = $this->queue_processor->get_queue_statistics();
        $memory_mb = $stats['memory_usage'] / (1024 * 1024);
        $this->assertLessThan(100, $memory_mb, 'Memory usage should be under 100MB');
    }
    
    public function testErrorHandlingAndLogging()
    {
        // Test comprehensive error handling and logging
        
        // Create job with invalid data to trigger errors
        $job_id = $this->queue_processor->add_to_queue(
            'invalid_entity_type', // Invalid entity type
            1234,
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL,
            ['test' => 'error_handling']
        );
        
        // Job should not be created due to validation
        $this->assertFalse($job_id, 'Invalid job should not be queued');
        
        // Create valid job but with non-existent entity
        $valid_job_id = $this->queue_processor->add_to_queue(
            EntityMappingService::ENTITY_CUSTOMER,
            99999, // Non-existent customer
            'create',
            'perfex_to_moloni',
            QueueProcessor::PRIORITY_NORMAL,
            ['test' => 'error_handling']
        );
        
        $this->assertNotFalse($valid_job_id, 'Valid job structure should be queued');
        
        // Process should handle error gracefully
        $result = $this->queue_processor->process_queue(1, 30);
        
        $this->assertEquals(1, $result['processed']);
        $this->assertEquals(0, $result['success']);
        $this->assertEquals(1, $result['errors']);
        
        // Error should be logged and job should be retried or moved to dead letter
        $stats = $this->queue_processor->get_queue_statistics();
        $this->assertGreaterThanOrEqual(0, $stats['delayed'] + $stats['dead_letter']);
    }
    
    public function testWebhookIntegration()
    {
        // Test webhook integration from Moloni
        
        $webhook_data = [
            'entity_type' => EntityMappingService::ENTITY_CUSTOMER,
            'entity_id' => 5555, // Moloni customer ID
            'action' => 'update',
            'event_type' => 'customer.updated',
            'timestamp' => time(),
            'data' => [
                'customer_id' => 5555,
                'name' => 'Updated via Webhook',
                'email' => 'webhook@test.com'
            ]
        ];
        
        // Trigger webhook handler
        $this->perfex_hooks->handle_moloni_webhook($webhook_data);
        
        // Verify job was queued
        $stats = $this->queue_processor->get_queue_statistics();
        $initial_queued = $stats['pending_main'] + $stats['pending_priority'];
        
        $this->assertGreaterThan(0, $initial_queued, 'Webhook should queue a job');
        
        // Process the webhook job
        $result = $this->queue_processor->process_queue(1, 30);
        
        $this->assertGreaterThanOrEqual(1, $result['processed']);
    }
    
    public function testQueueHealthAndMonitoring()
    {
        // Test queue health monitoring
        
        // Get initial health status
        $health = $this->queue_processor->health_check();
        
        $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']);
        
        // Test queue statistics
        $stats = $this->queue_processor->get_queue_statistics();
        
        $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->assertArrayHasKey('memory_usage', $stats);
        
        // Success rate should be a valid percentage
        $this->assertGreaterThanOrEqual(0, $stats['success_rate']);
        $this->assertLessThanOrEqual(100, $stats['success_rate']);
    }
    
    private function cleanupTestData()
    {
        // Clean up any test data from previous runs
        // This would involve clearing test mappings, queue items, etc.
        
        if (ENVIRONMENT !== 'production') {
            // Only clear in non-production environments
            try {
                $this->queue_processor->clear_all_queues();
            } catch (Exception $e) {
                // Queue might not be initialized yet, that's okay
            }
        }
    }
    
    protected function tearDown(): void
    {
        // Clean up after tests
        $this->cleanupTestData();
        
        $this->client_sync = null;
        $this->product_sync = null;
        $this->invoice_sync = null;
        $this->estimate_sync = null;
        $this->queue_processor = null;
        $this->perfex_hooks = null;
        $this->entity_mapping = null;
    }
}