<?php

namespace App\Tests\Unit\Security\Voter\Core;

use App\Entity\Access\Access;
use App\Entity\Core\File;
use App\Entity\Organization\Organization;
use App\Entity\Person\Person;
use App\Enum\Core\FileTypeEnum;
use App\Enum\Core\FileVisibilityEnum;
use App\Security\Voter\EntityVoter\Core\FileVoter;
use App\Service\Access\Utils;
use App\Service\Security\InternalRequestsService;
use App\Service\Security\SwitchUser;
use App\Service\Utils\DatesUtils;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;

class TestableFileVoter extends FileVoter {
    public function canView(object $subject): bool {
        return parent::canView($subject);
    }
    public function canEdit(object $subject): bool {
        return parent::canEdit($subject);
    }
    public function canCreate(object $subject): bool {
        return parent::canCreate($subject);
    }
    public function canDelete(object $subject): bool {
        return parent::canDelete($subject);
    }
    public function isInFileOwningGroup(File $file, Access $user): bool {
        return parent::isInFileOwningGroup($file, $user);
    }
    public function isInFileOwningGroupWithRole(File $file, Access $user): bool {
        return parent::isInFileOwningGroupWithRole($file, $user);
    }
    public function isAvailable(File $file, ?Access $user): bool { return parent::isAvailable($file, $user); }

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


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

    public function setUp(): void {
        $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->entityManager = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
        $this->switchUserService = $this->getMockBuilder(SwitchUser::class)->disableOriginalConstructor()->getMock();
    }

    public function tearDown(): void
    {
        DatesUtils::clearFakeDatetime();
    }

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

    public function testCanViewPublicFile(): void {
        $fileVoter = $this->makeFileVoterMockFor('canView');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);

        $file->method('getVisibility')->willReturn(FileVisibilityEnum::EVERYBODY()->getValue());

