<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Messenger\Tests\Stamp;

use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\RecoverableExceptionInterface;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
use Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer;
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer as SymfonySerializer;

class ErrorDetailsStampTest extends TestCase
{
    public function testGetters()
    {
        $exception = new \Exception('exception message');
        $flattenException = FlattenException::createFromThrowable($exception);
        $flattenException->setTrace([], $exception->getFile(), $exception->getLine());

        $stamp = ErrorDetailsStamp::create($exception);

        $this->assertSame(\Exception::class, $stamp->getExceptionClass());
        $this->assertSame('exception message', $stamp->getExceptionMessage());
        $this->assertEquals($flattenException, $stamp->getFlattenException());
    }

    public function testUnwrappingHandlerFailedException()
    {
        $wrappedException = new \Exception('I am inside', 123);
        $envelope = new Envelope(new \stdClass());
        $exception = new HandlerFailedException($envelope, [$wrappedException]);
        $flattenException = FlattenException::createFromThrowable($wrappedException);
        $flattenException->setTrace([], $wrappedException->getFile(), $wrappedException->getLine());

        $stamp = ErrorDetailsStamp::create($exception);

        $this->assertSame(\Exception::class, $stamp->getExceptionClass());
        $this->assertSame('I am inside', $stamp->getExceptionMessage());
        $this->assertSame(123, $stamp->getExceptionCode());
        $this->assertEquals($flattenException, $stamp->getFlattenException());
    }

    public function testStripsTraceFromNestedExceptions()
    {
        $deepestException = new \Exception('I am the deepest');
        $nestedException = new \Exception('I am nested', 0, $deepestException);
        $exception = new \Exception('I am on top', 0, $nestedException);

        $stamp = ErrorDetailsStamp::create($exception);
        $flattenException = $stamp->getFlattenException();

        $this->assertNotNull($flattenException);
        $nested = $flattenException->getPrevious();
        $this->assertNotNull($nested);
        $deepest = $nested->getPrevious();
        $this->assertNotNull($deepest);

        $this->assertCount(1, $flattenException->getTrace());
        $this->assertCount(1, $nested->getTrace());
        $this->assertCount(1, $deepest->getTrace());
        $this->assertNotSame('', $nested->getTraceAsString());
        $this->assertNotSame('', $deepest->getTraceAsString());
    }

    public function testDeserialization()
    {
        $exception = new \Exception('exception message');
        $stamp = ErrorDetailsStamp::create($exception);

        // FlattenExceptionNormalizer drops trace args, mirror that on the expected stamp
        $flattenException = $stamp->getFlattenException();
        $trace = array_map(static function (array $frame): array {
            unset($frame['args']);

            return $frame;
        }, $flattenException->getTrace());
        (new \ReflectionProperty(FlattenException::class, 'trace'))->setValue($flattenException, $trace);

        $serializer = new Serializer(
            new SymfonySerializer([
                new ArrayDenormalizer(),
                new FlattenExceptionNormalizer(),
                new ObjectNormalizer(),
            ], [new JsonEncoder()])
        );

        $deserializedEnvelope = $serializer->decode($serializer->encode(new Envelope(new \stdClass(), [$stamp])));

        $deserializedStamp = $deserializedEnvelope->last(ErrorDetailsStamp::class);
        $this->assertInstanceOf(ErrorDetailsStamp::class, $deserializedStamp);
        $this->assertEquals($stamp, $deserializedStamp);
    }

    public function testRecoverableExceptionSkipsFlattenException()
    {
        $exception = new RecoverableTestException('recoverable message', 10);

        $stamp = ErrorDetailsStamp::create($exception);

        $this->assertSame($exception::class, $stamp->getExceptionClass());
        $this->assertSame('recoverable message', $stamp->getExceptionMessage());
        $this->assertSame(10, $stamp->getExceptionCode());
        $this->assertNull($stamp->getFlattenException());
    }

    public function testEqualsUsesFlattenExceptionLocation()
    {
        $exceptionA = new \RuntimeException('same');
        $exceptionB = new \RuntimeException('same');

        $stampA = ErrorDetailsStamp::create($exceptionA);
        $stampB = ErrorDetailsStamp::create($exceptionB);

        $this->assertFalse($stampA->equals($stampB));
    }

    public function testEqualsFallsBackToExceptionDetailsWithoutFlattenException()
    {
        $exceptionA = new RecoverableTestException('recoverable', 123);
        $exceptionB = new RecoverableTestException('recoverable', 123);

        $stampA = ErrorDetailsStamp::create($exceptionA);
        $stampB = ErrorDetailsStamp::create($exceptionB);

        $this->assertTrue($stampA->equals($stampB));
    }
}

class RecoverableTestException extends \RuntimeException implements RecoverableExceptionInterface
{
    public function getRetryDelay(): ?int
    {
        return null;
    }
}
