<?php

declare(strict_types=1);

namespace App\Tests\Unit\Shared\Infrastructure\Messenger;

use App\Shared\Infrastructure\Messenger\FibonacciRetryStrategy;
use InvalidArgumentException;
use LogicException;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use stdClass;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Twig\Error\LoaderError;

final class FibonacciRetryStrategyTest extends TestCase
{
    private FibonacciRetryStrategy $strategy;

    protected function setUp(): void
    {
        $this->strategy = new FibonacciRetryStrategy();
    }

    public function testIsRetryableForTransientError(): void
    {
        $envelope = new Envelope(new stdClass());
        $throwable = new TransportException('SMTP timeout');

        $this->assertTrue($this->strategy->isRetryable($envelope, $throwable));
    }

    public function testIsNotRetryableForPermanentError(): void
    {
        $envelope = new Envelope(new stdClass());
        $throwable = new LoaderError('Template not found');

        $this->assertFalse($this->strategy->isRetryable($envelope, $throwable));
    }

    public function testIsNotRetryableForInvalidArgumentException(): void
    {
        $envelope = new Envelope(new stdClass());
        $throwable = new InvalidArgumentException('Invalid email');

        $this->assertFalse($this->strategy->isRetryable($envelope, $throwable));
    }

    public function testIsNotRetryableForLogicException(): void
    {
        $envelope = new Envelope(new stdClass());
        $throwable = new LogicException('Logic error');

        $this->assertFalse($this->strategy->isRetryable($envelope, $throwable));
    }

    public function testIsNotRetryableWhenMaxRetriesReached(): void
    {
        $envelope = new Envelope(new stdClass(), [new RedeliveryStamp(7)]);
        $throwable = new TransportException('SMTP timeout');

        $this->assertFalse($this->strategy->isRetryable($envelope, $throwable));
    }

    public function testIsRetryableAtRetry6(): void
    {
        $envelope = new Envelope(new stdClass(), [new RedeliveryStamp(6)]);
        $throwable = new TransportException('SMTP timeout');

        $this->assertTrue($this->strategy->isRetryable($envelope, $throwable));
    }

    public function testIsRetryableWithNullThrowable(): void
    {
        $envelope = new Envelope(new stdClass());

        $this->assertTrue($this->strategy->isRetryable($envelope, null));
    }

    /**
     * @dataProvider fibonacciSequenceProvider
     */
    public function testFibonacciWaitingTimeSequence(int $retryCount, int $expectedDelayMs): void
    {
        $envelope = new Envelope(new stdClass());
        if ($retryCount > 0) {
            $envelope = $envelope->with(new RedeliveryStamp($retryCount));
        }

        $this->assertSame($expectedDelayMs, $this->strategy->getWaitingTime($envelope));
    }

    /**
     * @return iterable<string, array{int, int}>
     */
    public static function fibonacciSequenceProvider(): iterable
    {
        yield 'retry 0 → 1s' => [0, 1000];
        yield 'retry 1 → 1s' => [1, 1000];
        yield 'retry 2 → 2s' => [2, 2000];
        yield 'retry 3 → 3s' => [3, 3000];
        yield 'retry 4 → 5s' => [4, 5000];
        yield 'retry 5 → 8s' => [5, 8000];
        yield 'retry 6 → 13s' => [6, 13000];
    }

    public function testIsRetryableWithRuntimeException(): void
    {
        $envelope = new Envelope(new stdClass());
        $throwable = new RuntimeException('Connection refused');

        $this->assertTrue($this->strategy->isRetryable($envelope, $throwable));
    }

    public function testIsRetryableWithWrappedTransientError(): void
    {
        $envelope = new Envelope(new stdClass());
        $inner = new TransportException('SMTP timeout');
        $throwable = new RuntimeException('Wrapped', 0, $inner);

        $this->assertTrue($this->strategy->isRetryable($envelope, $throwable));
    }

    public function testIsNotRetryableWithWrappedPermanentError(): void
    {
        $envelope = new Envelope(new stdClass());
        $inner = new LoaderError('Template not found');
        $throwable = new RuntimeException('Wrapped', 0, $inner);

        $this->assertFalse($this->strategy->isRetryable($envelope, $throwable));
    }
}
