<?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\HttpCache;

use Doctrine\Common\Collections\ArrayCollection;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\HttpCache\UserContextProvider;
use FOS\HttpCache\UserContext\UserContext;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

/**
 * Locks in the user-context-hash contract that powers role-bucket caching.
 *
 * The interesting case (and the one that motivated the test file) is
 * {@see SwitchUserToken}: an admin impersonating another user must land in a
 * different cache bucket than the same user visiting un-impersonated, so the
 * page template's conditional `render_esi` call for the impersonation banner
 * (gated on `is_granted('IS_IMPERSONATOR')`) bakes the right HTML into the
 * cached response.
 *
 * If this test ever regresses — two tokens producing the same hash params —
 * the impersonation banner stops appearing for some impersonators because
 * Varnish serves them a cached page whose `<esi:include>` was rendered for a
 * different impersonator (or omitted entirely).
 */
final class UserContextProviderTest extends TestCase
{
    public function testAnonymousAddsEmptyRolesAndNoImpersonatorFlag(): void
    {
        $context = $this->updateContext(null);

        self::assertSame([], $context->getParameters()['roles']);
        self::assertArrayNotHasKey(
            'impersonator',
            $context->getParameters(),
            'Anonymous traffic must not carry the impersonator flag — would split the public bucket pointlessly.'
        );
    }

    public function testAuthenticatedUserAddsRolesButNoImpersonatorFlag(): void
    {
        $user = $this->makeUser(['ROLE_USER']);
        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        $context = $this->updateContext($token);

        self::assertSame(['ROLE_USER'], $context->getParameters()['roles']);
        self::assertArrayNotHasKey(
            'impersonator',
            $context->getParameters(),
            'A regular authenticated session must NOT carry the impersonator flag.'
        );
    }

    public function testSwitchUserTokenAddsImpersonatorFlag(): void
    {
        // The admin (the "previous" token's user) impersonates `klaus`.
        $impersonated = $this->makeUser(['ROLE_USER']);
        $previousAdmin = $this->makeUser(['ROLE_ADMIN']);
        $previousToken = new UsernamePasswordToken($previousAdmin, 'main', $previousAdmin->getRoles());
        $switchToken = new SwitchUserToken($impersonated, 'main', $impersonated->getRoles(), $previousToken);

        $context = $this->updateContext($switchToken);

        self::assertTrue(
            $context->getParameters()['impersonator'] ?? false,
            'SwitchUserToken must add `impersonator: true` so the layout can emit the banner ESI '
            .'call into a separate cache bucket. Without this flag the banner either appears for '
            .'everyone in the role bucket (cache poisoning) or never (because the first hit was '
            .'rendered without impersonation).'
        );
    }

    public function testImpersonatorFlagPartitionsHashBucketFromNonImpersonator(): void
    {
        // Same user, same roles — only the impersonation state differs. The
        // hash MUST come out different, otherwise Varnish can serve a cached
        // page rendered without the banner to an impersonator (or vice versa).
        $user = $this->makeUser(['ROLE_USER']);

        $regular = new UsernamePasswordToken($user, 'main', $user->getRoles());
        $switch = new SwitchUserToken(
            $user,
            'main',
            $user->getRoles(),
            new UsernamePasswordToken($this->makeUser(['ROLE_ADMIN']), 'main', ['ROLE_ADMIN']),
        );

        $regularContext = $this->updateContext($regular);
        $switchContext = $this->updateContext($switch);

        self::assertNotSame(
            $regularContext->getParameters(),
            $switchContext->getParameters(),
            'Impersonator and non-impersonator must produce different context parameter sets so '
            .'the resulting Varnish cache buckets are distinct.'
        );
    }

    private function updateContext(?TokenInterface $token): UserContext
    {
        $tokenStorage = $this->createMock(TokenStorageInterface::class);
        $tokenStorage->method('getToken')->willReturn($token);

        $provider = new UserContextProvider($tokenStorage);
        $context = new UserContext();
        $provider->updateUserContext($context);

        return $context;
    }

    /**
     * @param list<string> $roles
     */
    private function makeUser(array $roles): User
    {
        $user = $this->createMock(User::class);
        $user->method('getRoles')->willReturn($roles);
        $user->method('isAdmin')->willReturn(\in_array('ROLE_ADMIN', $roles, true));
        $user->method('isHost')->willReturn(\in_array('ROLE_HOST', $roles, true));
        $user->method('getHosts')->willReturn(new ArrayCollection());

        return $user;
    }
}
