<?php

declare(strict_types=1);

namespace apexl\Io\modules\jobs\Tests;

use apexl\Io\modules\jobs\Entity\QueueJob;
use apexl\Io\modules\jobs\Interface\QueueTaskHandlerInterface;
use apexl\Io\TestInfrastructure\FeatureTest;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use RuntimeException;

/**
 * A handler class the container cannot build without the job's own contents.
 *
 * It is a common enough shape — a promoted `array $data` that null is a
 * TypeError against, or a constructor parameter named something other than
 * `data` — and it used to cost nothing, because nothing resolved the handler
 * until the job actually ran. shouldRun() changed that: it asks before every
 * job is locked, so a resolution failure now lands inside TaskRunnerService's
 * try. The catch there calls handleFailure(), which resolved the handler
 * again and threw again — out of the catch, out of the loop, and out of the
 * pass. One bad handler class took the whole queue down with it.
 *
 * Resolution failure is now "no opinion": the job runs, and if it fails it
 * fails normally.
 */
final class UnresolvableHandlerTest extends FeatureTest
{
    protected \Faker\Generator $faker;

    private function job(): QueueJob
    {
        $job = new UnresolvableHandlerQueueJob();
        $job->sequence = random_int(1, 2_000_000_000);
        $job->handler = 'SomeHandlerTheContainerCannotBuild';
        $job->created = new DateTimeImmutable();
        $job->contents = ['nothing' => 'much'];
        $job->locked = false;
        $job->attempts = 0;
        $job->run_after = 0;
        $job->store();

        return $job;
    }

    #[Test]
    public function anUnresolvableHandlerIsTreatedAsWillingToRun(): void
    {
        $job = $this->job();

        self::assertTrue($job->shouldRun(), 'The behaviour every job had before shouldRun() existed.');
        self::assertSame(0, $job->skipDelaySeconds());
    }

    #[Test]
    public function failureHandlingSurvivesAnUnresolvableHandler(): void
    {
        $job = $this->job();

        $job->handleFailure('Queue Job 1', new RuntimeException('the job itself blew up'));

        $reloaded = UnresolvableHandlerQueueJob::fromPrimaryKey($job->id);

        self::assertSame(1, (int) $reloaded->attempts, 'The attempt is still recorded.');
        self::assertNull($reloaded->failed, 'One attempt is below the legacy threshold.');
    }

    /** Two failures still fail the job, exactly as a resolvable non-retryable handler would. */
    #[Test]
    public function anUnresolvableHandlerStillExhaustsItsAttempts(): void
    {
        $job = $this->job();

        $job->handleFailure('Queue Job 1', new RuntimeException('once'));
        $job->handleFailure('Queue Job 1', new RuntimeException('twice'));

        self::assertNotNull(
            UnresolvableHandlerQueueJob::fromPrimaryKey($job->id)->failed,
            'It must reach a terminal state rather than retry for ever.',
        );
    }
}

class UnresolvableHandlerQueueJob extends QueueJob
{
    protected function emptyHandler(): QueueTaskHandlerInterface
    {
        throw new RuntimeException('Parameter $contents of __construct() has no value defined or guessable');
    }
}
