<?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\EventListener\Security;

use Ferienpass\CoreBundle\EventListener\Security\SwitchUserExitGracefulListener;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;

/**
 * Locks in the contract that stops Sentry 7221664843 from re-firing:
 *
 *   - "Could not find original Token object" + `?_switch_user=_exit`
 *     → redirect, no 500.
 *   - Same exception WITHOUT the query parameter → bubble (real auth bug).
 *   - Other exceptions → never touched.
 */
final class SwitchUserExitGracefulListenerTest extends TestCase
{
    public function testConvertsSwitchUserExitFailureToRedirect(): void
    {
        $event = $this->makeEvent(
            Request::create('/admin/accounts?_switch_user=_exit'),
            new AuthenticationCredentialsNotFoundException('Could not find original Token object.'),
        );

        new SwitchUserExitGracefulListener()($event);

        $response = $event->getResponse();
        self::assertInstanceOf(RedirectResponse::class, $response);
        self::assertSame(
            '/admin/accounts',
            $response->getTargetUrl(),
            'Must redirect to the same path with `_switch_user` stripped — the user just wanted to land on /admin/accounts.'
        );
        self::assertTrue(
            $event->isPropagationStopped(),
            'Must stop propagation so the security ExceptionListener (priority 1) cannot overwrite our redirect with a login redirect.'
        );
    }

    public function testPreservesOtherQueryParameters(): void
    {
        $event = $this->makeEvent(
            Request::create('/admin/accounts?_switch_user=_exit&filter%5Brole%5D=admin&page=3'),
            new AuthenticationCredentialsNotFoundException('Could not find original Token object.'),
        );

        new SwitchUserExitGracefulListener()($event);

        $response = $event->getResponse();
        self::assertInstanceOf(RedirectResponse::class, $response);
        // http_build_query encodes brackets — we don't care which form, just that
        // the filter survived and `_switch_user` is gone.
        $target = $response->getTargetUrl();
        self::assertStringStartsWith('/admin/accounts?', $target);
        self::assertStringNotContainsString('_switch_user', $target);
        self::assertStringContainsString('page=3', $target);
        self::assertMatchesRegularExpression('/filter(%5B|\[)role(%5D|\])=admin/', $target);
    }

    public function testIgnoresAuthExceptionWithoutSwitchUserParam(): void
    {
        // A genuine "no token" auth bug elsewhere in the stack should still
        // surface — don't silently swallow it just because the message matches.
        $event = $this->makeEvent(
            Request::create('/admin/accounts'),
            new AuthenticationCredentialsNotFoundException('Could not find original Token object.'),
        );

        new SwitchUserExitGracefulListener()($event);

        self::assertNull(
            $event->getResponse(),
            'Without `_switch_user=_exit` in the query, the exception is a real auth failure and must keep bubbling.'
        );
        self::assertFalse(
            $event->isPropagationStopped(),
            'A genuine auth failure must reach the security ExceptionListener — never stop propagation here.'
        );
    }

    public function testIgnoresUnrelatedExceptions(): void
    {
        $event = $this->makeEvent(
            Request::create('/admin/accounts?_switch_user=_exit'),
            new \RuntimeException('Some other crash entirely'),
        );

        new SwitchUserExitGracefulListener()($event);

        self::assertNull($event->getResponse());
    }

    private function makeEvent(Request $request, \Throwable $exception): ExceptionEvent
    {
        return new ExceptionEvent(
            $this->createMock(HttpKernelInterface::class),
            $request,
            HttpKernelInterface::MAIN_REQUEST,
            $exception,
        );
    }
}
