<?php

namespace App\Tests\Unit\Security\Voter;

use App\Entity\Access\Access;
use App\Entity\Person\Person;
use App\Security\Voter\EntityVoter\AbstractEntityVoter;
use App\Service\Access\Utils;
use App\Service\Security\InternalRequestsService;
use App\Service\Security\SwitchUser;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class TestableAbstractEntityVoter extends AbstractEntityVoter
{
    public const READ = 'READ';
    public const EDIT = 'EDIT';
    public const CREATE = 'CREATE';
    public const DELETE = 'DELETE';

    public static function setEntityClass(?string $entityClass): void
    {
        static::$entityClass = $entityClass;
    }

    public static function getEntityClass(): ?string
    {
        return static::$entityClass;
    }

    /**
     * @param array<string> $allowedOperations
     */
    public static function setAllowedOperations(array $allowedOperations): void
    {
        static::$allowedOperations = $allowedOperations;
    }

    public static function getAllowedOperations(): array
    {
        return static::$allowedOperations;
    }

    public function supports(string $attribute, mixed $subject): bool
    {
        return parent::supports($attribute, $subject);
    }

    public function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        return parent::voteOnAttribute($attribute, $subject, $token);
    }

    public function canView(object $subject): bool
    {
        return true;
    }

    public function canEdit(object $subject): bool
    {
        return true;
    }

    public function canCreate(object $subject): bool
    {
        return true;
    }

    public function canDelete(object $subject): bool
    {
        return true;
    }

    public function isUserLoggedIn(): bool
    {
        return parent::isUserLoggedIn();
    }

    public function getUser(): ?Access
    {
        return parent::getUser();
    }

    public function isValidInternalRequest(): bool
    {
        return parent::isValidInternalRequest();
    }
}

class AbstractVoterTest extends TestCase
{
    protected Security|MockObject $security;
    protected Utils|MockObject $accessUtils;
    protected EntityManagerInterface|MockObject $em;
    protected InternalRequestsService|MockObject $internalRequestsService;
    protected SwitchUser|MockObject $switchUserService;

    private bool $initialConditionSaved = false;
    private ?string $initialEntityClass = null;
    private ?array $initialAllowedOperations = null;
    private ?int $initialSwitchHeader = null;
    private ?string $initialRemoteAddr = null;
    private ?string $initialInternalRequestToken = null;

