<?php

namespace Tests\Unit\Domain\CronJob;

use Domain\CronJob\CronJobProcessor;
use Domain\CronJob\CronJobRepository;
use Domain\CronJob\CronJobType;
use PHPUnit\Framework\TestCase;

class CronJobProcessorTest extends TestCase
{
    /** @var \PHPUnit\Framework\MockObject\MockObject|CronJobRepository */
    private $mockRepo;

    /** @var CronJobProcessor */
    private $processor;

    protected function setUp(): void
    {
        $this->mockRepo = $this->createMock(CronJobRepository::class);
        $this->processor = new CronJobProcessor($this->mockRepo);
    }

    // --- registerHandler ---

    public function testRegisterHandlerAndProcessJob(): void
    {
        $handlerCalled = false;

        $this->processor->registerHandler('test_job', function ($payload) use (&$handlerCalled) {
            $handlerCalled = true;
            return true;
        });

        $this->mockRepo->method('fetchNext')->willReturn([
            ['id' => 1, 'job_type' => 'test_job', 'payload' => null, 'attempts' => 1],
        ]);

        $this->mockRepo->expects($this->once())->method('markCompleted')->with(1, null);

        $stats = $this->processor->processQueue(1);

        $this->assertTrue($handlerCalled);
        $this->assertSame(1, $stats['processed']);
        $this->assertSame(1, $stats['succeeded']);
        $this->assertSame(0, $stats['failed']);
    }

    // --- processQueue ---

    public function testProcessQueueReturnsEmptyStatsWhenNoJobs(): void
    {
        $this->mockRepo->method('fetchNext')->willReturn([]);

        $stats = $this->processor->processQueue(5);

        $this->assertSame(0, $stats['processed']);
        $this->assertSame(0, $stats['succeeded']);
        $this->assertSame(0, $stats['failed']);
        $this->assertSame(0, $stats['skipped']);
    }

    public function testProcessQueueHandlerReturnsFalse(): void
    {
        $this->processor->registerHandler('fail_job', function ($payload) {
            return false;
        });

        $this->mockRepo->method('fetchNext')->willReturn([
            ['id' => 2, 'job_type' => 'fail_job', 'payload' => null, 'attempts' => 1],
        ]);

        $this->mockRepo->expects($this->once())->method('markFailed')
            ->with(2, 'Handler returned false', 1);

        $stats = $this->processor->processQueue(1);

        $this->assertSame(1, $stats['failed']);
        $this->assertSame(0, $stats['succeeded']);
    }

    public function testProcessQueueHandlerThrowsException(): void
    {
        $this->processor->registerHandler('error_job', function ($payload) {
            throw new \RuntimeException('Connection failed');
        });

        $this->mockRepo->method('fetchNext')->willReturn([
            ['id' => 3, 'job_type' => 'error_job', 'payload' => null, 'attempts' => 2],
        ]);

        $this->mockRepo->expects($this->once())->method('markFailed')
            ->with(3, 'Connection failed', 2);

        $stats = $this->processor->processQueue(1);

        $this->assertSame(1, $stats['failed']);
    }

    public function testProcessQueueNoHandlerRegistered(): void
    {
        $this->mockRepo->method('fetchNext')->willReturn([
            ['id' => 4, 'job_type' => 'unknown_job', 'payload' => null, 'attempts' => 1],
        ]);

        $this->mockRepo->expects($this->once())->method('markFailed')
            ->with(4, $this->stringContains('No handler registered'), 1);

        $stats = $this->processor->processQueue(1);

        $this->assertSame(1, $stats['skipped']);
    }

    public function testProcessQueueHandlerReturnsArray(): void
    {
        $resultData = ['synced' => true, 'items' => 5];

        $this->processor->registerHandler('array_job', function ($payload) use ($resultData) {
            return $resultData;
        });

        $this->mockRepo->method('fetchNext')->willReturn([
            ['id' => 5, 'job_type' => 'array_job', 'payload' => null, 'attempts' => 1],
        ]);

        $this->mockRepo->expects($this->once())->method('markCompleted')
            ->with(5, $resultData);

        $stats = $this->processor->processQueue(1);

        $this->assertSame(1, $stats['succeeded']);
    }

    public function testProcessQueuePassesPayloadToHandler(): void
    {
        $receivedPayload = null;

        $this->processor->registerHandler('payload_job', function ($payload) use (&$receivedPayload) {
            $receivedPayload = $payload;
            return true;
        });

        $this->mockRepo->method('fetchNext')->willReturn([
            ['id' => 6, 'job_type' => 'payload_job', 'payload' => ['order_id' => 42], 'attempts' => 1],
        ]);

        $this->mockRepo->method('markCompleted');

        $this->processor->processQueue(1);

        $this->assertSame(['order_id' => 42], $receivedPayload);
    }

