<?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 Doctrine\Common\Collections\ArrayCollection;
use Ferienpass\CoreBundle\Entity\Dbafs;
use Ferienpass\CoreBundle\Entity\DbafsOfferAssociation;
use Ferienpass\CoreBundle\Entity\DbafsWithOffer;
use Ferienpass\CoreBundle\Entity\Host;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Security\Voter\DbafsVoter;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

final class DbafsVoterTest extends TestCase
{
    private DbafsVoter $voter;

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

    public function testAbstainsForNonDbafsSubject(): void
    {
        $token = $this->makeToken(new User());
        $result = $this->voter->vote($token, new \stdClass(), ['view']);

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

    public function testAbstainsForNonViewAttribute(): void
    {
        $token = $this->makeToken(new User());
        $dbafs = $this->createStub(Dbafs::class);
        $result = $this->voter->vote($token, $dbafs, ['edit']);

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

    public function testDeniesWhenNoUser(): void
    {
        $token = $this->makeToken(null);
        $dbafs = $this->createStub(Dbafs::class);
        $result = $this->voter->vote($token, $dbafs, ['view']);

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

    public function testGrantsWhenUserIsAdmin(): void
    {
        $user = $this->createMock(User::class);
        $user->method('isAdmin')->willReturn(true);
        $token = $this->makeToken($user);
        $dbafs = $this->createStub(Dbafs::class);

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

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

    public function testGrantsWhenUploadedByCurrentUser(): void
    {
        $user = $this->createMock(User::class);
        $user->method('isAdmin')->willReturn(false);
        $user->method('getHosts')->willReturn(new ArrayCollection());
        $token = $this->makeToken($user);

        $dbafs = $this->createMock(Dbafs::class);
        $dbafs->method('getUploadedBy')->willReturn($user);

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

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

    public function testDeniesWhenNotAdminAndNotUploader(): void
    {
        $user = $this->createMock(User::class);
        $user->method('isAdmin')->willReturn(false);
        $user->method('getHosts')->willReturn(new ArrayCollection());
        $token = $this->makeToken($user);

        $otherUser = $this->createStub(User::class);
        $dbafs = $this->createMock(Dbafs::class);
        $dbafs->method('getUploadedBy')->willReturn($otherUser);

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

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

    public function testGrantsWhenDbafsWithOfferAndUserBelongsToOfferHost(): void
    {
        $host = new Host();

        $user = $this->createMock(User::class);
        $user->method('isAdmin')->willReturn(false);
        $user->method('getHosts')->willReturn(new ArrayCollection([$host]));
        $token = $this->makeToken($user);

        $otherUser = $this->createStub(User::class);

        $offer = $this->createMock(OfferInterface::class);
        $offer->method('getHosts')->willReturn(new ArrayCollection([$host]));

        $relation = $this->createMock(DbafsOfferAssociation::class);
        $relation->method('getOffer')->willReturn($offer);

        $dbafsWithOffer = new class($relation, $otherUser) implements Dbafs, DbafsWithOffer {
            public function __construct(private DbafsOfferAssociation $relation, private User $uploader)
            {
            }

            public function getUuid(): ?\Symfony\Component\Uid\Uuid
            {
                return null;
            }

            public function getOffers(): \Doctrine\Common\Collections\Collection
            {
                return new ArrayCollection([$this->relation]);
            }

            public function getUploadedBy(): ?User
            {
                return $this->uploader;
            }

            public function setUploadedBy(?User $uploadedBy): void
            {
            }
        };

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

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

    public function testDeniesWhenDbafsWithOfferButNoMatchingHost(): void
    {
        $userHost = new Host();
        $otherHost = new Host();

        $user = $this->createMock(User::class);
        $user->method('isAdmin')->willReturn(false);
        $user->method('getHosts')->willReturn(new ArrayCollection([$userHost]));
        $token = $this->makeToken($user);

        $otherUser = $this->createStub(User::class);

        $offer = $this->createMock(OfferInterface::class);
        $offer->method('getHosts')->willReturn(new ArrayCollection([$otherHost]));

        $relation = $this->createMock(DbafsOfferAssociation::class);
        $relation->method('getOffer')->willReturn($offer);

        $dbafsWithOffer = new class($relation, $otherUser) implements Dbafs, DbafsWithOffer {
            public function __construct(private DbafsOfferAssociation $relation, private User $uploader)
            {
            }

            public function getUuid(): ?\Symfony\Component\Uid\Uuid
            {
                return null;
            }

            public function getOffers(): \Doctrine\Common\Collections\Collection
            {
                return new ArrayCollection([$this->relation]);
            }

            public function getUploadedBy(): ?User
            {
                return $this->uploader;
            }

            public function setUploadedBy(?User $uploadedBy): void
            {
            }
        };

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

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

    public function testGrantsWhenSubjectHasGetHostsAndUserBelongsToHost(): void
    {
        $host = new Host();

        $user = $this->createMock(User::class);
        $user->method('isAdmin')->willReturn(false);
        $user->method('getHosts')->willReturn(new ArrayCollection([$host]));
        $token = $this->makeToken($user);

        $otherUser = $this->createStub(User::class);

        $dbafs = new class($otherUser, $host) implements Dbafs {
            public function __construct(private User $uploader, private Host $host)
            {
            }

            public function getUuid(): ?\Symfony\Component\Uid\Uuid
            {
                return null;
            }

            public function getUploadedBy(): ?User
            {
                return $this->uploader;
            }

            public function setUploadedBy(?User $uploadedBy): void
            {
            }

            public function getHosts(): \Doctrine\Common\Collections\Collection
            {
                return new ArrayCollection([$this->host]);
            }
        };

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

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

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

        return $token;
    }
}
