<?php

declare(strict_types=1);

use App\Framework\Queue\Exceptions\InvalidDeadLetterQueueNameException;
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueueName;

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

    describe('Construction and Validation', function () {
        it('can create valid dead letter queue names', function () {
            $validNames = [
                'failed',
                'email_failed',
                'background-jobs-failed',
                'user.registration.failed',
                'queue_123_failed',
                'a', // minimum length
                str_repeat('a', 100), // maximum length
            ];

            foreach ($validNames as $name) {
                $dlqName = DeadLetterQueueName::fromString($name);
                expect($dlqName->toString())->toBe($name);
            }
        });

        it('rejects invalid dead letter queue names', function () {
            $invalidNames = [
                '', // too short
                str_repeat('a', 101), // too long
                'queue with spaces',
                'queue@invalid',
                'queue#invalid',
                'queue$invalid',
                'queue%invalid',
                'queue&invalid',
                'queue*invalid',
                'queue(invalid)',
                'queue+invalid',
                'queue=invalid',
                'queue[invalid]',
                'queue{invalid}',
                'queue|invalid',
                'queue\\invalid',
                'queue:invalid',
                'queue;invalid',
                'queue"invalid',
                'queue\'invalid',
                'queue<invalid>',
                'queue,invalid',
                'queue?invalid',
                'queue/invalid',
                'queue~invalid',
                'queue`invalid',
            ];

            foreach ($invalidNames as $name) {
                expect(fn () => DeadLetterQueueName::fromString($name))
                    ->toThrow(InvalidDeadLetterQueueNameException::class);
            }
        });

        it('is readonly and immutable', function () {
            $dlqName = DeadLetterQueueName::fromString('test-failed');

            $reflection = new ReflectionClass($dlqName);
            expect($reflection->isReadOnly())->toBeTrue();

            $nameProperty = $reflection->getProperty('name');
            expect($nameProperty->isReadOnly())->toBeTrue();
        });
    });

    describe('Factory Methods', function () {
        it('creates default dead letter queue name', function () {
            $defaultDlq = DeadLetterQueueName::default();
            expect($defaultDlq->toString())->toBe('failed');
        });

        it('creates dead letter queue for specific queue', function () {
            $queueName = QueueName::fromString('email');
            $dlqName = DeadLetterQueueName::forQueue($queueName);

            expect($dlqName->toString())->toBe('email_failed');
        });

        it('creates dead letter queue for complex queue names', function () {
            $queueName = QueueName::fromString('user.registration');
            $dlqName = DeadLetterQueueName::forQueue($queueName);

            expect($dlqName->toString())->toBe('user.registration_failed');
        });
    });

    describe('Equality and Comparison', function () {
        it('equals() compares names correctly', function () {
            $dlq1 = DeadLetterQueueName::fromString('failed');
            $dlq2 = DeadLetterQueueName::fromString('failed');
            $dlq3 = DeadLetterQueueName::fromString('other_failed');

            expect($dlq1->equals($dlq2))->toBeTrue();
            expect($dlq1->equals($dlq3))->toBeFalse();
            expect($dlq2->equals($dlq3))->toBeFalse();
        });

        it('string representation works correctly', function () {
            $name = 'test-failed-queue';
            $dlqName = DeadLetterQueueName::fromString($name);

            expect($dlqName->toString())->toBe($name);
            expect((string) $dlqName)->toBe($name);
        });
    });

    describe('Edge Cases', function () {
        it('handles minimum and maximum length names', function () {
            $minName = DeadLetterQueueName::fromString('a');
            expect($minName->toString())->toBe('a');

            $maxName = DeadLetterQueueName::fromString(str_repeat('x', 100));
            expect($maxName->toString())->toBe(str_repeat('x', 100));
        });

        it('handles all valid characters', function () {
            $validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.';
            $dlqName = DeadLetterQueueName::fromString($validChars);

            expect($dlqName->toString())->toBe($validChars);
        });

        it('provides specific error messages for validation failures', function () {
            // Too short
            try {
                DeadLetterQueueName::fromString('');
                expect(false)->toBeTrue('Should have thrown exception');
            } catch (InvalidDeadLetterQueueNameException $e) {
                expect($e->getMessage())->toContain('too short');
            }

            // Too long
            try {
                DeadLetterQueueName::fromString(str_repeat('a', 101));
                expect(false)->toBeTrue('Should have thrown exception');
            } catch (InvalidDeadLetterQueueNameException $e) {
                expect($e->getMessage())->toContain('too long');
            }

            // Invalid format
            try {
                DeadLetterQueueName::fromString('invalid@name');
                expect(false)->toBeTrue('Should have thrown exception');
            } catch (InvalidDeadLetterQueueNameException $e) {
                expect($e->getMessage())->toContain('invalid format');
            }
        });
    });
});