    public function testProcessQueueMultipleJobs(): void
    {
        $this->processor->registerHandler('ok_job', function ($payload) {
            return true;
        });
        $this->processor->registerHandler('fail_job', function ($payload) {
            return false;
        });

        $this->mockRepo->method('fetchNext')->willReturn([
            ['id' => 10, 'job_type' => 'ok_job', 'payload' => null, 'attempts' => 1],
            ['id' => 11, 'job_type' => 'fail_job', 'payload' => null, 'attempts' => 1],
            ['id' => 12, 'job_type' => 'ok_job', 'payload' => null, 'attempts' => 1],
        ]);

        $stats = $this->processor->processQueue(10);

        $this->assertSame(3, $stats['processed']);
        $this->assertSame(2, $stats['succeeded']);
        $this->assertSame(1, $stats['failed']);
    }

    // --- createScheduledJobs ---

    public function testCreateScheduledJobsFromDueSchedules(): void
    {
        $this->mockRepo->method('getDueSchedules')->willReturn([
            [
                'id' => 1,
                'job_type' => 'price_history',
                'interval_seconds' => 86400,
                'priority' => 100,
                'max_attempts' => 3,
                'payload' => null,
            ],
        ]);

        $this->mockRepo->method('hasPendingJob')->willReturn(false);

        $this->mockRepo->expects($this->once())->method('enqueue')
            ->with('price_history', null, 100, 3);

        $this->mockRepo->expects($this->once())->method('touchSchedule')
            ->with(1, 86400);

        $created = $this->processor->createScheduledJobs();
        $this->assertSame(1, $created);
    }

    public function testCreateScheduledJobsSkipsDuplicates(): void
    {
        $this->mockRepo->method('getDueSchedules')->willReturn([
            [
                'id' => 2,
                'job_type' => 'apilo_send_order',
                'interval_seconds' => 60,
                'priority' => 50,
                'max_attempts' => 10,
                'payload' => null,
            ],
        ]);

        $this->mockRepo->method('hasPendingJob')->willReturn(true);

        $this->mockRepo->expects($this->never())->method('enqueue');
        // touchSchedule still called to prevent re-checking
        $this->mockRepo->expects($this->once())->method('touchSchedule');

        $created = $this->processor->createScheduledJobs();
        $this->assertSame(0, $created);
    }

    public function testCreateScheduledJobsWithPayload(): void
    {
        $this->mockRepo->method('getDueSchedules')->willReturn([
            [
                'id' => 3,
                'job_type' => 'custom_job',
                'interval_seconds' => 600,
                'priority' => 100,
                'max_attempts' => 3,
                'payload' => '{"key":"value"}',
            ],
        ]);

        $this->mockRepo->method('hasPendingJob')->willReturn(false);

        $this->mockRepo->expects($this->once())->method('enqueue')
            ->with('custom_job', ['key' => 'value'], 100, 3);

        $this->processor->createScheduledJobs();
    }

    public function testCreateScheduledJobsReturnsZeroWhenNoSchedules(): void
    {
        $this->mockRepo->method('getDueSchedules')->willReturn([]);

        $created = $this->processor->createScheduledJobs();
        $this->assertSame(0, $created);
    }

    // --- run ---

    public function testRunExecutesFullPipeline(): void
    {
        $this->mockRepo->expects($this->once())->method('recoverStuck')->with(30);
        $this->mockRepo->method('getDueSchedules')->willReturn([]);
        $this->mockRepo->method('fetchNext')->willReturn([]);
        $this->mockRepo->expects($this->once())->method('cleanup')->with(30);

        $stats = $this->processor->run(20);

        $this->assertArrayHasKey('scheduled', $stats);
        $this->assertArrayHasKey('processed', $stats);
        $this->assertArrayHasKey('succeeded', $stats);
        $this->assertArrayHasKey('failed', $stats);
        $this->assertArrayHasKey('skipped', $stats);
    }

    public function testRunReturnsScheduledCount(): void
    {
        $this->mockRepo->method('getDueSchedules')->willReturn([
            [
                'id' => 1,
                'job_type' => 'job_a',
                'interval_seconds' => 60,
                'priority' => 100,
                'max_attempts' => 3,
                'payload' => null,
            ],
            [
                'id' => 2,
                'job_type' => 'job_b',
                'interval_seconds' => 120,
                'priority' => 100,
                'max_attempts' => 3,
                'payload' => null,
            ],
        ]);

        $this->mockRepo->method('hasPendingJob')->willReturn(false);
        $this->mockRepo->method('fetchNext')->willReturn([]);

        $stats = $this->processor->run(20);

        $this->assertSame(2, $stats['scheduled']);
    }
}
