<?php

declare(strict_types=1);

namespace apexl\Io\modules\jobs\Tests;

use apexl\Io\modules\jobs\Interface\QueueTaskHandlerInterface;
use apexl\Io\modules\jobs\Interface\RetryableTaskHandlerInterface;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Throwable;

/**
 * Living-documentation contract for RetryableTaskHandlerInterface.
 *
 * Confirms the interface extends the non-retryable parent, that a sample
 * implementation satisfies both contracts, and that the boundary
 * behaviour any consumer needs to honour (maxAttempts, retryDelaySeconds)
 * round-trips as declared.
 */
final class RetryableTaskHandlerInterfaceTest extends TestCase
{
    #[Test]
    public function extends_the_base_queue_task_handler_contract(): void
    {
        $reflection = new \ReflectionClass(RetryableTaskHandlerInterface::class);
        self::assertTrue($reflection->implementsInterface(QueueTaskHandlerInterface::class));
    }

    #[Test]
    public function sample_implementation_satisfies_both_interfaces(): void
    {
        $handler = new SampleRetryableHandler(maxAttempts: 5, baseDelay: 30);
        self::assertInstanceOf(RetryableTaskHandlerInterface::class, $handler);
        self::assertInstanceOf(QueueTaskHandlerInterface::class, $handler);
    }

    #[Test]
    public function max_attempts_round_trips(): void
    {
        $handler = new SampleRetryableHandler(maxAttempts: 7, baseDelay: 10);
        self::assertSame(7, $handler->maxAttempts());
    }

    #[Test]
    public function retry_delay_seconds_can_implement_linear_backoff(): void
    {
        $handler = new SampleRetryableHandler(maxAttempts: 5, baseDelay: 10);
        self::assertSame(10, $handler->retryDelaySeconds(1));
        self::assertSame(20, $handler->retryDelaySeconds(2));
        self::assertSame(30, $handler->retryDelaySeconds(3));
    }

    #[Test]
    public function exponential_backoff_implementation_works(): void
    {
        $handler = new SampleExponentialHandler(maxAttempts: 5, baseDelay: 2);
        self::assertSame(2,  $handler->retryDelaySeconds(1)); // 2 * 2^0
        self::assertSame(4,  $handler->retryDelaySeconds(2)); // 2 * 2^1
        self::assertSame(8,  $handler->retryDelaySeconds(3)); // 2 * 2^2
        self::assertSame(16, $handler->retryDelaySeconds(4));
    }

    #[Test]
    public function handlers_can_record_failure_callbacks(): void
    {
        $handler = new SampleRetryableHandler(maxAttempts: 3, baseDelay: 5);
        $handler->handleFailure('queue-job-42', new RuntimeException('boom'));
        self::assertCount(1, $handler->failures);
        self::assertSame('queue-job-42', $handler->failures[0]['task']);
        self::assertSame('boom', $handler->failures[0]['throwable']->getMessage());
    }
}

final class SampleRetryableHandler implements RetryableTaskHandlerInterface
{
    /** @var list<array{task:string,throwable:Throwable}> */
    public array $failures = [];

    public function __construct(
        private readonly int $maxAttempts,
        private readonly int $baseDelay,
    ) {}

    public function run(): void
    {
        throw new RuntimeException('not under test');
    }

    public function handleFailure(string $task, Throwable $throwable): void
    {
        $this->failures[] = ['task' => $task, 'throwable' => $throwable];
    }

    public function maxAttempts(): int { return $this->maxAttempts; }

    /** Linear backoff: attempt * base. */
    public function retryDelaySeconds(int $attempt): int
    {
        return $this->baseDelay * max(1, $attempt);
    }
}

final class SampleExponentialHandler implements RetryableTaskHandlerInterface
{
    public function __construct(
        private readonly int $maxAttempts,
        private readonly int $baseDelay,
    ) {}

    public function run(): void {}
    public function handleFailure(string $task, Throwable $throwable): void {}
    public function maxAttempts(): int { return $this->maxAttempts; }

    /** Exponential backoff: base * 2 ** (attempt - 1). */
    public function retryDelaySeconds(int $attempt): int
    {
        return $this->baseDelay * (2 ** max(0, $attempt - 1));
    }
}
