<?php

namespace App\Tests\Unit\Service\Access;

use App\Entity\Access\Access;
use App\Entity\Access\FunctionType;
use App\Entity\Access\OrganizationFunction;
use App\Entity\Organization\Organization;
use App\Repository\Access\AccessRepository;
use App\Service\Access\Utils as AccessUtils;
use App\Service\ServiceIterator\OptionalsRolesIterator;
use App\Service\Utils\DatesUtils;
use Doctrine\Common\Collections\ArrayCollection;
use Hoa\Iterator\Mock;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Role\RoleHierarchy;

class UtilsTest extends TestCase
{
    private MockObject | RoleHierarchy $roleHierarchy;
    private MockObject | OptionalsRolesIterator $optionalsRolesIterator;
    private MockObject | AccessRepository $accessRepository;

    public function setUp():void
    {
        $this->roleHierarchy = $this->getMockBuilder(RoleHierarchy::class)->disableOriginalConstructor()->getMock();
        $this->optionalsRolesIterator = $this->getMockBuilder(OptionalsRolesIterator::class)->disableOriginalConstructor()->getMock();
        $this->accessRepository = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
    }

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

    private function buildAccessUtilsMockForMethod(string $methodName): MockObject | AccessUtils {
        return $this->getMockBuilder(AccessUtils::class)
            ->setConstructorArgs([$this->roleHierarchy,  $this->accessRepository, $this->optionalsRolesIterator])
            ->setMethodsExcept([$methodName])
            ->getMock();
    }


    /**
     * @see Utils::filterAccesses()
     */
    public function testFilterAccesses(): void
    {
        $accessUtils = $this->buildAccessUtilsMockForMethod('filterAccesses');

        $access1 = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $access1->method('getId')->willReturn(1);

        $access2 = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $access2->method('getId')->willReturn(2);

        $access3 = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $access3->method('getId')->willReturn(3);

        $accesses = [$access1, $access2, $access3];

        $result = $accessUtils->filterAccesses($accesses, $access2);

        $this->assertEqualsCanonicalizing(
            [$access1->getId(), $access3->getId()],
            array_map(static function ($access) { return $access->getId(); }, $result)
        );
    }

    /**
     * @see Utils::hasRole()
     */
    public function testHasRole(): void
    {
        $accessUtils = $this->buildAccessUtilsMockForMethod('hasRole');

        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();

        $accessUtils->method('getAllRoles')->with($access)->willReturn(['ROLE_A']);

        $this->assertTrue($accessUtils->hasRole($access, 'ROLE_A'));
        $this->assertFalse($accessUtils->hasRole($access, 'ROLE_B'));
    }

    /**
     * @see Utils::getAllRoles()
     */
    public function testGetAllRoles(): void
    {
        $accessUtils = $this->buildAccessUtilsMockForMethod('getAllRoles');

        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $access->method('getRoles')->willReturn(['ROLE_A']);

        $this->roleHierarchy->method('getReachableRoleNames')->willReturn(["ROLE_A", "ROLE_B"]);
        $this->optionalsRolesIterator->method('getOptionalsRoles')->willReturn(["ROLE_OPT"]);

        $this->assertEquals(['ROLE_A', 'ROLE_B'], $accessUtils->getAllRoles($access));
    }

    /**
     * @see Utils::findAdminFor()
     */
    public function testFindAdminFor(): void
    {
        $accessUtils = $this->buildAccessUtilsMockForMethod('findAdminFor');

        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
        $organization = $this->getMockBuilder(Organization::class)->disableOriginalConstructor()->getMock();

        $this->accessRepository->method('findOneBy')->with(['adminAccess' => true, 'organization' => $organization])->willReturn($access);

        $this->assertNotEmpty($accessUtils->findAdminFor($organization));
    }

    /**
     * @see Utils::findAdminFor()
     */
    public function testFindAdminForNotFound(): void
    {
        $accessUtils = $this->buildAccessUtilsMockForMethod('findAdminFor');

        $organization = $this->getMockBuilder(Organization::class)->disableOriginalConstructor()->getMock();

        $this->accessRepository->method('findOneBy')->with(['adminAccess' => true, 'organization' => $organization])->willReturn(null);

        $this->assertNull($accessUtils->findAdminFor($organization));
    }