        $this->assertTrue($fileVoter->canView($file));
    }

    public function testCanViewWithValidInternalRequest(): void {
        $fileVoter = $this->makeFileVoterMockFor('canView');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);
        $fileVoter->method('isValidInternalRequest')->willReturn(true);

        $file->method('getVisibility')->willReturn(FileVisibilityEnum::NOBODY()->getValue());

        $this->assertTrue($fileVoter->canView($file));
    }

    public function testCanViewNoUserLoggedIn(): void {
        $fileVoter = $this->makeFileVoterMockFor('canView');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);
        $fileVoter->method('isValidInternalRequest')->willReturn(false);
        $fileVoter->method('isUserLoggedIn')->willReturn(false);

        $file->method('getVisibility')->willReturn(FileVisibilityEnum::NOBODY()->getValue());

        $this->assertFalse($fileVoter->canView($file));
    }

    public function testCanViewUserInFilePersons(): void {
        $fileVoter = $this->makeFileVoterMockFor('canView');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);
        $fileVoter->method('isValidInternalRequest')->willReturn(false);
        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $file->method('getVisibility')->willReturn(FileVisibilityEnum::NOBODY()->getValue());

        $owner = $this->getMockBuilder(Person::class)->getMock();
        $userPerson = $this->getMockBuilder(Person::class)->getMock();

        $user->method('getPerson')->willReturn($userPerson);

        $file->method('getAccessPersons')->willReturn(new ArrayCollection([$userPerson]));

        $this->assertTrue($fileVoter->canView($file));
    }

    public function testCanViewUserIsOwner(): void {
        $fileVoter = $this->makeFileVoterMockFor('canView');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);
        $fileVoter->method('isValidInternalRequest')->willReturn(false);
        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $file->method('getVisibility')->willReturn(FileVisibilityEnum::NOBODY()->getValue());

        $owner = $this->getMockBuilder(Person::class)->getMock();

        $user->method('getPerson')->willReturn($owner);

        $file->method('getAccessPersons')->willReturn(new ArrayCollection([]));
        $file->method('getPerson')->willReturn($owner);

        $this->assertTrue($fileVoter->canView($file));
    }

    public function testCanViewUserHasRoleFile(): void {
        $fileVoter = $this->makeFileVoterMockFor('canView');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);
        $fileVoter->method('isValidInternalRequest')->willReturn(false);
        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $file->method('getVisibility')->willReturn(FileVisibilityEnum::NOBODY()->getValue());

        $owner = $this->getMockBuilder(Person::class)->getMock();
        $userPerson = $this->getMockBuilder(Person::class)->getMock();

        $file->method('getAccessPersons')->willReturn(new ArrayCollection([]));

        $this->accessUtils->method('hasRole')->with($user, 'ROLE_FILE')->willReturn(true);
        $fileVoter->method('isInFileOwningGroup')->with($file, $user)->willReturn(true);

        $this->assertTrue($fileVoter->canView($file));
    }

    public function testCanViewUserInOwningGroupWithRole(): void {
        $fileVoter = $this->makeFileVoterMockFor('canView');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);
        $fileVoter->method('isValidInternalRequest')->willReturn(false);
        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $file->method('getVisibility')->willReturn(FileVisibilityEnum::NOBODY()->getValue());

        $owner = $this->getMockBuilder(Person::class)->getMock();
        $userPerson = $this->getMockBuilder(Person::class)->getMock();

        $file->method('getAccessPersons')->willReturn(new ArrayCollection([]));

        $this->accessUtils->method('hasRole')->with($user, 'ROLE_FILE')->willReturn(false);
        $fileVoter->method('isInFileOwningGroup')->with($file, $user)->willReturn(true);
        $fileVoter->method('isInFileOwningGroupWithRole')->with($file, $user)->willReturn(true);

        $this->assertTrue($fileVoter->canView($file));
    }

    public function testCanViewUserHasNotRoleFile(): void {
        $fileVoter = $this->makeFileVoterMockFor('canView');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);

        $fileVoter->method('isValidInternalRequest')->willReturn(false);

        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $file->method('getVisibility')->willReturn(FileVisibilityEnum::NOBODY()->getValue());

        $owner = $this->getMockBuilder(Person::class)->getMock();
        $userPerson = $this->getMockBuilder(Person::class)->getMock();

        $file->method('getAccessPersons')->willReturn(new ArrayCollection([]));

        $this->accessUtils->method('hasRole')->with($user, 'ROLE_FILE')->willReturn(false);
        $fileVoter->method('isInFileOwningGroup')->with($file, $user)->willReturn(true);

        $this->assertFalse($fileVoter->canView($file));
    }

    public function testCanViewUserNotInOwningGroup(): void {
        $fileVoter = $this->makeFileVoterMockFor('canView');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);

        $fileVoter->method('isValidInternalRequest')->willReturn(false);

        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $file->method('getVisibility')->willReturn(FileVisibilityEnum::NOBODY()->getValue());

        $owner = $this->getMockBuilder(Person::class)->getMock();
        $userPerson = $this->getMockBuilder(Person::class)->getMock();

        $file->method('getAccessPersons')->willReturn(new ArrayCollection([]));

        $this->accessUtils->method('hasRole')->with($user, 'ROLE_FILE')->willReturn(true);
        $fileVoter->method('isInFileOwningGroup')->with($file, $user)->willReturn(false);

        $this->assertFalse($fileVoter->canView($file));
    }

    public function testCanViewFileNotAvailable(): void {
        $fileVoter = $this->makeFileVoterMockFor('canView');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(false);

        $this->assertFalse($fileVoter->canView($file));
    }

    public function testCanEditNotAvailable(): void {
        $fileVoter = $this->makeFileVoterMockFor('canEdit');

        $file = $this->getMockBuilder(File::class)->getMock();
        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(false);

        $this->assertFalse($fileVoter->canEdit($file));
    }

    public function testCanEditNoUserLoggedIn(): void {
        $fileVoter = $this->makeFileVoterMockFor('canEdit');

        $file = $this->getMockBuilder(File::class)->getMock();
        $fileVoter->method('getUser')->willReturn(null);
        $fileVoter->method('isUserLoggedIn')->willReturn(false);

        $fileVoter->method('isAvailable')->with($file, null)->willReturn(true);

        $this->assertFalse($fileVoter->canEdit($file));
    }

    public function testCanEditUserHasRoleFile(): void {
        $fileVoter = $this->makeFileVoterMockFor('canEdit');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);
        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);

        $this->accessUtils->method('hasRole')->with($user, 'ROLE_FILE')->willReturn(true);
        $fileVoter->method('isInFileOwningGroup')->with($file, $user)->willReturn(true);

        $this->assertTrue($fileVoter->canEdit($file));
    }

    public function testCanEditUserHasRoleFileButNotInOwningGroup(): void {
        $fileVoter = $this->makeFileVoterMockFor('canEdit');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);
        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);

        $this->accessUtils->method('hasRole')->with($user, 'ROLE_FILE')->willReturn(true);
        $fileVoter->method('isInFileOwningGroup')->with($file, $user)->willReturn(false);

        $this->assertFalse($fileVoter->canEdit($file));
    }

    public function testCanEditUserInOwningGroupButHasNotRoleFile(): void {
        $fileVoter = $this->makeFileVoterMockFor('canEdit');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);
        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);

        $this->accessUtils->method('hasRole')->with($user, 'ROLE_FILE')->willReturn(false);
        $fileVoter->method('isInFileOwningGroup')->with($file, $user)->willReturn(true);

        $this->assertFalse($fileVoter->canEdit($file));
    }

    public function testCanEditIsOwner(): void {
        $fileVoter = $this->makeFileVoterMockFor('canEdit');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $userPerson = $this->getMockBuilder(Person::class)->getMock();
        $userPerson->method('getId')->willReturn(1);
        $user->method('getPerson')->willReturn($userPerson);

        $fileVoter->method('getUser')->willReturn($user);
        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);

        $this->accessUtils->method('hasRole')->with($user, 'ROLE_FILE')->willReturn(false);
        $fileVoter->method('isInFileOwningGroup')->with($file, $user)->willReturn(true);

        $file->method('getPerson')->willReturn($userPerson);

        $this->assertTrue($fileVoter->canEdit($file));
    }

    public function testCanEditLoggedInButHasNotRight(): void {
        $fileVoter = $this->makeFileVoterMockFor('canEdit');

        $file = $this->getMockBuilder(File::class)->getMock();

        $user = $this->getMockBuilder(Access::class)->getMock();
        $userPerson = $this->getMockBuilder(Person::class)->getMock();
        $userPerson->method('getId')->willReturn(1);
        $user->method('getPerson')->willReturn($userPerson);

        $fileVoter->method('getUser')->willReturn($user);
        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $fileVoter->method('isAvailable')->with($file, $user)->willReturn(true);

        $this->accessUtils->method('hasRole')->with($user, 'ROLE_FILE')->willReturn(false);
        $fileVoter->method('isInFileOwningGroup')->with($file, $user)->willReturn(true);

        $owner = $this->getMockBuilder(Person::class)->getMock();
        $owner->method('getId')->willReturn(2);

        $file->method('getPerson')->willReturn($owner);

        $this->assertFalse($fileVoter->canEdit($file));
    }

    public function testCanCreateUserLoggedIn(): void
    {
        $fileVoter = $this->makeFileVoterMockFor('canCreate');

        $file = $this->getMockBuilder(File::class)->getMock();

        $fileVoter->method('isUserLoggedIn')->willReturn(true);

        $this->assertTrue($fileVoter->canCreate($file));
    }

    public function testCanCreateUserNotLoggedIn(): void
    {
        $fileVoter = $this->makeFileVoterMockFor('canCreate');

        $file = $this->getMockBuilder(File::class)->getMock();

        $fileVoter->method('isUserLoggedIn')->willReturn(false);

        $this->assertFalse($fileVoter->canCreate($file));
    }

    public function testCanDeleteCanEdit(): void
    {
        $fileVoter = $this->makeFileVoterMockFor('canDelete');

        $file = $this->getMockBuilder(File::class)->getMock();

        $fileVoter->method('canEdit')->with($file)->willReturn(true);

        $this->assertTrue($fileVoter->canDelete($file));
    }

    public function testCanDeleteCanNotEdit(): void
    {
        $fileVoter = $this->makeFileVoterMockFor('canDelete');

        $file = $this->getMockBuilder(File::class)->getMock();

        $fileVoter->method('canEdit')->with($file)->willReturn(false);

        $this->assertFalse($fileVoter->canDelete($file));
    }

    public function testIsInFileOwningGroupIsOrganizationOwned(): void {
        $fileVoter = $this->makeFileVoterMockFor('isInFileOwningGroup');

        $userPerson = $this->getMockBuilder(Person::class)->getMock();
        $userPerson->method('getId')->willReturn(2);

        $userOrganization = $this->getMockBuilder(Organization::class)->getMock();
        $userOrganization->method('getId')->willReturn(10);

        $user = $this->getMockBuilder(Access::class)->getMock();
        $user->method('getOrganization')->willReturn($userOrganization);
        $user->method('getPerson')->willReturn($userPerson);
        $fileVoter->method('getUser')->willReturn($user);

        $organization = $this->getMockBuilder(Organization::class)->getMock();
        $organization->method('getId')->willReturn(10);

        $person = $this->getMockBuilder(Person::class)->getMock();
        $person->method('getId')->willReturn(1);

        $file = $this->getMockBuilder(File::class)->getMock();
        $file->method('getOrganization')->willReturn($organization);
        $file->method('getPerson')->willReturn($person);

        $this->assertTrue($fileVoter->isInFileOwningGroup($file, $user));
    }

    public function testIsInFileOwningGroupIsPersonOwned(): void {
        $fileVoter = $this->makeFileVoterMockFor('isInFileOwningGroup');

        $userPerson = $this->getMockBuilder(Person::class)->getMock();
        $userPerson->method('getId')->willReturn(1);

        $userOrganization = $this->getMockBuilder(Organization::class)->getMock();
        $userOrganization->method('getId')->willReturn(20);

        $user = $this->getMockBuilder(Access::class)->getMock();
        $user->method('getOrganization')->willReturn($userOrganization);
        $user->method('getPerson')->willReturn($userPerson);
        $fileVoter->method('getUser')->willReturn($user);

        $organization = $this->getMockBuilder(Organization::class)->getMock();
        $organization->method('getId')->willReturn(10);

        $person = $this->getMockBuilder(Person::class)->getMock();
        $person->method('getId')->willReturn(1);

        $file = $this->getMockBuilder(File::class)->getMock();
        $file->method('getOrganization')->willReturn($organization);
        $file->method('getPerson')->willReturn($person);

        $this->assertTrue($fileVoter->isInFileOwningGroup($file, $user));
    }

    public function testIsInFileOwningGroupNotInGroup(): void {
        $fileVoter = $this->makeFileVoterMockFor('isInFileOwningGroup');

        $userPerson = $this->getMockBuilder(Person::class)->getMock();
        $userPerson->method('getId')->willReturn(2);

        $userOrganization = $this->getMockBuilder(Organization::class)->getMock();
        $userOrganization->method('getId')->willReturn(20);

        $user = $this->getMockBuilder(Access::class)->getMock();
        $user->method('getOrganization')->willReturn($userOrganization);
        $user->method('getPerson')->willReturn($userPerson);
        $fileVoter->method('getUser')->willReturn($user);

        $organization = $this->getMockBuilder(Organization::class)->getMock();
        $organization->method('getId')->willReturn(10);

        $person = $this->getMockBuilder(Person::class)->getMock();
        $person->method('getId')->willReturn(1);

        $file = $this->getMockBuilder(File::class)->getMock();
        $file->method('getOrganization')->willReturn($organization);
        $file->method('getPerson')->willReturn($person);

        $this->assertFalse($fileVoter->isInFileOwningGroup($file, $user));
    }

    public function testIsAvailablePassedAvailabilityDate(): void {
        $fileVoter = $this->makeFileVoterMockFor('isAvailable');

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        DatesUtils::setFakeDatetime('2020-01-31');

        $file = $this->getMockBuilder(File::class)->getMock();
        $file->method('getAvailabilityDate')->willReturn(new \DateTime('2020-01-01'));

        $this->assertTrue($fileVoter->isAvailable($file, $user));
    }

    public function testIsAvailableAnonymousAndPassedAvailabilityDate(): void {
        $fileVoter = $this->makeFileVoterMockFor('isAvailable');

        DatesUtils::setFakeDatetime('2020-01-31');

        $file = $this->getMockBuilder(File::class)->getMock();
        $file->method('getAvailabilityDate')->willReturn(new \DateTime('2020-01-01'));

        $this->assertTrue($fileVoter->isAvailable($file, null));
    }

    public function testIsAvailableNoAvailabilityDate(): void {
        $fileVoter = $this->makeFileVoterMockFor('isAvailable');

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        $file = $this->getMockBuilder(File::class)->getMock();
        $file->method('getAvailabilityDate')->willReturn(null);

        $this->assertTrue($fileVoter->isAvailable($file, $user));
    }

    public function testIsAvailableFutureAvailabilityDate(): void {
        $fileVoter = $this->makeFileVoterMockFor('isAvailable');

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        DatesUtils::setFakeDatetime('2020-01-01');

        $file = $this->getMockBuilder(File::class)->getMock();
        $file->method('getAvailabilityDate')->willReturn(new \DateTime('2020-01-31'));
        $file->method('getType')->willReturn(FileTypeEnum::UNKNOWN()->getValue());
        $this->accessUtils->method('hasRole')->with($user, 'ROLE_BILLACCOUNTING')->willReturn(false);

        $this->assertFalse($fileVoter->isAvailable($file, $user));
    }

    public function testIsAvailableFutureAvailabilityDateSpecialBillingCase(): void {
        $fileVoter = $this->makeFileVoterMockFor('isAvailable');

        $user = $this->getMockBuilder(Access::class)->getMock();
        $fileVoter->method('getUser')->willReturn($user);

        DatesUtils::setFakeDatetime('2020-01-01');

        $file = $this->getMockBuilder(File::class)->getMock();
        $file->method('getAvailabilityDate')->willReturn(new \DateTime('2020-01-31'));
        $file->method('getType')->willReturn(FileTypeEnum::BILL()->getValue());
        $this->accessUtils->method('hasRole')->with($user, 'ROLE_BILLACCOUNTING')->willReturn(true);

        $this->assertTrue($fileVoter->isAvailable($file, $user));
    }
}
