<?php

declare(strict_types=1);

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Queue\ValueObjects\JobMetadata;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueuePriority;
use App\Framework\Retry\Strategies\ExponentialBackoffStrategy;
use App\Framework\Retry\Strategies\FixedDelayStrategy;

describe('JobPayload Value Object', function () {

    beforeEach(function () {
        $this->simpleJob = new class () {
            public function handle(): string
            {
                return 'executed';
            }
        };

        $this->complexJob = new class () {
            public function __construct(
                public string $id = 'test-123',
                public array $data = ['key' => 'value']
            ) {
            }

            public function process(): array
            {
                return $this->data;
            }
        };
    });

    describe('Basic Construction', function () {
        it('can be created with minimal parameters', function () {
            $payload = JobPayload::create($this->simpleJob);

            expect($payload->job)->toBe($this->simpleJob);
            expect($payload->priority->isNormal())->toBeTrue();
            expect($payload->delay->toSeconds())->toBe(0);
            expect($payload->timeout)->toBeNull();
            expect($payload->retryStrategy)->toBeNull();
            expect($payload->metadata)->not->toBeNull();
        });

        it('accepts all configuration parameters', function () {
            $priority = QueuePriority::high();
            $delay = Duration::fromMinutes(5);
            $timeout = Duration::fromSeconds(30);
            $retryStrategy = new ExponentialBackoffStrategy(maxAttempts: 3);
            $metadata = JobMetadata::create(['user_id' => 123]);

            $payload = JobPayload::create(
                $this->complexJob,
                $priority,
                $delay,
                $timeout,
                $retryStrategy,
                $metadata
            );

            expect($payload->job)->toBe($this->complexJob);
            expect($payload->priority)->toBe($priority);
            expect($payload->delay)->toBe($delay);
            expect($payload->timeout)->toBe($timeout);
            expect($payload->retryStrategy)->toBe($retryStrategy);
            expect($payload->metadata)->toBe($metadata);
        });

        it('is immutable - readonly properties cannot be changed', function () {
            $payload = JobPayload::create($this->simpleJob);

            // This would cause a PHP error if attempted:
            // $payload->job = new stdClass(); // Fatal error: Cannot modify readonly property
            // $payload->priority = QueuePriority::high(); // Fatal error: Cannot modify readonly property

            // Test that the properties are indeed readonly
            $reflection = new ReflectionClass($payload);
            foreach (['job', 'priority', 'delay', 'timeout', 'retryStrategy', 'metadata'] as $prop) {
                $property = $reflection->getProperty($prop);
                expect($property->isReadOnly())->toBeTrue("Property {$prop} should be readonly");
            }
        });
    });

    describe('Factory Methods', function () {
        it('creates immediate jobs with high priority and no delay', function () {
            $payload = JobPayload::immediate($this->simpleJob);

            expect($payload->priority->isHigh())->toBeTrue();
            expect($payload->delay->toSeconds())->toBe(0);
            expect($payload->isReady())->toBeTrue();
        });

        it('creates delayed jobs with specified delay', function () {
            $delay = Duration::fromMinutes(15);
            $payload = JobPayload::delayed($this->simpleJob, $delay);

            expect($payload->delay)->toBe($delay);
            expect($payload->isDelayed())->toBeTrue();
            expect($payload->isReady())->toBeFalse();
        });

        it('creates critical jobs with critical priority and short timeout', function () {
            $payload = JobPayload::critical($this->simpleJob);

            expect($payload->priority->isCritical())->toBeTrue();
            expect($payload->delay->toSeconds())->toBe(0);
            expect($payload->timeout->toSeconds())->toBe(30);
            expect($payload->hasTimeout())->toBeTrue();
        });

        it('creates background jobs with low priority and retry strategy', function () {
            $payload = JobPayload::background($this->simpleJob);

            expect($payload->priority->isLow())->toBeTrue();
            expect($payload->timeout->toMinutes())->toBe(30);
            expect($payload->hasRetryStrategy())->toBeTrue();
            expect($payload->retryStrategy->getMaxAttempts())->toBe(5);
        });
    });

    describe('Immutable Transformations', function () {
        beforeEach(function () {
            $this->originalPayload = JobPayload::create(
                $this->simpleJob,
                QueuePriority::normal(),
                Duration::zero()
            );
        });

        it('withPriority() creates new instance with different priority', function () {
            $newPriority = QueuePriority::high();
            $newPayload = $this->originalPayload->withPriority($newPriority);

            expect($newPayload)->not->toBe($this->originalPayload);
            expect($newPayload->priority)->toBe($newPriority);
            expect($newPayload->job)->toBe($this->originalPayload->job);
            expect($newPayload->delay)->toBe($this->originalPayload->delay);

            // Original should be unchanged
            expect($this->originalPayload->priority->isNormal())->toBeTrue();
        });

        it('withDelay() creates new instance with different delay', function () {
            $newDelay = Duration::fromMinutes(10);
            $newPayload = $this->originalPayload->withDelay($newDelay);

            expect($newPayload)->not->toBe($this->originalPayload);
            expect($newPayload->delay)->toBe($newDelay);
            expect($newPayload->job)->toBe($this->originalPayload->job);
            expect($newPayload->priority)->toBe($this->originalPayload->priority);

            // Original should be unchanged
            expect($this->originalPayload->delay->toSeconds())->toBe(0);
        });

        it('withTimeout() creates new instance with timeout', function () {
            $timeout = Duration::fromSeconds(45);
            $newPayload = $this->originalPayload->withTimeout($timeout);

            expect($newPayload)->not->toBe($this->originalPayload);
            expect($newPayload->timeout)->toBe($timeout);
            expect($newPayload->hasTimeout())->toBeTrue();

            // Original should be unchanged
            expect($this->originalPayload->timeout)->toBeNull();
        });

        it('withRetryStrategy() creates new instance with retry strategy', function () {
            $retryStrategy = new FixedDelayStrategy(Duration::fromSeconds(30), 3);
            $newPayload = $this->originalPayload->withRetryStrategy($retryStrategy);

            expect($newPayload)->not->toBe($this->originalPayload);
            expect($newPayload->retryStrategy)->toBe($retryStrategy);
            expect($newPayload->hasRetryStrategy())->toBeTrue();

            // Original should be unchanged
            expect($this->originalPayload->retryStrategy)->toBeNull();
        });

        it('withMetadata() creates new instance with metadata', function () {
            $metadata = JobMetadata::create(['source' => 'test', 'version' => '1.0']);
            $newPayload = $this->originalPayload->withMetadata($metadata);

            expect($newPayload)->not->toBe($this->originalPayload);
            expect($newPayload->metadata)->toBe($metadata);

            // Original should be unchanged
            expect($this->originalPayload->metadata)->not->toBe($metadata);
        });

        it('can chain multiple transformations', function () {
            $finalPayload = $this->originalPayload
                ->withPriority(QueuePriority::critical())
                ->withDelay(Duration::fromSeconds(30))
                ->withTimeout(Duration::fromMinutes(5))
                ->withRetryStrategy(new ExponentialBackoffStrategy(maxAttempts: 3));

            expect($finalPayload->priority->isCritical())->toBeTrue();
            expect($finalPayload->delay->toSeconds())->toBe(30);
            expect($finalPayload->timeout->toMinutes())->toBe(5);
            expect($finalPayload->hasRetryStrategy())->toBeTrue();

            // Original should be completely unchanged
            expect($this->originalPayload->priority->isNormal())->toBeTrue();
            expect($this->originalPayload->delay->toSeconds())->toBe(0);
            expect($this->originalPayload->timeout)->toBeNull();
            expect($this->originalPayload->retryStrategy)->toBeNull();
        });
    });

    describe('Status Checking Methods', function () {
        it('isReady() returns true for jobs with no delay', function () {
            $payload = JobPayload::create($this->simpleJob, delay: Duration::zero());
            expect($payload->isReady())->toBeTrue();

            $immediate = JobPayload::immediate($this->simpleJob);
            expect($immediate->isReady())->toBeTrue();
        });

        it('isReady() returns false for delayed jobs', function () {
            $payload = JobPayload::delayed($this->simpleJob, Duration::fromSeconds(30));
            expect($payload->isReady())->toBeFalse();
        });

        it('isDelayed() returns true for jobs with delay', function () {
            $payload = JobPayload::delayed($this->simpleJob, Duration::fromMinutes(1));
            expect($payload->isDelayed())->toBeTrue();
        });

        it('isDelayed() returns false for immediate jobs', function () {
            $payload = JobPayload::immediate($this->simpleJob);
            expect($payload->isDelayed())->toBeFalse();
        });

        it('hasRetryStrategy() reflects retry strategy presence', function () {
            $withoutRetry = JobPayload::create($this->simpleJob);
            expect($withoutRetry->hasRetryStrategy())->toBeFalse();

            $withRetry = JobPayload::background($this->simpleJob);
            expect($withRetry->hasRetryStrategy())->toBeTrue();
        });

        it('hasTimeout() reflects timeout presence', function () {
            $withoutTimeout = JobPayload::create($this->simpleJob);
            expect($withoutTimeout->hasTimeout())->toBeFalse();

            $withTimeout = JobPayload::critical($this->simpleJob);
            expect($withTimeout->hasTimeout())->toBeTrue();
        });
    });

    describe('Time Calculations', function () {
        it('getAvailableTime() returns current time for immediate jobs', function () {
            $payload = JobPayload::immediate($this->simpleJob);
            $available = $payload->getAvailableTime();
            $now = time();

            expect($available)->toBeGreaterThanOrEqual($now - 1);
            expect($available)->toBeLessThanOrEqual($now + 1);
        });

        it('getAvailableTime() returns future time for delayed jobs', function () {
            $delay = Duration::fromSeconds(300); // 5 minutes
            $payload = JobPayload::delayed($this->simpleJob, $delay);

            $available = $payload->getAvailableTime();
            $expected = time() + 300;

            expect($available)->toBeGreaterThanOrEqual($expected - 1);
            expect($available)->toBeLessThanOrEqual($expected + 1);
        });
    });

    describe('Serialization and Array Conversion', function () {
        it('can serialize job objects', function () {
            $payload = JobPayload::create($this->complexJob);
            $serialized = $payload->serialize();

            expect($serialized)->toBeString();
            expect(strlen($serialized))->toBeGreaterThan(0);

            // Should be able to unserialize
            $unserialized = unserialize($serialized);
            expect($unserialized)->toBeInstanceOf(get_class($this->complexJob));
            expect($unserialized->id)->toBe('test-123');
        });

        it('getJobClass() returns correct class name', function () {
            $payload = JobPayload::create($this->complexJob);
            $className = $payload->getJobClass();

            expect($className)->toBeString();
            expect($className)->toContain('class@anonymous');
        });

        it('toArray() provides comprehensive job information', function () {
            $retryStrategy = new ExponentialBackoffStrategy(maxAttempts: 5);
            $payload = JobPayload::create(
                $this->complexJob,
                QueuePriority::high(),
                Duration::fromSeconds(120),
                Duration::fromMinutes(10),
                $retryStrategy,
                JobMetadata::create(['source' => 'api'])
            );

            $array = $payload->toArray();

            expect($array)->toHaveKey('job_class');
            expect($array)->toHaveKey('priority');
            expect($array)->toHaveKey('priority_value');
            expect($array)->toHaveKey('delay_seconds');
            expect($array)->toHaveKey('timeout_seconds');
            expect($array)->toHaveKey('has_retry_strategy');
            expect($array)->toHaveKey('max_attempts');
            expect($array)->toHaveKey('available_at');
            expect($array)->toHaveKey('metadata');

            expect($array['priority'])->toBe('high');
            expect($array['priority_value'])->toBe(100);
            expect($array['delay_seconds'])->toBe(120);
            expect($array['timeout_seconds'])->toBe(600);
            expect($array['has_retry_strategy'])->toBeTrue();
            expect($array['max_attempts'])->toBe(5);
            expect($array['available_at'])->toBeInt();
            expect($array['metadata'])->toBeArray();
        });

        it('toArray() handles null values correctly', function () {
            $payload = JobPayload::create($this->simpleJob);
            $array = $payload->toArray();

            expect($array['timeout_seconds'])->toBeNull();
            expect($array['has_retry_strategy'])->toBeFalse();
            expect($array['max_attempts'])->toBeNull();
        });
    });

    describe('Edge Cases and Error Handling', function () {
        it('maintains object reference integrity', function () {
            $job = new stdClass();
            $job->property = 'value';

            $payload = JobPayload::create($job);

            // Same object reference should be maintained
            expect($payload->job)->toBe($job);
            expect($payload->job->property)->toBe('value');

            // Modifying original object should affect payload job (reference)
            $job->property = 'modified';
            expect($payload->job->property)->toBe('modified');
        });

        it('handles complex job objects with dependencies', function () {
            $complexJob = new class () {
                public array $config;

                public \DateTime $created;

                public function __construct()
                {
                    $this->config = ['timeout' => 30, 'retries' => 3];
                    $this->created = new \DateTime();
                }

                public function getData(): array
                {
                    return [
                        'config' => $this->config,
                        'created' => $this->created->format('Y-m-d H:i:s'),
                    ];
                }
            };

            $payload = JobPayload::create($complexJob);
            expect($payload->job->getData())->toBeArray();
            expect($payload->job->config['timeout'])->toBe(30);
        });

        it('preserves metadata across transformations', function () {
            $originalMetadata = JobMetadata::create(['initial' => 'data']);
            $payload = JobPayload::create($this->simpleJob, metadata: $originalMetadata);

            // Transform without changing metadata
            $newPayload = $payload->withPriority(QueuePriority::critical());

            expect($newPayload->metadata)->toBe($originalMetadata);
        });
    });
});

