<?php

declare(strict_types=1);

use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Ulid\Ulid;

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

    describe('Creation and Validation', function () {
        it('can generate unique JobIds', function () {
            $id1 = JobId::generate();
            $id2 = JobId::generate();

            expect($id1)->toBeInstanceOf(JobId::class);
            expect($id2)->toBeInstanceOf(JobId::class);
            expect($id1->toString())->not->toBe($id2->toString());
        });

        it('can create from string', function () {
            $idString = 'job_test_123';
            $jobId = JobId::fromString($idString);

            expect($jobId->toString())->toBe($idString);
            expect($jobId->getValue())->toBe($idString);
        });

        it('can create from ULID object', function () {
            $ulid = Ulid::generate();
            $jobId = JobId::fromUlid($ulid);

            expect($jobId->toString())->toBe($ulid->toString());
            expect($jobId->toUlid()->toString())->toBe($ulid->toString());
        });

        it('rejects empty JobId', function () {
            expect(fn () => JobId::fromString(''))
                ->toThrow(\InvalidArgumentException::class, 'JobId cannot be empty');
        });

        it('validates JobId format correctly', function () {
            // Valid formats should work
            expect(fn () => JobId::fromString('job_12345'))->not->toThrow();
            expect(fn () => JobId::fromString('01FXYZ0123456789ABCDEF1234'))->not->toThrow(); // ULID format
            expect(fn () => JobId::fromString('simple-id'))->not->toThrow();

            // Any non-empty string is currently accepted
            expect(fn () => JobId::fromString('a'))->not->toThrow();
            expect(fn () => JobId::fromString('very-long-job-identifier-12345'))->not->toThrow();
        });

        it('is readonly and immutable', function () {
            $jobId = JobId::fromString('test-job-123');

            // Verify the class is readonly
            $reflection = new ReflectionClass($jobId);
            expect($reflection->isReadOnly())->toBeTrue();

            // The value property should be readonly
            $valueProperty = $reflection->getProperty('value');
            expect($valueProperty->isReadOnly())->toBeTrue();
        });
    });

    describe('String Representation', function () {
        it('toString() returns the internal value', function () {
            $value = 'test-job-456';
            $jobId = JobId::fromString($value);

            expect($jobId->toString())->toBe($value);
        });

        it('getValue() is alias for toString()', function () {
            $value = 'another-test-job';
            $jobId = JobId::fromString($value);

            expect($jobId->getValue())->toBe($jobId->toString());
            expect($jobId->getValue())->toBe($value);
        });

        it('__toString() magic method works', function () {
            $value = 'magic-method-test';
            $jobId = JobId::fromString($value);

            expect((string) $jobId)->toBe($value);
            expect("Job ID: {$jobId}")->toBe("Job ID: {$value}");
        });

        it('jsonSerialize() returns string value', function () {
            $value = 'json-test-job';
            $jobId = JobId::fromString($value);

            expect($jobId->jsonSerialize())->toBe($value);
            expect(json_encode($jobId))->toBe('"' . $value . '"');
        });
    });

    describe('Equality and Comparison', function () {
        it('equals() compares JobIds correctly', function () {
            $id1 = JobId::fromString('same-id');
            $id2 = JobId::fromString('same-id');
            $id3 = JobId::fromString('different-id');

            expect($id1->equals($id2))->toBeTrue();
            expect($id1->equals($id3))->toBeFalse();
            expect($id2->equals($id3))->toBeFalse();
        });

        it('isBefore() and isAfter() compare string values', function () {
            $idA = JobId::fromString('aaa');
            $idB = JobId::fromString('bbb');
            $idC = JobId::fromString('ccc');

            expect($idA->isBefore($idB))->toBeTrue();
            expect($idB->isBefore($idC))->toBeTrue();
            expect($idA->isBefore($idC))->toBeTrue();

            expect($idC->isAfter($idB))->toBeTrue();
            expect($idB->isAfter($idA))->toBeTrue();
            expect($idC->isAfter($idA))->toBeTrue();

            expect($idB->isBefore($idA))->toBeFalse();
            expect($idA->isAfter($idB))->toBeFalse();
        });

        it('comparison works with numeric-like strings', function () {
            $id1 = JobId::fromString('job_001');
            $id2 = JobId::fromString('job_002');
            $id10 = JobId::fromString('job_010');

            expect($id1->isBefore($id2))->toBeTrue();
            expect($id2->isBefore($id10))->toBeTrue();
            expect($id10->isAfter($id1))->toBeTrue();
        });
    });

    describe('ULID Integration', function () {
        it('can convert to ULID when valid format', function () {
            $originalUlid = Ulid::generate();
            $jobId = JobId::fromUlid($originalUlid);
            $convertedUlid = $jobId->toUlid();

            expect($convertedUlid->toString())->toBe($originalUlid->toString());
        });

        it('getTimestamp() extracts timestamp from ULID', function () {
            $ulid = Ulid::generate();
            $jobId = JobId::fromUlid($ulid);

            $timestamp = $jobId->getTimestamp();
            expect($timestamp)->toBeInstanceOf(\DateTimeImmutable::class);

            // Should be very recent
            $now = new \DateTimeImmutable();
            $diff = $now->getTimestamp() - $timestamp->getTimestamp();
            expect($diff)->toBeLessThan(5); // Within 5 seconds
        });

        it('generateForQueue() creates ULID-based JobId', function () {
            $jobId = JobId::generateForQueue('email-queue');

            expect($jobId)->toBeInstanceOf(JobId::class);
            expect(strlen($jobId->toString()))->toBe(26); // ULID length
        });

        it('getTimePrefix() extracts time portion', function () {
            $jobId = JobId::generateForQueue('test-queue');
            $timePrefix = $jobId->getTimePrefix();

            expect($timePrefix)->toBeString();
            expect(strlen($timePrefix))->toBe(10);
        });

        it('getRandomSuffix() extracts random portion', function () {
            $jobId = JobId::generateForQueue('test-queue');
            $randomSuffix = $jobId->getRandomSuffix();

            expect($randomSuffix)->toBeString();
            expect(strlen($randomSuffix))->toBe(16);
        });
    });

    describe('Edge Cases and Error Handling', function () {
        it('handles very long job IDs', function () {
            $longId = str_repeat('a', 1000);
            $jobId = JobId::fromString($longId);

            expect($jobId->toString())->toBe($longId);
            expect(strlen($jobId->toString()))->toBe(1000);
        });

        it('handles special characters in job IDs', function () {
            $specialId = 'job-with_special.chars@123!';
            $jobId = JobId::fromString($specialId);

            expect($jobId->toString())->toBe($specialId);
        });

        it('handles unicode characters', function () {
            $unicodeId = 'job-测试-🚀-123';
            $jobId = JobId::fromString($unicodeId);

            expect($jobId->toString())->toBe($unicodeId);
        });

        it('toUlid() may fail for non-ULID format strings', function () {
            $nonUlidJobId = JobId::fromString('not-a-ulid-format');

            // This should throw an exception since it's not a valid ULID
            expect(fn () => $nonUlidJobId->toUlid())
                ->toThrow();
        });

        it('getTimestamp() may fail for non-ULID format', function () {
            $nonUlidJobId = JobId::fromString('simple-job-id');

            // This should throw an exception since it's not a valid ULID
            expect(fn () => $nonUlidJobId->getTimestamp())
                ->toThrow();
        });
    });

    describe('Performance and Uniqueness', function () {
        it('generates unique IDs in rapid succession', function () {
            $ids = [];
            $count = 1000;

            $start = microtime(true);

            for ($i = 0; $i < $count; $i++) {
                $id = JobId::generate();
                $ids[] = $id->toString();
            }

            $elapsed = microtime(true) - $start;

            // All IDs should be unique
            $unique = array_unique($ids);
            expect(count($unique))->toBe($count);

            // Should generate quickly
            expect($elapsed)->toBeLessThan(1.0); // Within 1 second
        });

        it('ULID-based IDs are time-ordered', function () {
            $ids = [];

            // Generate several IDs with small delays
            for ($i = 0; $i < 5; $i++) {
                $ids[] = JobId::generateForQueue('test');
                if ($i < 4) {
                    usleep(1000); // 1ms delay
                }
            }

            // Each ID should be "after" the previous one (time-ordered)
            for ($i = 1; $i < count($ids); $i++) {
                expect($ids[$i]->isAfter($ids[$i - 1]))->toBeTrue();
            }
        });

        it('maintains consistent string representation', function () {
            $jobId = JobId::fromString('consistent-test');

            // Multiple calls should return the same result
            expect($jobId->toString())->toBe($jobId->toString());
            expect($jobId->getValue())->toBe($jobId->toString());
            expect((string) $jobId)->toBe($jobId->toString());
            expect($jobId->jsonSerialize())->toBe($jobId->toString());
        });
    });
});