describe('Dead Letter Queue Mock Implementation', function () {

    beforeEach(function () {
        // Create a mock dead letter queue for testing
        $this->mockDlq = new class () {
            private array $jobs = [];

            private array $stats = [];

            public function addFailedJob(array $jobData): void
            {
                $this->jobs[] = $jobData;
            }

            public function getJobs(DeadLetterQueueName $queueName, int $limit = 100): array
            {
                return array_slice(
                    array_filter($this->jobs, fn ($job) => $job['dlq_name'] === $queueName->toString()),
                    0,
                    $limit
                );
            }

            public function retryJob(string $jobId): bool
            {
                foreach ($this->jobs as $index => $job) {
                    if ($job['id'] === $jobId) {
                        unset($this->jobs[$index]);

                        return true;
                    }
                }

                return false;
            }

            public function deleteJob(string $jobId): bool
            {
                foreach ($this->jobs as $index => $job) {
                    if ($job['id'] === $jobId) {
                        unset($this->jobs[$index]);

                        return true;
                    }
                }

                return false;
            }

            public function clearQueue(DeadLetterQueueName $queueName): int
            {
                $initialCount = count($this->jobs);
                $this->jobs = array_filter(
                    $this->jobs,
                    fn ($job) => $job['dlq_name'] !== $queueName->toString()
                );

                return $initialCount - count($this->jobs);
            }

            public function getQueueStats(DeadLetterQueueName $queueName): array
            {
                $jobs = $this->getJobs($queueName);

                return [
                    'queue_name' => $queueName->toString(),
                    'total_jobs' => count($jobs),
                    'avg_failed_attempts' => count($jobs) > 0 ? array_sum(array_column($jobs, 'failed_attempts')) / count($jobs) : 0,
                    'max_failed_attempts' => count($jobs) > 0 ? max(array_column($jobs, 'failed_attempts')) : 0,
                    'avg_retry_count' => count($jobs) > 0 ? array_sum(array_column($jobs, 'retry_count')) / count($jobs) : 0,
                    'max_retry_count' => count($jobs) > 0 ? max(array_column($jobs, 'retry_count')) : 0,
                    'oldest_job' => count($jobs) > 0 ? min(array_column($jobs, 'failed_at')) : null,
                    'newest_job' => count($jobs) > 0 ? max(array_column($jobs, 'failed_at')) : null,
                ];
            }

            public function getAvailableQueues(): array
            {
                $queueNames = array_unique(array_column($this->jobs, 'dlq_name'));

                return array_map(fn ($name) => DeadLetterQueueName::fromString($name), $queueNames);
            }
        };
    });

    describe('Dead Letter Queue Operations', function () {
        it('can add failed jobs', function () {
            $dlqName = DeadLetterQueueName::fromString('email_failed');
            $jobId = JobId::generate();

            $jobData = [
                'id' => uniqid(),
                'original_job_id' => $jobId->toString(),
                'dlq_name' => $dlqName->toString(),
                'original_queue' => 'email',
                'job_payload' => serialize(['type' => 'email', 'data' => 'test']),
                'failure_reason' => 'Connection timeout',
                'exception_type' => 'TimeoutException',
                'stack_trace' => 'Stack trace here...',
                'failed_attempts' => 3,
                'failed_at' => date('Y-m-d H:i:s'),
                'retry_count' => 0,
            ];

            $this->mockDlq->addFailedJob($jobData);

            $jobs = $this->mockDlq->getJobs($dlqName);
            expect(count($jobs))->toBe(1);
            expect($jobs[0]['original_job_id'])->toBe($jobId->toString());
        });

        it('can retrieve jobs by queue name', function () {
            $emailDlq = DeadLetterQueueName::fromString('email_failed');
            $reportDlq = DeadLetterQueueName::fromString('report_failed');

            // Add jobs to different queues
            $this->mockDlq->addFailedJob([
                'id' => 'job1',
                'original_job_id' => 'original1',
                'dlq_name' => $emailDlq->toString(),
                'original_queue' => 'email',
                'job_payload' => 'payload1',
                'failure_reason' => 'Error 1',
                'exception_type' => 'Exception',
                'stack_trace' => 'trace1',
                'failed_attempts' => 1,
                'failed_at' => date('Y-m-d H:i:s'),
                'retry_count' => 0,
            ]);

            $this->mockDlq->addFailedJob([
                'id' => 'job2',
                'original_job_id' => 'original2',
                'dlq_name' => $reportDlq->toString(),
                'original_queue' => 'report',
                'job_payload' => 'payload2',
                'failure_reason' => 'Error 2',
                'exception_type' => 'Exception',
                'stack_trace' => 'trace2',
                'failed_attempts' => 2,
                'failed_at' => date('Y-m-d H:i:s'),
                'retry_count' => 0,
            ]);

            $emailJobs = $this->mockDlq->getJobs($emailDlq);
            $reportJobs = $this->mockDlq->getJobs($reportDlq);

            expect(count($emailJobs))->toBe(1);
            expect(count($reportJobs))->toBe(1);
            expect($emailJobs[0]['id'])->toBe('job1');
            expect($reportJobs[0]['id'])->toBe('job2');
        });

        it('can retry failed jobs', function () {
            $dlqName = DeadLetterQueueName::fromString('test_failed');
            $jobId = 'retry_test_job';

            $this->mockDlq->addFailedJob([
                'id' => $jobId,
                'original_job_id' => 'original_retry',
                'dlq_name' => $dlqName->toString(),
                'original_queue' => 'test',
                'job_payload' => 'retry_payload',
                'failure_reason' => 'Temporary error',
                'exception_type' => 'TemporaryException',
                'stack_trace' => 'retry_trace',
                'failed_attempts' => 1,
                'failed_at' => date('Y-m-d H:i:s'),
                'retry_count' => 0,
            ]);

            expect(count($this->mockDlq->getJobs($dlqName)))->toBe(1);

            $retryResult = $this->mockDlq->retryJob($jobId);
            expect($retryResult)->toBeTrue();

            // Job should be removed from DLQ after retry
            expect(count($this->mockDlq->getJobs($dlqName)))->toBe(0);
        });

        it('can delete failed jobs permanently', function () {
            $dlqName = DeadLetterQueueName::fromString('delete_test');
            $jobId = 'delete_test_job';

            $this->mockDlq->addFailedJob([
                'id' => $jobId,
                'original_job_id' => 'original_delete',
                'dlq_name' => $dlqName->toString(),
                'original_queue' => 'delete_test',
                'job_payload' => 'delete_payload',
                'failure_reason' => 'Permanent error',
                'exception_type' => 'PermanentException',
                'stack_trace' => 'delete_trace',
                'failed_attempts' => 5,
                'failed_at' => date('Y-m-d H:i:s'),
                'retry_count' => 0,
            ]);

            expect(count($this->mockDlq->getJobs($dlqName)))->toBe(1);

            $deleteResult = $this->mockDlq->deleteJob($jobId);
            expect($deleteResult)->toBeTrue();

            expect(count($this->mockDlq->getJobs($dlqName)))->toBe(0);
        });

        it('can clear entire queue', function () {
            $dlqName = DeadLetterQueueName::fromString('clear_test');

            // Add multiple jobs
            for ($i = 1; $i <= 5; $i++) {
                $this->mockDlq->addFailedJob([
                    'id' => "clear_job_{$i}",
                    'original_job_id' => "original_{$i}",
                    'dlq_name' => $dlqName->toString(),
                    'original_queue' => 'clear_test',
                    'job_payload' => "payload_{$i}",
                    'failure_reason' => "Error {$i}",
                    'exception_type' => 'Exception',
                    'stack_trace' => "trace_{$i}",
                    'failed_attempts' => $i,
                    'failed_at' => date('Y-m-d H:i:s'),
                    'retry_count' => 0,
                ]);
            }

            expect(count($this->mockDlq->getJobs($dlqName)))->toBe(5);

            $clearedCount = $this->mockDlq->clearQueue($dlqName);
            expect($clearedCount)->toBe(5);
            expect(count($this->mockDlq->getJobs($dlqName)))->toBe(0);
        });
    });

    describe('Dead Letter Queue Statistics', function () {
        it('provides accurate queue statistics', function () {
            $dlqName = DeadLetterQueueName::fromString('stats_test');

            // Add jobs with varying statistics
            $jobs = [
                ['failed_attempts' => 1, 'retry_count' => 0],
                ['failed_attempts' => 3, 'retry_count' => 1],
                ['failed_attempts' => 5, 'retry_count' => 2],
                ['failed_attempts' => 2, 'retry_count' => 0],
            ];

            foreach ($jobs as $index => $jobStats) {
                $this->mockDlq->addFailedJob([
                    'id' => "stats_job_{$index}",
                    'original_job_id' => "original_stats_{$index}",
                    'dlq_name' => $dlqName->toString(),
                    'original_queue' => 'stats_test',
                    'job_payload' => "payload_{$index}",
                    'failure_reason' => "Error {$index}",
                    'exception_type' => 'Exception',
                    'stack_trace' => "trace_{$index}",
                    'failed_attempts' => $jobStats['failed_attempts'],
                    'failed_at' => date('Y-m-d H:i:s', time() - $index * 3600), // Different times
                    'retry_count' => $jobStats['retry_count'],
                ]);
            }

            $stats = $this->mockDlq->getQueueStats($dlqName);

            expect($stats['queue_name'])->toBe($dlqName->toString());
            expect($stats['total_jobs'])->toBe(4);
            expect($stats['avg_failed_attempts'])->toBe(2.75); // (1+3+5+2)/4
            expect($stats['max_failed_attempts'])->toBe(5);
            expect($stats['avg_retry_count'])->toBe(0.75); // (0+1+2+0)/4
            expect($stats['max_retry_count'])->toBe(2);
            expect($stats['oldest_job'])->not->toBeNull();
            expect($stats['newest_job'])->not->toBeNull();
        });

        it('handles empty queue statistics', function () {
            $dlqName = DeadLetterQueueName::fromString('empty_stats');

            $stats = $this->mockDlq->getQueueStats($dlqName);

            expect($stats['total_jobs'])->toBe(0);
            expect($stats['avg_failed_attempts'])->toBe(0);
            expect($stats['max_failed_attempts'])->toBe(0);
            expect($stats['avg_retry_count'])->toBe(0);
            expect($stats['max_retry_count'])->toBe(0);
            expect($stats['oldest_job'])->toBeNull();
            expect($stats['newest_job'])->toBeNull();
        });

        it('can list available queues', function () {
            $queues = [
                DeadLetterQueueName::fromString('email_failed'),
                DeadLetterQueueName::fromString('report_failed'),
                DeadLetterQueueName::fromString('background_failed'),
            ];

            foreach ($queues as $index => $queue) {
                $this->mockDlq->addFailedJob([
                    'id' => "queue_job_{$index}",
                    'original_job_id' => "original_{$index}",
                    'dlq_name' => $queue->toString(),
                    'original_queue' => 'test',
                    'job_payload' => "payload_{$index}",
                    'failure_reason' => "Error {$index}",
                    'exception_type' => 'Exception',
                    'stack_trace' => "trace_{$index}",
                    'failed_attempts' => 1,
                    'failed_at' => date('Y-m-d H:i:s'),
                    'retry_count' => 0,
                ]);
            }

            $availableQueues = $this->mockDlq->getAvailableQueues();

            expect(count($availableQueues))->toBe(3);

            $queueNames = array_map(fn ($q) => $q->toString(), $availableQueues);
            expect($queueNames)->toContain('email_failed');
            expect($queueNames)->toContain('report_failed');
            expect($queueNames)->toContain('background_failed');
        });
    });

    describe('Dead Letter Queue Error Scenarios', function () {
        it('handles retry of non-existent job', function () {
            $result = $this->mockDlq->retryJob('non_existent_job');
            expect($result)->toBeFalse();
        });

        it('handles delete of non-existent job', function () {
            $result = $this->mockDlq->deleteJob('non_existent_job');
            expect($result)->toBeFalse();
        });

        it('handles clear of non-existent queue', function () {
            $nonExistentQueue = DeadLetterQueueName::fromString('non_existent');
            $clearedCount = $this->mockDlq->clearQueue($nonExistentQueue);
            expect($clearedCount)->toBe(0);
        });

        it('respects job limit when retrieving jobs', function () {
            $dlqName = DeadLetterQueueName::fromString('limit_test');

            // Add 10 jobs
            for ($i = 1; $i <= 10; $i++) {
                $this->mockDlq->addFailedJob([
                    'id' => "limit_job_{$i}",
                    'original_job_id' => "original_{$i}",
                    'dlq_name' => $dlqName->toString(),
                    'original_queue' => 'limit_test',
                    'job_payload' => "payload_{$i}",
                    'failure_reason' => "Error {$i}",
                    'exception_type' => 'Exception',
                    'stack_trace' => "trace_{$i}",
                    'failed_attempts' => 1,
                    'failed_at' => date('Y-m-d H:i:s'),
                    'retry_count' => 0,
                ]);
            }

            $limitedJobs = $this->mockDlq->getJobs($dlqName, 5);
            expect(count($limitedJobs))->toBe(5);

            $allJobs = $this->mockDlq->getJobs($dlqName, 100);
            expect(count($allJobs))->toBe(10);
        });
    });
});

