<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CoreBundle\Tests\Twig;

use Ferienpass\CoreBundle\Tests\Twig\Fixtures\SampleEntity;
use Ferienpass\CoreBundle\Twig\SandboxedTextRenderer;
use PHPUnit\Framework\TestCase;
use Twig\Error\Error as TwigError;
use Twig\Sandbox\SecurityPolicyInterface;

/**
 * Regression cover for the notification-text sandbox: a token that reads off a
 * Doctrine lazy-loading proxy (class `Proxies\__CG__\Ferienpass\…`) must still
 * render. Before the {@see \Ferienpass\CoreBundle\Twig\NotificationSecurityPolicy}
 * parent-chain fix, the proxy's own class name fell outside the allowed
 * `Ferienpass\` namespace, so the getter was blocked with a Twig sandbox error;
 * {@see \Ferienpass\CoreBundle\Notification\AbstractNotification::getContent()}
 * swallows that error and returns the RAW template — so recipients received an
 * e-mail with literal `{{ offer.name }}` / `{{ participant.firstname }}` tokens.
 */
final class NotificationSandboxProxyTest extends TestCase
{
    public static function setUpBeforeClass(): void
    {
        require_once __DIR__.'/Fixtures/SandboxObjectFixtures.php';
    }

    public function testRendersTokenOnRealEntity(): void
    {
        $renderer = new SandboxedTextRenderer();

        self::assertSame(
            'Angebot Schwimmkurs',
            $renderer->render('Angebot {{ offer.name }}', ['offer' => new SampleEntity()]),
        );
    }

    public function testRendersTokenOnDoctrineProxy(): void
    {
        $renderer = new SandboxedTextRenderer();

        $proxyClass = 'Proxies\__CG__\Ferienpass\CoreBundle\Tests\Twig\Fixtures\SampleEntity';
        $proxy = new $proxyClass();

        // Must render the value, not leak the raw `{{ offer.name }}` token.
        self::assertSame(
            'Angebot Schwimmkurs',
            $renderer->render('Angebot {{ offer.name }}', ['offer' => $proxy]),
        );
    }

    public function testStillRejectsForeignObject(): void
    {
        $renderer = new SandboxedTextRenderer();

        $this->expectException(TwigError::class);

        $foreignClass = 'Acme\ForeignObject';
        $renderer->render('{{ obj.name }}', ['obj' => new $foreignClass()]);
    }

    public function testAllowsSideEffectFreeTests(): void
    {
        $renderer = new SandboxedTextRenderer();

        // Twig core flags these as always-allowed-in-sandbox, so they bypass the policy.
        self::assertSame(
            'ja',
            $renderer->render('{% if offer.name is defined %}ja{% endif %}', ['offer' => new SampleEntity()]),
        );
    }

    /**
     * Twig only hands the used tests to the policy when the policy declares the 4th
     * `array $tests` argument of {@see SecurityPolicyInterface::checkSecurity()}. With a
     * 3-parameter signature SandboxExtension drops them, and `constant` — the one core
     * test not flagged always-allowed-in-sandbox — reaches nobody. Assert it arrives.
     */
    public function testConstantTestIsHandedToTheSecurityPolicy(): void
    {
        $policy = new class implements SecurityPolicyInterface {
            /** @var string[] */
            public array $seenTests = [];

            public function checkSecurity($tags, $filters, $functions, array $tests = []): void
            {
                $this->seenTests = $tests;
            }

            public function checkMethodAllowed($obj, $method): void
            {
            }

            public function checkPropertyAllowed($obj, $property): void
            {
            }
        };

        new SandboxedTextRenderer($policy)->render('{% if value is constant("PHP_EOL") %}ja{% endif %}', ['value' => 1]);

        self::assertSame(['constant'], $policy->seenTests);
    }
}
