<?php

declare(strict_types=1);

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Queue\InMemoryQueue;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueuePriority;

// Test job classes
class SimpleTestJob
{
    public function handle(): string
    {
        return 'test job executed';
    }
}

class CounterTestJob
{
    public function __construct(public int $id)
    {
    }

    public function handle(): string
    {
        return "job {$this->id} executed";
    }
}

class PriorityTestJob
{
    public function __construct(public string $priority)
    {
    }

    public function handle(): string
    {
        return "job with {$this->priority} priority executed";
    }
}

describe('Queue Interface Basic Operations', function () {

    beforeEach(function () {
        $this->queue = new InMemoryQueue();
        $this->testJob = new SimpleTestJob();
    });

    describe('push() operation', function () {
        it('can push jobs to queue', function () {
            $payload = JobPayload::create($this->testJob);

            $this->queue->push($payload);

            expect($this->queue->size())->toBe(1);
        });

        it('maintains priority order when pushing multiple jobs', function () {
            $lowPriorityJob = JobPayload::create($this->testJob, QueuePriority::low());
            $highPriorityJob = JobPayload::create($this->testJob, QueuePriority::high());
            $criticalJob = JobPayload::create($this->testJob, QueuePriority::critical());

            // Push in random order
            $this->queue->push($lowPriorityJob);
            $this->queue->push($criticalJob);
            $this->queue->push($highPriorityJob);

            expect($this->queue->size())->toBe(3);

            // Peek should return critical priority first
            $next = $this->queue->peek();
            expect($next->priority->isCritical())->toBeTrue();
        });

        it('accepts jobs with different configurations', function () {
            $immediateJob = JobPayload::immediate($this->testJob);
            $delayedJob = JobPayload::delayed($this->testJob, Duration::fromSeconds(30));
            $backgroundJob = JobPayload::background($this->testJob);
            $criticalJob = JobPayload::critical($this->testJob);

            $this->queue->push($immediateJob);
            $this->queue->push($delayedJob);
            $this->queue->push($backgroundJob);
            $this->queue->push($criticalJob);

            expect($this->queue->size())->toBe(4);
        });
    });

    describe('pop() operation', function () {
        it('returns null when queue is empty', function () {
            expect($this->queue->pop())->toBeNull();
        });

        it('returns and removes highest priority job first', function () {
            $lowJob = JobPayload::create($this->testJob, QueuePriority::low());
            $highJob = JobPayload::create($this->testJob, QueuePriority::high());

            $this->queue->push($lowJob);
            $this->queue->push($highJob);

            $popped = $this->queue->pop();
            expect($popped->priority->isHigh())->toBeTrue();
            expect($this->queue->size())->toBe(1);

            $remaining = $this->queue->pop();
            expect($remaining->priority->isLow())->toBeTrue();
            expect($this->queue->size())->toBe(0);
        });

        it('processes FIFO for same priority jobs', function () {
            $job1 = (object)['id' => 1];
            $job2 = (object)['id' => 2];

            $payload1 = JobPayload::create($job1, QueuePriority::normal());
            $payload2 = JobPayload::create($job2, QueuePriority::normal());

            $this->queue->push($payload1);
            $this->queue->push($payload2);

            $first = $this->queue->pop();
            expect($first->job->id)->toBe(1);

            $second = $this->queue->pop();
            expect($second->job->id)->toBe(2);
        });
    });

    describe('peek() operation', function () {
        it('returns null when queue is empty', function () {
            expect($this->queue->peek())->toBeNull();
        });

        it('returns next job without removing it', function () {
            $payload = JobPayload::create($this->testJob);
            $this->queue->push($payload);

            $peeked = $this->queue->peek();
            expect($peeked)->not->toBeNull();
            expect($this->queue->size())->toBe(1);

            // Should return same job when peeked again
            $peekedAgain = $this->queue->peek();
            expect($peekedAgain)->toBe($peeked);
        });

        it('shows highest priority job', function () {
            $normalJob = JobPayload::create($this->testJob, QueuePriority::normal());
            $criticalJob = JobPayload::create($this->testJob, QueuePriority::critical());

            $this->queue->push($normalJob);
            $this->queue->push($criticalJob);

            $peeked = $this->queue->peek();
            expect($peeked->priority->isCritical())->toBeTrue();
        });
    });

    describe('size() operation', function () {
        it('returns 0 for empty queue', function () {
            expect($this->queue->size())->toBe(0);
        });

        it('tracks size correctly as jobs are added and removed', function () {
            expect($this->queue->size())->toBe(0);

            $this->queue->push(JobPayload::create($this->testJob));
            expect($this->queue->size())->toBe(1);

            $this->queue->push(JobPayload::create($this->testJob));
            expect($this->queue->size())->toBe(2);

            $this->queue->pop();
            expect($this->queue->size())->toBe(1);

            $this->queue->pop();
            expect($this->queue->size())->toBe(0);
        });
    });

    describe('clear() operation', function () {
        it('returns 0 when clearing empty queue', function () {
            expect($this->queue->clear())->toBe(0);
        });

        it('removes all jobs and returns count', function () {
            $this->queue->push(JobPayload::create($this->testJob));
            $this->queue->push(JobPayload::create($this->testJob));
            $this->queue->push(JobPayload::create($this->testJob));

            expect($this->queue->size())->toBe(3);

            $cleared = $this->queue->clear();
            expect($cleared)->toBe(3);
            expect($this->queue->size())->toBe(0);
        });

        it('queue is usable after clearing', function () {
            $this->queue->push(JobPayload::create($this->testJob));
            $this->queue->clear();

            // Should be able to add new jobs
            $this->queue->push(JobPayload::create($this->testJob));
            expect($this->queue->size())->toBe(1);
        });
    });

    describe('getStats() operation', function () {
        it('returns basic stats for empty queue', function () {
            $stats = $this->queue->getStats();

            expect($stats)->toHaveKey('size');
            expect($stats['size'])->toBe(0);
            expect($stats)->toHaveKey('priority_breakdown');
            expect($stats['priority_breakdown'])->toBe([]);
        });

        it('provides priority breakdown for populated queue', function () {
            $this->queue->push(JobPayload::create($this->testJob, QueuePriority::high()));
            $this->queue->push(JobPayload::create($this->testJob, QueuePriority::high()));
            $this->queue->push(JobPayload::create($this->testJob, QueuePriority::normal()));
            $this->queue->push(JobPayload::create($this->testJob, QueuePriority::low()));

            $stats = $this->queue->getStats();

            expect($stats['size'])->toBe(4);
            expect($stats['priority_breakdown']['high'])->toBe(2);
            expect($stats['priority_breakdown']['normal'])->toBe(1);
            expect($stats['priority_breakdown']['low'])->toBe(1);
        });

        it('updates stats as queue changes', function () {
            $this->queue->push(JobPayload::create($this->testJob, QueuePriority::critical()));
            $this->queue->push(JobPayload::create($this->testJob, QueuePriority::normal()));

            $stats = $this->queue->getStats();
            expect($stats['size'])->toBe(2);
            expect($stats['priority_breakdown']['critical'])->toBe(1);

            // Remove one job
            $this->queue->pop();
            $updatedStats = $this->queue->getStats();
            expect($updatedStats['size'])->toBe(1);
            expect($updatedStats['priority_breakdown']['critical'] ?? 0)->toBe(0);
            expect($updatedStats['priority_breakdown']['normal'])->toBe(1);
        });
    });
});

