<?php

declare(strict_types=1);

namespace Ferienpass\CoreBundle\Tests\Security\Voter;

use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Security\Voter\ParticipantVoter;
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 ParticipantVoterTest extends TestCase
{
    private function makeVoter(bool $isAdmin = false, bool $isHost = false): ParticipantVoter
    {
        $security = $this->createMock(Security::class);
        $security->method('isGranted')->willReturnCallback(function (string $role) use ($isAdmin, $isHost) {
            return match ($role) {
                'ROLE_ADMIN' => $isAdmin,
                'ROLE_HOST' => $isHost,
                default => false,
            };
        });

        return new ParticipantVoter($security);
    }

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

        return $token;
    }

    public function testAbstainsForNonParticipantSubject(): 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 BaseParticipant(), ['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 BaseParticipant(), ['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 BaseParticipant(), ['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 BaseParticipant(), ['view']);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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