describe('JobId in Queue Context', function () {

    it('can be used as array keys', function () {
        $id1 = JobId::fromString('job-1');
        $id2 = JobId::fromString('job-2');

        $jobs = [];
        $jobs[$id1->toString()] = 'First Job';
        $jobs[$id2->toString()] = 'Second Job';

        expect($jobs[$id1->toString()])->toBe('First Job');
        expect($jobs[$id2->toString()])->toBe('Second Job');
        expect(count($jobs))->toBe(2);
    });

    it('works with job tracking scenarios', function () {
        $processingJobs = [];
        $completedJobs = [];

        // Simulate job lifecycle
        $jobId = JobId::generate();

        // Job starts processing
        $processingJobs[$jobId->toString()] = time();
        expect(isset($processingJobs[$jobId->toString()]))->toBeTrue();

        // Job completes
        $completedJobs[$jobId->toString()] = [
            'started_at' => $processingJobs[$jobId->toString()],
            'completed_at' => time(),
            'status' => 'success',
        ];
        unset($processingJobs[$jobId->toString()]);

        expect(isset($processingJobs[$jobId->toString()]))->toBeFalse();
        expect(isset($completedJobs[$jobId->toString()]))->toBeTrue();
        expect($completedJobs[$jobId->toString()]['status'])->toBe('success');
    });

    it('demonstrates time-based job identification', function () {
        // Generate jobs for different queues
        $emailJobId = JobId::generateForQueue('email');
        $reportJobId = JobId::generateForQueue('reports');
        $backgroundJobId = JobId::generateForQueue('background');

        // All should be unique
        expect($emailJobId->toString())->not->toBe($reportJobId->toString());
        expect($reportJobId->toString())->not->toBe($backgroundJobId->toString());
        expect($backgroundJobId->toString())->not->toBe($emailJobId->toString());

        // All should have ULID format (26 characters)
        expect(strlen($emailJobId->toString()))->toBe(26);
        expect(strlen($reportJobId->toString()))->toBe(26);
        expect(strlen($backgroundJobId->toString()))->toBe(26);
    });

    it('supports job priority scenarios', function () {
        // Generate jobs with time component
        $urgentJob = JobId::generateForQueue('urgent');
        sleep(1); // Ensure different timestamp
        $normalJob = JobId::generateForQueue('normal');

        // Later job should have later timestamp
        expect($normalJob->isAfter($urgentJob))->toBeTrue();

        // Can use timestamps for ordering
        $urgentTime = $urgentJob->getTimestamp();
        $normalTime = $normalJob->getTimestamp();

        expect($normalTime > $urgentTime)->toBeTrue();
    });
});
