<?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\Edition;
use Ferienpass\CoreBundle\Entity\Host;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Repository\HostRepository;
use Ferienpass\CoreBundle\Security\Voter\EditionVoter;
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 EditionVoterTest extends TestCase
{
    public function testAbstainsForNonEditionSubject(): 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 Edition(), ['unknown']);

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

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

        $result = $voter->vote($token, new Edition(), ['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 Edition(), ['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 Edition(), ['view']);

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

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

        $result = $voter->vote($token, new Edition(), ['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 Edition(), ['delete']);

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

    public function testGrantsOfferCreateToAdminWhenEditionEditableForHosts(): void
    {
        $voter = $this->makeVoter(isAdmin: true);
        $token = $this->makeToken(new User());
        $edition = new Edition(); // no host_editing_stage tasks → always editable

        $result = $voter->vote($token, $edition, ['offer.create']);

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

    public function testDeniesOfferCreateForNonAdminWithNoMatchingHost(): void
    {
        $voter = $this->makeVoter(isAdmin: false);
        $token = $this->makeToken(new User());
        $edition = new Edition(); // no tasks, so hosts empty → userIsAllowedHost returns true, but not editable check

        // Edition is editable (no host_editing_stage) and hosts empty → allowed
        // But voter is non-admin, and no matching host in user's hosts
        $result = $voter->vote($token, $edition, ['offer.create']);

        // Edition has no hosts (empty), so userIsAllowedHost returns true,
        // and isEditableForHosts is true → ACCESS_GRANTED
        self::assertSame(VoterInterface::ACCESS_GRANTED, $result);
    }

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

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

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

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

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

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

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

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

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

    public function testDeniesOfferCreateWhenUserIsNotAllowedHost(): void
    {
        $host = $this->createMock(Host::class);
        $host->method('getId')->willReturn(99);

        $security = $this->createMock(Security::class);
        $security->method('isGranted')->with('ROLE_ADMIN')->willReturn(false);

        $hosts = $this->createMock(HostRepository::class);
        $hosts->method('findByUser')->willReturn([]); // user has no hosts

        $voter = new EditionVoter($security, $hosts);
        $token = $this->makeToken(new User());

        $edition = $this->createMock(Edition::class);
        $edition->method('getHosts')->willReturn(new ArrayCollection([$host]));
        $edition->method('isEditableForHosts')->willReturn(true);

        $result = $voter->vote($token, $edition, ['offer.create']);

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

    private function makeVoter(bool $isAdmin = false): EditionVoter
    {
        $security = $this->createMock(Security::class);
        $security->method('isGranted')->with('ROLE_ADMIN')->willReturn($isAdmin);
        $hosts = $this->createStub(HostRepository::class);
        $hosts->method('findByUser')->willReturn([]);

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

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

        return $token;
    }
}
