<?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\Attendance;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Security\Voter\AttendanceVoter;
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 AttendanceVoterTest extends TestCase
{
    public function testAbstainsForUnsupportedAttribute(): void
    {
        $voter = $this->makeVoter();
        $token = $this->makeToken(new User());
        $attendance = new Attendance(new Base(), new BaseParticipant());

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

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

    public function testGrantsViewWhenUserOwnsAttendance(): void
    {
        $voter = $this->makeVoter(isAdmin: false);

        $user = new User();
        new \ReflectionProperty(User::class, 'id')->setValue($user, 7);

        $participant = new BaseParticipant();
        $participant->setUser($user);

        $attendance = new Attendance(new Base(), $participant);
        $token = $this->makeToken($user);

        self::assertSame(VoterInterface::ACCESS_GRANTED, $voter->vote($token, $attendance, ['view']));
    }

    public function testGrantsViewWhenUserIsAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $attendance = new Attendance(new Base(), new BaseParticipant());
        $token = $this->makeToken(new User());

        self::assertSame(VoterInterface::ACCESS_GRANTED, $voter->vote($token, $attendance, ['view']));
    }

    public function testDeniesViewForNonOwner(): void
    {
        $voter = $this->makeVoter(isAdmin: false);

        $owner = new User();
        new \ReflectionProperty(User::class, 'id')->setValue($owner, 1);
        $other = new User();
        new \ReflectionProperty(User::class, 'id')->setValue($other, 2);

        $participant = new BaseParticipant();
        $participant->setUser($owner);

        $attendance = new Attendance(new Base(), $participant);

        self::assertSame(VoterInterface::ACCESS_DENIED, $voter->vote($this->makeToken($other), $attendance, ['view']));
    }

    public function testDeniesViewWhenNoUser(): void
    {
        $voter = $this->makeVoter();
        $attendance = new Attendance(new Base(), new BaseParticipant());

        self::assertSame(VoterInterface::ACCESS_DENIED, $voter->vote($this->makeToken(null), $attendance, ['view']));
    }

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

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

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

    public function testDeniesWhenNoUser(): void
    {
        $voter = $this->makeVoter();
        $token = $this->makeToken(null);
        $attendance = new Attendance(new Base(), new BaseParticipant());

        $result = $voter->vote($token, $attendance, ['withdraw']);

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

    public function testGrantsWhenUserIsAdmin(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $token = $this->makeToken(new User());
        $attendance = new Attendance(new Base(), new BaseParticipant());

        $result = $voter->vote($token, $attendance, ['withdraw']);

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

    public function testGrantsWhenUserOwnsAttendance(): void
    {
        $voter = $this->makeVoter(isAdmin: false);

        $user = new User();
        $prop = new \ReflectionProperty(User::class, 'id');
        $prop->setValue($user, 42);

        $participant = new BaseParticipant();
        $participant->setUser($user);

        $attendance = new Attendance(new Base(), $participant);
        $token = $this->makeToken($user);

        $result = $voter->vote($token, $attendance, ['withdraw']);

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

    public function testDeniesWhenUserDoesNotOwnAttendance(): void
    {
        $voter = $this->makeVoter(isAdmin: false);

        $owner = new User();
        $ownerProp = new \ReflectionProperty(User::class, 'id');
        $ownerProp->setValue($owner, 1);

        $other = new User();
        $otherProp = new \ReflectionProperty(User::class, 'id');
        $otherProp->setValue($other, 2);

        $participant = new BaseParticipant();
        $participant->setUser($owner);

        $attendance = new Attendance(new Base(), $participant);
        $token = $this->makeToken($other);

        $result = $voter->vote($token, $attendance, ['withdraw']);

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

    public function testDeniesWhenAttendanceHasNoParticipant(): void
    {
        $voter = $this->makeVoter(isAdmin: false);

        $attendance = new Attendance(new Base(), null);
        $token = $this->makeToken(new User());

        $result = $voter->vote($token, $attendance, ['withdraw']);

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

    public function testDeniesWhenParticipantHasNoUser(): void
    {
        $voter = $this->makeVoter(isAdmin: false);

        $participant = new BaseParticipant();
        // no user set on participant

        $attendance = new Attendance(new Base(), $participant);
        $token = $this->makeToken(new User());

        $result = $voter->vote($token, $attendance, ['withdraw']);

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

    public function testVoteOnAttributeThrowsLogicExceptionForUnknownAttribute(): void
    {
        $voter = $this->makeVoter();
        $attendance = new Attendance(new Base(), new BaseParticipant());
        $token = $this->makeToken(new User());

        $rm = new \ReflectionMethod($voter, 'voteOnAttribute');

        $this->expectException(\LogicException::class);
        $rm->invoke($voter, 'unknown_attribute', $attendance, $token);
    }

    private function makeVoter(bool $isAdmin = false): AttendanceVoter
    {
        $security = $this->createMock(Security::class);
        $security->method('isGranted')->with('ROLE_ADMIN')->willReturn($isAdmin);

        return new AttendanceVoter($security);
    }

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

        return $token;
    }
}
