<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Infrastructure\Security;

use App\Administration\Domain\Model\User\Role;
use App\Administration\Infrastructure\Api\Resource\SubjectResource;
use App\Administration\Infrastructure\Security\SubjectVoter;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;

final class SubjectVoterTest extends TestCase
{
    private SubjectVoter $voter;

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

    #[Test]
    public function itAbstainsForUnrelatedAttributes(): void
    {
        $token = $this->tokenWithRole(Role::ADMIN->value);

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

        self::assertSame(Voter::ACCESS_ABSTAIN, $result);
    }

    #[Test]
    public function itDeniesAccessToUnauthenticatedUsers(): void
    {
        $token = $this->createMock(TokenInterface::class);
        $token->method('getUser')->willReturn(null);

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

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    // --- VIEW ---

    #[Test]
    #[DataProvider('viewAllowedRolesProvider')]
    public function itGrantsViewToStaffRoles(string $role): void
    {
        $token = $this->tokenWithRole($role);

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

        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    /**
     * @return iterable<string, array{string}>
     */
    public static function viewAllowedRolesProvider(): iterable
    {
        yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value];
        yield 'ADMIN' => [Role::ADMIN->value];
        yield 'PROF' => [Role::PROF->value];
        yield 'VIE_SCOLAIRE' => [Role::VIE_SCOLAIRE->value];
        yield 'SECRETARIAT' => [Role::SECRETARIAT->value];
    }

    #[Test]
    #[DataProvider('viewDeniedRolesProvider')]
    public function itDeniesViewToNonStaffRoles(string $role): void
    {
        $token = $this->tokenWithRole($role);

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

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    /**
     * @return iterable<string, array{string}>
     */
    public static function viewDeniedRolesProvider(): iterable
    {
        yield 'PARENT' => [Role::PARENT->value];
        yield 'ELEVE' => [Role::ELEVE->value];
    }

    #[Test]
    public function itSupportsViewWithSubjectResourceSubject(): void
    {
        $token = $this->tokenWithRole(Role::ADMIN->value);
        $subject = new SubjectResource();

        $result = $this->voter->vote($token, $subject, [SubjectVoter::VIEW]);

        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    // --- CREATE ---

    #[Test]
    #[DataProvider('adminRolesProvider')]
    public function itGrantsCreateToAdminRoles(string $role): void
    {
        $token = $this->tokenWithRole($role);

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

        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    #[DataProvider('nonAdminRolesProvider')]
    public function itDeniesCreateToNonAdminRoles(string $role): void
    {
        $token = $this->tokenWithRole($role);

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

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    // --- EDIT ---

    #[Test]
    #[DataProvider('adminRolesProvider')]
    public function itGrantsEditToAdminRoles(string $role): void
    {
        $token = $this->tokenWithRole($role);

        $result = $this->voter->vote($token, new SubjectResource(), [SubjectVoter::EDIT]);

        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    #[DataProvider('nonAdminRolesProvider')]
    public function itDeniesEditToNonAdminRoles(string $role): void
    {
        $token = $this->tokenWithRole($role);

        $result = $this->voter->vote($token, new SubjectResource(), [SubjectVoter::EDIT]);

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    // --- DELETE ---

    #[Test]
    #[DataProvider('adminRolesProvider')]
    public function itGrantsDeleteToAdminRoles(string $role): void
    {
        $token = $this->tokenWithRole($role);

        $result = $this->voter->vote($token, new SubjectResource(), [SubjectVoter::DELETE]);

        self::assertSame(Voter::ACCESS_GRANTED, $result);
    }

    #[Test]
    #[DataProvider('nonAdminRolesProvider')]
    public function itDeniesDeleteToNonAdminRoles(string $role): void
    {
        $token = $this->tokenWithRole($role);

        $result = $this->voter->vote($token, new SubjectResource(), [SubjectVoter::DELETE]);

        self::assertSame(Voter::ACCESS_DENIED, $result);
    }

    // --- Data Providers ---

    /**
     * @return iterable<string, array{string}>
     */
    public static function adminRolesProvider(): iterable
    {
        yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value];
        yield 'ADMIN' => [Role::ADMIN->value];
    }

    /**
     * @return iterable<string, array{string}>
     */
    public static function nonAdminRolesProvider(): iterable
    {
        yield 'PROF' => [Role::PROF->value];
        yield 'VIE_SCOLAIRE' => [Role::VIE_SCOLAIRE->value];
        yield 'SECRETARIAT' => [Role::SECRETARIAT->value];
        yield 'PARENT' => [Role::PARENT->value];
        yield 'ELEVE' => [Role::ELEVE->value];
    }

    private function tokenWithRole(string $role): TokenInterface
    {
        $user = $this->createMock(UserInterface::class);
        $user->method('getRoles')->willReturn([$role]);

        $token = $this->createMock(TokenInterface::class);
        $token->method('getUser')->willReturn($user);

        return $token;
    }
}
