<?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\modules\jobs\Interface\SkippableTaskHandlerInterface;
use apexl\Io\TestInfrastructure\FeatureTest;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use Throwable;

/**
 * Building a handler to ask it questions has to work for every shape of handler
 * in the estate, and there are two: `array $data`, which null is a TypeError
 * against, and a DTO, which `[]` is a TypeError against. Whichever literal was
 * hardcoded, the other half of the estate stopped resolving.
 *
 * That failure is survivable by design — inspectableHandler() answers "no
 * opinion" — which is precisely why it needed covering: a handler that declined
 * to run was silently treated as willing, and a barriered queue went back to
 * locking, attempting and erroring on every job behind the barrier with nothing
 * above debug to say so.
 */
final class EmptyHandlerResolutionTest extends FeatureTest
{
    private function job(string $handler): QueueJob
    {
        $job = new QueueJob();
        $job->sequence = random_int(1, 2_000_000_000);
        $job->handler = $handler;
        $job->created = new DateTimeImmutable();
        $job->contents = ['ref' => 1];
        $job->locked = false;
        $job->attempts = 0;
        $job->run_after = 0;
        $job->store();

        return $job;
    }

    /** The shape `[]` was introduced for. */
    #[Test]
    public function aHandlerTakingAnArrayIsAsked(): void
    {
        $job = $this->job(ArrayDataDecliningHandler::class);

        self::assertFalse($job->shouldRun());
        self::assertSame(600, $job->skipDelaySeconds());
    }

    /** The shape `[]` broke, and null before it. */
    #[Test]
    public function aHandlerTakingADtoIsAsked(): void
    {
        $job = $this->job(DtoDataDecliningHandler::class);

        self::assertFalse($job->shouldRun());
        self::assertSame(600, $job->skipDelaySeconds());
    }

    /** A handler that wants nothing from the job at all. */
    #[Test]
    public function aHandlerTakingNoDataIsAsked(): void
    {
        $job = $this->job(NoDataDecliningHandler::class);

        self::assertFalse($job->shouldRun());
    }
}

final readonly class SomeJobDataDto
{
    public function __construct(public string $table) {}
}

abstract class DecliningHandler implements QueueTaskHandlerInterface, SkippableTaskHandlerInterface
{
    public function run(): void {}

    public function handleFailure(string $task, Throwable $throwable): void {}

    public function shouldRun(): bool
    {
        return false;
    }

    public function skipDelaySeconds(): int
    {
        return 600;
    }
}

/** Not nullable and no default: the container has to be handed something. */
final class ArrayDataDecliningHandler extends DecliningHandler
{
    public function __construct(private readonly array $data) {}
}

final class DtoDataDecliningHandler extends DecliningHandler
{
    public function __construct(private readonly ?SomeJobDataDto $data = null) {}
}

final class NoDataDecliningHandler extends DecliningHandler
{
    public function __construct() {}
}
