<?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\Host;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Repository\HostRepository;
use Ferienpass\CoreBundle\Security\Voter\HostVoter;
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 HostVoterTest extends TestCase
{
    public function testAbstainsForNonHostSubject(): 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 Host(), ['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 Host(), ['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 Host(), ['view']);

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

    public function testDeniesViewToNonAdminWithNoMatchingHost(): void
    {
        $voter = $this->makeVoter(isAdmin: false, userHosts: []);
        $token = $this->makeToken(new User());

        $host = new Host();
        $prop = new \ReflectionProperty(Host::class, 'id');
        $prop->setValue($host, 5);

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

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

    public function testGrantsViewWhenUserHostMatchesSubjectHost(): void
    {
        $host = new Host();
        $idProp = new \ReflectionProperty(Host::class, 'id');
        $idProp->setValue($host, 5);

        $voter = $this->makeVoter(isAdmin: false, userHosts: [$host]);
        $token = $this->makeToken(new User());

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

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

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

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

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

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

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

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

    private function makeVoter(bool $isAdmin = false, array $userHosts = []): HostVoter
    {
        $security = $this->createMock(Security::class);
        $security->method('isGranted')->with('ROLE_ADMIN')->willReturn($isAdmin);

        $hosts = $this->createMock(HostRepository::class);
        $hosts->method('findByUser')->willReturn($userHosts);

        return new HostVoter($security, $hosts);
    }

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

        return $token;
    }
}
