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

use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Security\Voter\UserVoter;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

final class UserVoterTest extends TestCase
{
    public function testAbstainsForNonUserSubject(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $token = $this->makeToken(new User());

        $result = $voter->vote($token, new \stdClass(), ['view']);

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

    public function testAbstainsForUnsupportedAttribute(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $token = $this->makeToken(new User());

        $result = $voter->vote($token, new User(), ['unknown']);

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

    public function testDeniesWhenNoUser(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $token = $this->makeToken(null);

        $result = $voter->vote($token, new User(), ['view']);

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

    public function testGrantsViewToAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $token = $this->makeToken(new User());

        $result = $voter->vote($token, new User(), ['view']);

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

    public function testDeniesViewToNonAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: false);
        $token = $this->makeToken(new User());

        $result = $voter->vote($token, new User(), ['view']);

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

    public function testGrantsEditWhenUserIsSelf(): void
    {
        $voter = $this->makeVoter(isAdmin: false);
        $user = new User();
        $token = $this->makeToken($user);

        $result = $voter->vote($token, $user, ['edit']);

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

    public function testDeniesEditForAdminAccountByNonSuperAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true, isSuperAdmin: false);
        $currentUser = new User();
        $token = $this->makeToken($currentUser);

        $adminAccount = $this->createMock(User::class);
        $adminAccount->method('isAdmin')->willReturn(true);

        $result = $voter->vote($token, $adminAccount, ['edit']);

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

    public function testGrantsEditForAdminAccountBySuperAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true, isSuperAdmin: true);
        $currentUser = new User();
        $token = $this->makeToken($currentUser);

        $adminAccount = $this->createMock(User::class);
        $adminAccount->method('isAdmin')->willReturn(true);

        $result = $voter->vote($token, $adminAccount, ['edit']);

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

    public function testGrantsDeleteToAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $token = $this->makeToken(new User());

        $account = $this->createMock(User::class);
        $account->method('isAdmin')->willReturn(false);

        $result = $voter->vote($token, $account, ['delete']);

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

    public function testGrantsUnblockToAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $token = $this->makeToken(new User());

        $result = $voter->vote($token, new User(), ['unblock']);

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

    public function testGrantsPasswordChangeToAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $token = $this->makeToken(new User());

        $account = $this->createMock(User::class);
        $account->method('isAdmin')->willReturn(false);

        $result = $voter->vote($token, $account, ['password']);

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

    public function testGrantsLoginLinkToAdminForNonAdminAccount(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $token = $this->makeToken(new User());

        $account = $this->createMock(User::class);
        $account->method('isAdmin')->willReturn(false);

        $result = $voter->vote($token, $account, ['login_link']);

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

    public function testDeniesLoginLinkForAdminAccountByNonSuperAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true, isSuperAdmin: false);
        $token = $this->makeToken(new User());

        $account = $this->createMock(User::class);
        $account->method('isAdmin')->willReturn(true);

        $result = $voter->vote($token, $account, ['login_link']);

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

    public function testDeniesDeleteAdminAccountByNonSuperAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true, isSuperAdmin: false);
        $token = $this->makeToken(new User());

        $account = $this->createMock(User::class);
        $account->method('isAdmin')->willReturn(true);

        $result = $voter->vote($token, $account, ['delete']);

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

    public function testDeniesPasswordChangeForAdminAccountByNonSuperAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true, isSuperAdmin: false);
        $token = $this->makeToken(new User());

        $account = $this->createMock(User::class);
        $account->method('isAdmin')->willReturn(true);

        $result = $voter->vote($token, $account, ['password']);

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

    public function testGrantsEditForNonAdminAccountByAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $currentUser = new User();
        $token = $this->makeToken($currentUser);

        $account = $this->createMock(User::class);
        $account->method('isAdmin')->willReturn(false);

        $result = $voter->vote($token, $account, ['edit']);

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

    private function makeVoter(bool $isAdmin = false, bool $isSuperAdmin = false): UserVoter
    {
        $security = $this->createMock(Security::class);
        $security->method('isGranted')->willReturnCallback(static function (string $role) use ($isAdmin, $isSuperAdmin) {
            if ('ROLE_SUPER_ADMIN' === $role) {
                return $isSuperAdmin;
            }

            return $isAdmin;
        });

        return new UserVoter($security);
    }

    private function makeToken(?User $user): TokenInterface
    {
        $token = $this->createStub(TokenInterface::class);
        $token->method('getUser')->willReturn($user);

        return $token;
    }
}