describe('Queue Priority Processing', function () {

    beforeEach(function () {
        $this->queue = new InMemoryQueue();
    });

    it('processes jobs in correct priority order', function () {
        $jobs = [];

        // Create jobs with different priorities
        $jobs['low'] = JobPayload::create((object)['type' => 'low'], QueuePriority::low());
        $jobs['deferred'] = JobPayload::create((object)['type' => 'deferred'], QueuePriority::deferred());
        $jobs['normal'] = JobPayload::create((object)['type' => 'normal'], QueuePriority::normal());
        $jobs['high'] = JobPayload::create((object)['type' => 'high'], QueuePriority::high());
        $jobs['critical'] = JobPayload::create((object)['type' => 'critical'], QueuePriority::critical());

        // Push in random order
        $this->queue->push($jobs['normal']);
        $this->queue->push($jobs['deferred']);
        $this->queue->push($jobs['critical']);
        $this->queue->push($jobs['low']);
        $this->queue->push($jobs['high']);

        // Pop should return in priority order
        $order = [];
        while (($job = $this->queue->pop()) !== null) {
            $order[] = $job->job->type;
        }

        expect($order)->toBe(['critical', 'high', 'normal', 'low', 'deferred']);
    });

    it('handles custom priority values correctly', function () {
        $customHigh = JobPayload::create((object)['id' => 'custom_high'], new QueuePriority(500));
        $customLow = JobPayload::create((object)['id' => 'custom_low'], new QueuePriority(-50));
        $standardHigh = JobPayload::create((object)['id' => 'standard_high'], QueuePriority::high());

        $this->queue->push($customLow);
        $this->queue->push($standardHigh);
        $this->queue->push($customHigh);

        $first = $this->queue->pop();
        expect($first->job->id)->toBe('custom_high'); // 500 priority

        $second = $this->queue->pop();
        expect($second->job->id)->toBe('standard_high'); // 100 priority

        $third = $this->queue->pop();
        expect($third->job->id)->toBe('custom_low'); // -50 priority
    });
});

describe('Queue Edge Cases', function () {

    beforeEach(function () {
        $this->queue = new InMemoryQueue();
    });

    it('handles many operations on empty queue gracefully', function () {
        expect($this->queue->pop())->toBeNull();
        expect($this->queue->pop())->toBeNull();
        expect($this->queue->peek())->toBeNull();
        expect($this->queue->peek())->toBeNull();
        expect($this->queue->size())->toBe(0);
        expect($this->queue->clear())->toBe(0);
        expect($this->queue->clear())->toBe(0);
    });

    it('maintains integrity after mixed operations', function () {
        $job = (object)['data' => 'test'];

        // Complex sequence of operations
        $this->queue->push(JobPayload::create($job));
        expect($this->queue->size())->toBe(1);

        $peeked = $this->queue->peek();
        expect($peeked->job->data)->toBe('test');
        expect($this->queue->size())->toBe(1);

        $popped = $this->queue->pop();
        expect($popped->job->data)->toBe('test');
        expect($this->queue->size())->toBe(0);

        expect($this->queue->peek())->toBeNull();
        expect($this->queue->pop())->toBeNull();

        // Add more after emptying
        $this->queue->push(JobPayload::create($job));
        expect($this->queue->size())->toBe(1);
    });

    it('handles large number of jobs efficiently', function () {
        $start = microtime(true);

        // Add 1000 jobs
        for ($i = 0; $i < 1000; $i++) {
            $job = new CounterTestJob($i);
            $payload = JobPayload::create($job, QueuePriority::normal());
            $this->queue->push($payload);
        }

        expect($this->queue->size())->toBe(1000);

        // Process all jobs
        $processed = 0;
        while ($this->queue->pop() !== null) {
            $processed++;
        }

        expect($processed)->toBe(1000);
        expect($this->queue->size())->toBe(0);

        $elapsed = microtime(true) - $start;
        expect($elapsed)->toBeLessThan(1.0); // Should complete within 1 second
    });
});