describe('Dead Letter Queue Integration Scenarios', function () {

    beforeEach(function () {
        $this->testJob = new class () {
            public function __construct(
                public string $email = 'test@example.com',
                public string $subject = 'Test Email'
            ) {
            }

            public function handle(): bool
            {
                // Simulate processing that might fail
                if ($this->email === 'invalid@test.com') {
                    throw new \Exception('Invalid email address');
                }

                return true;
            }
        };

        $this->failureScenarios = [
            'network_timeout' => [
                'reason' => 'Network connection timeout',
                'exception' => 'NetworkTimeoutException',
                'retryable' => true,
            ],
            'invalid_data' => [
                'reason' => 'Invalid email format',
                'exception' => 'ValidationException',
                'retryable' => false,
            ],
            'service_unavailable' => [
                'reason' => 'External service unavailable',
                'exception' => 'ServiceUnavailableException',
                'retryable' => true,
            ],
            'permission_denied' => [
                'reason' => 'Insufficient permissions',
                'exception' => 'PermissionException',
                'retryable' => false,
            ],
        ];
    });

    it('handles different types of job failures', function () {
        $emailDlq = DeadLetterQueueName::fromString('email_failed');

        foreach ($this->failureScenarios as $scenarioName => $scenario) {
            $jobPayload = JobPayload::create($this->testJob);
            $jobId = JobId::generate();

            // Simulate job failure based on scenario
            $expectedRetryable = $scenario['retryable'];

            expect($scenario['reason'])->toBeString();
            expect($scenario['exception'])->toBeString();
            expect($expectedRetryable)->toBeBool();
        }
    });

    it('demonstrates retry strategies for different failure types', function () {
        $retryableFailures = array_filter(
            $this->failureScenarios,
            fn ($scenario) => $scenario['retryable']
        );

        $nonRetryableFailures = array_filter(
            $this->failureScenarios,
            fn ($scenario) => ! $scenario['retryable']
        );

        expect(count($retryableFailures))->toBeGreaterThan(0);
        expect(count($nonRetryableFailures))->toBeGreaterThan(0);

        // Retryable failures should be candidates for retry
        foreach ($retryableFailures as $scenario) {
            expect($scenario['retryable'])->toBeTrue();
        }

        // Non-retryable failures should be handled differently
        foreach ($nonRetryableFailures as $scenario) {
            expect($scenario['retryable'])->toBeFalse();
        }
    });

    it('demonstrates queue-specific dead letter handling', function () {
        $queueTypes = [
            'email' => DeadLetterQueueName::forQueue(QueueName::fromString('email')),
            'reports' => DeadLetterQueueName::forQueue(QueueName::fromString('reports')),
            'background' => DeadLetterQueueName::forQueue(QueueName::fromString('background')),
        ];

        foreach ($queueTypes as $originalQueue => $dlqName) {
            expect($dlqName->toString())->toBe($originalQueue . '_failed');
        }

        // Each queue type should have its own DLQ
        $queueNames = array_map(fn ($dlq) => $dlq->toString(), $queueTypes);
        $uniqueNames = array_unique($queueNames);
        expect(count($uniqueNames))->toBe(count($queueTypes));
    });
});