    private function saveInitialCondition()
    {
        $this->initialEntityClass = TestableAbstractEntityVoter::getEntityClass();
        $this->initialAllowedOperations = TestableAbstractEntityVoter::getAllowedOperations();
        $this->initialSwitchHeader = $_SERVER['HTTP_X_SWITCH_USER'] ?? null;
        $this->initialRemoteAddr = $_SERVER['REMOTE_ADDR'] ?? null;
        $this->initialInternalRequestToken = $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] ?? null;
        $this->initialConditionSaved = true;
    }

    private function reinitialize()
    {
        // Reinitialize the TestableAbstractVoter static properties
        TestableAbstractEntityVoter::setEntityClass($this->initialEntityClass);
        TestableAbstractEntityVoter::setAllowedOperations($this->initialAllowedOperations);

        // Reinitialize the global variables
        if ($this->initialSwitchHeader !== null) {
            $_SERVER['HTTP_X_SWITCH_USER'] = $this->initialSwitchHeader;
        } elseif (array_key_exists('HTTP_X_SWITCH_USER', $_SERVER)) {
            unset($_SERVER['HTTP_X_SWITCH_USER']);
        }

        $_SERVER['REMOTE_ADDR'] = $this->initialRemoteAddr;

        if ($this->initialSwitchHeader !== null) {
            $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] = $this->initialSwitchHeader;
        } elseif (array_key_exists('HTTP_INTERNAL_REQUESTS_TOKEN', $_SERVER)) {
            unset($_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN']);
        }
    }

    public function setUp(): void
    {
        if (!$this->initialConditionSaved) {
            $this->saveInitialCondition();
        }

        $this->security = $this->getMockBuilder(Security::class)->disableOriginalConstructor()->getMock();
        $this->accessUtils = $this->getMockBuilder(Utils::class)->disableOriginalConstructor()->getMock();
        $this->internalRequestsService = $this->getMockBuilder(InternalRequestsService::class)->disableOriginalConstructor()->getMock();
        $this->em = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
        $this->switchUserService = $this->getMockBuilder(SwitchUser::class)->disableOriginalConstructor()->getMock();
    }

    public function tearDown(): void
    {
        $this->reinitialize();
    }

    private function makeAbstractVoterMockFor(string $methodName): MockObject|TestableAbstractEntityVoter
    {
        return $this->getMockBuilder(TestableAbstractEntityVoter::class)
            ->setConstructorArgs([
                $this->security,
                $this->accessUtils,
                $this->internalRequestsService,
                $this->em,
                $this->switchUserService,
            ])
            ->setMethodsExcept([$methodName])
            ->getMock();
    }

    public function testSupports(): void
    {
        TestableAbstractEntityVoter::setEntityClass(Access::class);

        $abstractVoter = $this->makeAbstractVoterMockFor('supports');

        $access = new Access();

        $this->assertTrue(
            $abstractVoter->supports(TestableAbstractEntityVoter::READ, $access)
        );
        $this->assertTrue(
            $abstractVoter->supports(TestableAbstractEntityVoter::EDIT, $access)
        );
        $this->assertTrue(
            $abstractVoter->supports(TestableAbstractEntityVoter::CREATE, $access)
        );
        $this->assertTrue(
            $abstractVoter->supports(TestableAbstractEntityVoter::DELETE, $access)
        );
    }

    public function testSupportsNotSupportedClass(): void
    {
        TestableAbstractEntityVoter::setEntityClass(Access::class);

        $abstractVoter = $this->makeAbstractVoterMockFor('supports');

        $person = new Person();

        $this->assertFalse(
            $abstractVoter->supports(TestableAbstractEntityVoter::READ, $person)
        );
    }

    public function testSupportsNotAllowedOperation(): void
    {
        TestableAbstractEntityVoter::setEntityClass(Access::class);
        TestableAbstractEntityVoter::setAllowedOperations([TestableAbstractEntityVoter::READ]);

        $abstractVoter = $this->makeAbstractVoterMockFor('supports');

        $access = new Access();

        $this->assertTrue(
            $abstractVoter->supports(TestableAbstractEntityVoter::READ, $access)
        );
        $this->assertFalse(
            $abstractVoter->supports(TestableAbstractEntityVoter::EDIT, $access)
        );
        $this->assertFalse(
            $abstractVoter->supports(TestableAbstractEntityVoter::CREATE, $access)
        );
        $this->assertFalse(
            $abstractVoter->supports(TestableAbstractEntityVoter::DELETE, $access)
        );
    }

    public function testSupportsMissingEntityClass(): void
    {
        $abstractVoter = $this->makeAbstractVoterMockFor('supports');

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Setup the self::$entityClass property, or override the supports() method');

        $access = new Access();
        $abstractVoter->supports(TestableAbstractEntityVoter::READ, $access);
    }

    public function testVoteOnAttributeRead(): void
    {
        $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');

        $subject = $this->getMockBuilder(Access::class)->getMock();
        $token = $this->getMockBuilder(TokenInterface::class)->getMock();

        $abstractVoter->expects(self::once())->method('canView')->with($subject)->willReturn(true);
        $abstractVoter->expects(self::never())->method('canEdit');
        $abstractVoter->expects(self::never())->method('canCreate');
        $abstractVoter->expects(self::never())->method('canDelete');

        $this->assertTrue(
            $abstractVoter->voteOnAttribute(TestableAbstractEntityVoter::READ, $subject, $token)
        );
    }

    public function testVoteOnAttributeEdit(): void
    {
        $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');

        $subject = $this->getMockBuilder(Access::class)->getMock();
        $token = $this->getMockBuilder(TokenInterface::class)->getMock();

        $abstractVoter->expects(self::never())->method('canView');
        $abstractVoter->expects(self::once())->method('canEdit')->with($subject)->willReturn(true);
        $abstractVoter->expects(self::never())->method('canCreate');
        $abstractVoter->expects(self::never())->method('canDelete');

        $this->assertTrue(
            $abstractVoter->voteOnAttribute(TestableAbstractEntityVoter::EDIT, $subject, $token)
        );
    }

    public function testVoteOnAttributeCreate(): void
    {
        $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');

        $subject = $this->getMockBuilder(Access::class)->getMock();
        $token = $this->getMockBuilder(TokenInterface::class)->getMock();

        $abstractVoter->expects(self::never())->method('canView');
        $abstractVoter->expects(self::never())->method('canEdit');
        $abstractVoter->expects(self::once())->method('canCreate')->with($subject)->willReturn(true);
        $abstractVoter->expects(self::never())->method('canDelete');

        $this->assertTrue(
            $abstractVoter->voteOnAttribute(TestableAbstractEntityVoter::CREATE, $subject, $token)
        );
    }

    public function testVoteOnAttributeDelete(): void
    {
        $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');

        $subject = $this->getMockBuilder(Access::class)->getMock();
        $token = $this->getMockBuilder(TokenInterface::class)->getMock();

        $abstractVoter->expects(self::never())->method('canView');
        $abstractVoter->expects(self::never())->method('canEdit');
        $abstractVoter->expects(self::never())->method('canCreate');
        $abstractVoter->expects(self::once())->method('canDelete')->with($subject)->willReturn(true);

        $this->assertTrue(
            $abstractVoter->voteOnAttribute(TestableAbstractEntityVoter::DELETE, $subject, $token)
        );
    }

    public function testVoteOnAttributeInvalidAttribute(): void
    {
        $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');

        $subject = $this->getMockBuilder(Access::class)->getMock();
        $token = $this->getMockBuilder(TokenInterface::class)->getMock();

        $abstractVoter->expects(self::never())->method('canView');
        $abstractVoter->expects(self::never())->method('canEdit');
        $abstractVoter->expects(self::never())->method('canCreate');
        $abstractVoter->expects(self::never())->method('canDelete');

        $this->assertFalse(
            $abstractVoter->voteOnAttribute('other_attr', $subject, $token)
        );
    }

    public function testGetUser()
    {
        $user = $this->getMockBuilder(Access::class)->getMock();

        $this->security->expects(self::once())->method('getUser')->willReturn($user);

        $abstractVoter = $this->makeAbstractVoterMockFor('getUser');

        $this->assertEquals(
            $user,
            $abstractVoter->getUser()
        );
    }

    public function testGetUserNoUser()
    {
        $this->security->expects(self::once())->method('getUser')->willReturn(null);

        $abstractVoter = $this->makeAbstractVoterMockFor('getUser');

        $this->assertNull(
            $abstractVoter->getUser()
        );
    }

    public function testGetUserSwitchUser()
    {
        $originalUser = $this->getMockBuilder(Access::class)->getMock();
        $switchUser = $this->getMockBuilder(Access::class)->getMock();

        $_SERVER['HTTP_X_SWITCH_USER'] = 1;

        $this->security->expects(self::once())->method('getUser')->willReturn($originalUser);
        $this->em->expects(self::once())->method('find')->with(Access::class, 1)->willReturn($switchUser);

        $abstractVoter = $this->makeAbstractVoterMockFor('getUser');

        $this->assertEquals(
            $switchUser,
            $abstractVoter->getUser()
        );
    }

    public function testIsUserLoggedIn(): void
    {
        $user = $this->getMockBuilder(Access::class)->getMock();

        $abstractVoter = $this->makeAbstractVoterMockFor('isUserLoggedIn');

        $abstractVoter->expects(self::once())->method('getUser')->willReturn($user);

        $this->assertTrue($abstractVoter->isUserLoggedIn());
    }

    public function testIsUserLoggedInNoUser(): void
    {
        $abstractVoter = $this->makeAbstractVoterMockFor('isUserLoggedIn');

        $abstractVoter->expects(self::once())->method('getUser')->willReturn(null);

        $this->assertFalse($abstractVoter->isUserLoggedIn());
    }

    public function testIsValidInternalRequest(): void
    {
        $clientIp = '0.0.0.0';
        $token = 'valid_token';

        $_SERVER['REMOTE_ADDR'] = $clientIp;
        $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] = $token;

        $this->internalRequestsService
            ->expects(self::once())
            ->method('isAllowed')
            ->with($clientIp, $token)
            ->willReturn(true);

        $abstractVoter = $this->makeAbstractVoterMockFor('isValidInternalRequest');

        $this->assertTrue($abstractVoter->isValidInternalRequest());
    }
}