describe('JobPayload Integration Scenarios', function () {

    beforeEach(function () {
        $this->emailJob = new class () {
            public function __construct(
                public string $to = 'test@example.com',
                public string $subject = 'Test Email',
                public string $body = 'Hello World'
            ) {
            }

            public function send(): bool
            {
                // Simulate email sending
                return true;
            }
        };

        $this->reportJob = new class () {
            public function __construct(
                public array $criteria = ['period' => 'monthly'],
                public string $format = 'pdf'
            ) {
            }

            public function generate(): string
            {
                return "Report generated with format: {$this->format}";
            }
        };
    });

    it('handles email job scenarios', function () {
        // Immediate notification
        $urgent = JobPayload::immediate($this->emailJob);
        expect($urgent->priority->isHigh())->toBeTrue();
        expect($urgent->isReady())->toBeTrue();

        // Delayed newsletter
        $newsletter = JobPayload::delayed($this->emailJob, Duration::fromHours(2));
        expect($newsletter->isDelayed())->toBeTrue();

        // Critical alert with timeout
        $alert = JobPayload::critical($this->emailJob);
        expect($alert->priority->isCritical())->toBeTrue();
        expect($alert->hasTimeout())->toBeTrue();
    });

    it('handles report generation scenarios', function () {
        // Background monthly report
        $monthlyReport = JobPayload::background($this->reportJob);
        expect($monthlyReport->priority->isLow())->toBeTrue();
        expect($monthlyReport->hasRetryStrategy())->toBeTrue();

        // Add custom metadata for tracking
        $metadata = JobMetadata::create([
            'user_id' => 123,
            'report_type' => 'financial',
            'department' => 'accounting',
        ]);

        $customReport = $monthlyReport->withMetadata($metadata);
        expect($customReport->metadata->get('user_id'))->toBe(123);
        expect($customReport->metadata->get('report_type'))->toBe('financial');
    });

    it('handles job priority escalation scenarios', function () {
        // Start as normal priority
        $job = JobPayload::create($this->emailJob, QueuePriority::normal());
        expect($job->priority->isNormal())->toBeTrue();

        // Escalate to high priority (customer complaint)
        $escalated = $job->withPriority(QueuePriority::high());
        expect($escalated->priority->isHigh())->toBeTrue();

        // Further escalate to critical (system outage notification)
        $critical = $escalated->withPriority(QueuePriority::critical());
        expect($critical->priority->isCritical())->toBeTrue();

        // Original should remain unchanged
        expect($job->priority->isNormal())->toBeTrue();
    });

    it('demonstrates retry strategy configuration', function () {
        // Simple retry for transient failures
        $simpleRetry = new FixedDelayStrategy(Duration::fromSeconds(30), 3);
        $payload = JobPayload::create($this->emailJob)->withRetryStrategy($simpleRetry);

        expect($payload->retryStrategy->getMaxAttempts())->toBe(3);

        // Exponential backoff for rate limiting
        $exponentialRetry = new ExponentialBackoffStrategy(maxAttempts: 5);
        $rateLimitedPayload = $payload->withRetryStrategy($exponentialRetry);

        expect($rateLimitedPayload->retryStrategy->getMaxAttempts())->toBe(5);
        expect($rateLimitedPayload->retryStrategy)->toBeInstanceOf(ExponentialBackoffStrategy::class);
    });
});
