<?php

declare(strict_types=1);

namespace App\Tests\Module\Core\Infrastructure\Security;

use App\Module\Core\Domain\Entity\Permission;
use App\Module\Core\Domain\Entity\Role;
use App\Module\Core\Domain\Entity\User;
use App\Module\Core\Infrastructure\Security\PermissionVoter;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\User\InMemoryUser;

/**
 * Tests unitaires du PermissionVoter RBAC.
 *
 * Le voter est teste via sa methode publique vote() qui retourne une des
 * trois constantes VoterInterface : ACCESS_GRANTED, ACCESS_DENIED, ACCESS_ABSTAIN.
 * - ACCESS_ABSTAIN : supports() a retourne false (attribut non-RBAC).
 * - ACCESS_GRANTED / ACCESS_DENIED : voteOnAttribute() a ete invoque.
 *
 * Aucun acces base de donnees : toutes les entites sont construites en memoire.
 *
 * @internal
 */
class PermissionVoterTest extends TestCase
{
    private PermissionVoter $voter;

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

    // ---------------------------------------------------------------
    // Abstention : attributs non-RBAC
    // ---------------------------------------------------------------

    /**
     * Le voter s'abstient sur ROLE_ADMIN : commence par une majuscule,
     * ne correspond pas au pattern snake_case minuscule avec point.
     */
    public function testAbstainsOnRoleAdminAttribute(): void
    {
        $user  = $this->buildUser(username: 'alice', isAdmin: false);
        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        $result = $this->voter->vote($token, null, ['ROLE_ADMIN']);

        $this->assertSame(VoterInterface::ACCESS_ABSTAIN, $result);
    }

    /**
     * Le voter s'abstient sur IS_AUTHENTICATED_FULLY : contient des majuscules,
     * pas de point de separation conforme au pattern RBAC.
     */
    public function testAbstainsOnIsAuthenticatedAttribute(): void
    {
        $user  = $this->buildUser(username: 'alice', isAdmin: false);
        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        $result = $this->voter->vote($token, null, ['IS_AUTHENTICATED_FULLY']);

        $this->assertSame(VoterInterface::ACCESS_ABSTAIN, $result);
    }

    /**
     * Le voter s'abstient sur des attributs malformes : sans point ou avec
     * majuscules.
     */
    #[DataProvider('malformedAttributeProvider')]
    public function testAbstainsOnMalformedAttribute(string $attribute): void
    {
        $user  = $this->buildUser(username: 'alice', isAdmin: false);
        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        $result = $this->voter->vote($token, null, [$attribute]);

        $this->assertSame(
            VoterInterface::ACCESS_ABSTAIN,
            $result,
            sprintf('Le voter aurait du s\'abstenir pour l\'attribut "%s".', $attribute),
        );
    }

    /**
     * @return array<string, array{string}>
     */
    public static function malformedAttributeProvider(): array
    {
        return [
            'sans point'       => ['nodot'],
            'majuscule milieu' => ['HAS.UPPERCASE'],
            'commence chiffre' => ['1core.users.view'],
            'chaine vide'      => [''],
        ];
    }

    // ---------------------------------------------------------------
    // Refus : utilisateur non reconnu
    // ---------------------------------------------------------------

    /**
     * Refuse l'acces quand le token ne porte pas une instance de User metier
     * (ex: InMemoryUser de Symfony).
     */
    public function testDeniesWhenUserIsNotAUserEntity(): void
    {
        $inMemoryUser = new InMemoryUser('anonymous', null, ['ROLE_USER']);
        $token        = new UsernamePasswordToken($inMemoryUser, 'main', $inMemoryUser->getRoles());

        $result = $this->voter->vote($token, null, ['core.users.view']);

        $this->assertSame(VoterInterface::ACCESS_DENIED, $result);
    }

    // ---------------------------------------------------------------
    // Bypass admin
    // ---------------------------------------------------------------

    /**
     * Accorde l'acces systematiquement a un administrateur, meme sans aucune
     * permission explicite assignee.
     */
    public function testGrantsForAdminBypass(): void
    {
        // Admin sans role ni permission directe : le bypass doit suffire.
        $user  = $this->buildUser(username: 'admin', isAdmin: true);
        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        $result = $this->voter->vote($token, null, ['core.users.view']);

        $this->assertSame(VoterInterface::ACCESS_GRANTED, $result);
    }

    // ---------------------------------------------------------------
    // Permissions effectives via role
    // ---------------------------------------------------------------

    /**
     * Accorde l'acces quand l'utilisateur possede la permission exacte via un role.
     */
    public function testGrantsWhenUserHasExactPermission(): void
    {
        $permission = new Permission('core.users.view', 'Voir les utilisateurs', 'core');
        $role       = new Role('viewer', 'Viewer');
        $role->addPermission($permission);

        $user = $this->buildUser(username: 'alice', isAdmin: false);
        $user->addRbacRole($role);

        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        $result = $this->voter->vote($token, null, ['core.users.view']);

        $this->assertSame(VoterInterface::ACCESS_GRANTED, $result);
    }

    /**
     * Refuse l'acces quand l'utilisateur possede une permission differente de
     * celle demandee.
     */
    public function testDeniesWhenUserLacksPermission(): void
    {
        $permission = new Permission('core.users.view', 'Voir les utilisateurs', 'core');
        $role       = new Role('viewer', 'Viewer');
        $role->addPermission($permission);

        $user = $this->buildUser(username: 'alice', isAdmin: false);
        $user->addRbacRole($role);

        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        // L'utilisateur a core.users.view mais pas core.roles.manage.
        $result = $this->voter->vote($token, null, ['core.roles.manage']);

        $this->assertSame(VoterInterface::ACCESS_DENIED, $result);
    }

    // ---------------------------------------------------------------
    // Permissions directes (hors roles)
    // ---------------------------------------------------------------

    /**
     * Accorde l'acces via une permission directe (assignee sans passer par un role).
     */
    public function testGrantsForDirectPermission(): void
    {
        $permission = new Permission('core.users.view', 'Voir les utilisateurs', 'core');

        $user = $this->buildUser(username: 'bob', isAdmin: false);
        $user->addDirectPermission($permission);

        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        $result = $this->voter->vote($token, null, ['core.users.view']);

        $this->assertSame(VoterInterface::ACCESS_GRANTED, $result);
    }

    // ---------------------------------------------------------------
    // Helpers
    // ---------------------------------------------------------------

    /**
     * Construit un User metier minimal sans persistance.
     */
    private function buildUser(string $username, bool $isAdmin): User
    {
        $user = new User();
        $user->setUsername($username);
        $user->setIsAdmin($isAdmin);
        // Mot de passe factice pour satisfaire PasswordAuthenticatedUserInterface.
        $user->setPassword('hashed_placeholder');

        return $user;
    }
}