    public function testGetActiveFunctions(): void {
        $accessUtils = $this->buildAccessUtilsMockForMethod('getActiveFunctions');

        DatesUtils::setFakeDatetime('2023-01-01 12:00');

        $access = $this->getMockBuilder(Access::class)->getMock();

        // Valide: pas de date de fin ou de début
        $functionType1 = $this->getMockBuilder(FunctionType::class)->getMock();
        $functionType1->method('getMission')->willReturn('Mission1');

        $function1 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
        $function1->method('getEndDate')->willReturn(null);
        $function1->method('getStartDate')->willReturn(null);
        $function1->method('getFunctionType')->willReturn($functionType1);

        // Valide: pas de date de fin ou de début
        $functionType2 = $this->getMockBuilder(FunctionType::class)->getMock();
        $functionType2->method('getMission')->willReturn('Mission2');

        $function2 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
        $function2->method('getEndDate')->willReturn(null);
        $function2->method('getStartDate')->willReturn(null);
        $function2->method('getFunctionType')->willReturn($functionType2);

        // Invalide: date de fin dépassée
        $functionType3 = $this->getMockBuilder(FunctionType::class)->getMock();
        $functionType3->method('getMission')->willReturn('Mission3');

        $function3 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
        $function3->method('getEndDate')->willReturn(new \DateTime('2022-01-01'));
        $function3->method('getStartDate')->willReturn(null);
        $function3->method('getFunctionType')->willReturn($functionType3);

        // Valide : date de fin dans les 24h suivant le jour même
        $functionType4 = $this->getMockBuilder(FunctionType::class)->getMock();
        $functionType4->method('getMission')->willReturn('Mission4');

        $function4 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
        $function4->method('getEndDate')->willReturn(new \DateTime('2023-01-01 18:00'));
        $function4->method('getStartDate')->willReturn(null);
        $function4->method('getFunctionType')->willReturn($functionType4);

        // Valide: date de fin dans le futur
        $functionType5 = $this->getMockBuilder(FunctionType::class)->getMock();
        $functionType5->method('getMission')->willReturn('Mission5');

        $function5 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
        $function5->method('getEndDate')->willReturn(new \DateTime('2023-06-01'));
        $function5->method('getStartDate')->willReturn(null);
        $function5->method('getFunctionType')->willReturn($functionType5);

        // Invalide: date de début dans le futur
        $functionType6 = $this->getMockBuilder(FunctionType::class)->getMock();
        $functionType6->method('getMission')->willReturn('Mission6');

        $function6 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
        $function6->method('getEndDate')->willReturn(null);
        $function6->method('getStartDate')->willReturn(new \DateTime('2023-06-01'));
        $function6->method('getFunctionType')->willReturn($functionType6);

        // Valide: date de début dans le passé
        $functionType7 = $this->getMockBuilder(FunctionType::class)->getMock();
        $functionType7->method('getMission')->willReturn('Mission7');

        $function7 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
        $function7->method('getEndDate')->willReturn(null);
        $function7->method('getStartDate')->willReturn(new \DateTime('2022-06-01'));
        $function7->method('getFunctionType')->willReturn($functionType7);

        // Valide: date de début dans le passé et date de fin dans le futur
        $functionType8 = $this->getMockBuilder(FunctionType::class)->getMock();
        $functionType8->method('getMission')->willReturn('Mission8');

        $function8 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
        $function8->method('getEndDate')->willReturn(new \DateTime('2023-06-01'));
        $function8->method('getStartDate')->willReturn(new \DateTime('2022-06-01'));
        $function8->method('getFunctionType')->willReturn($functionType8);

        $access->method('getOrganizationFunction')->willReturn(
            new ArrayCollection([$function1, $function2, $function3, $function4, $function5, $function6, $function7, $function8])
        );

        $this->assertEquals(
            ['Mission1', 'Mission2', 'Mission4', 'Mission5', 'Mission7', 'Mission8'],
            $accessUtils->getActiveFunctions($access)
        );
    }

    public function testHasActiveFunctions(): void {
        $accessUtils = $this->buildAccessUtilsMockForMethod('hasActiveFunction');

        $access = $this->getMockBuilder(Access::class)->getMock();

        $accessUtils
            ->method('getActiveFunctions')
            ->with($access)
            ->willReturn(['Mission1', 'Mission2']);

        $this->assertTrue(
            $accessUtils->hasActiveFunction($access, 'Mission1')
        );
        $this->assertFalse(
            $accessUtils->hasActiveFunction($access, 'Mission3')
        );
    }
}
