<?php

namespace App\Tests\Unit\Service\Utils;

use App\Entity\Access\Access;
use App\Entity\Billing\BillingSetting;
use App\Entity\Billing\ResidenceArea;
use App\Entity\Education\EducationTiming;
use App\Entity\Organization\Organization;
use App\Service\Utils\EntityUtils;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\Persistence\Mapping\ClassMetadata;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class TestableEntityUtils extends EntityUtils
{
    public function getEntityNamesMappingCache(): array
    {
        return $this->entityNamesMappingCache;
    }

    public function setEntityNamesMappingCache(array $entityNamesMappingCache)
    {
        $this->entityNamesMappingCache = $entityNamesMappingCache;
    }

    public function organizationDefaultValue($entity, Access $access): void
    {
        parent::organizationDefaultValue($entity, $access);
    }

    public function billingSettingDefaultValueDefaultValue($entity, Access $access): void
    {
        parent::billingSettingDefaultValueDefaultValue($entity, $access);
    }

    public function populateEntityNamesMappingCache(): void
    {
        parent::populateEntityNamesMappingCache();
    }
}

class EntityUtilsTest extends TestCase
{
    private EntityManagerInterface $entityManager;

    public function setUp(): void
    {
        $this->entityManager = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
    }

    protected function getEntityUtilsMockFor(string $method): MockObject|TestableEntityUtils
    {
        return $this->getMockBuilder(TestableEntityUtils::class)
            ->setConstructorArgs([$this->entityManager])
            ->setMethodsExcept(['getEntityNamesMappingCache', 'setEntityNamesMappingCache', $method])
            ->getMock();
    }

    /**
     * @see EntityUtils::defaultValueSettersByAccess()
     */
    public function testDefaultValueSettersByAccess(): void
    {
        $utils = $this->getEntityUtilsMockFor('defaultValueSettersByAccess');

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

        $utils->expects(self::once())->method('organizationDefaultValue')->with($entity, $access);
        $utils->expects(self::once())->method('billingSettingDefaultValueDefaultValue')->with($entity, $access);

        $utils->defaultValueSettersByAccess($entity, $access);
    }

    /**
     * @see EntityUtils::organizationDefaultValue()
     */
    public function testOrganizationDefaultValue(): void
    {
        $utils = $this->getEntityUtilsMockFor('organizationDefaultValue');

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

        $entity = new EducationTiming();  // Can't test this method with a mocked entity...

        $access = $this->getMockBuilder(Access::class)->getMock();
        $access->method('getOrganization')->willReturn($organization);

        $utils->organizationDefaultValue($entity, $access);

        $this->assertEquals($organization, $entity->getOrganization());
    }

    /**
     * @see EntityUtils::billingSettingDefaultValueDefaultValue()
     */
    public function testBillingSettingDefaultValueDefaultValue(): void
    {
        $utils = $this->getEntityUtilsMockFor('billingSettingDefaultValueDefaultValue');

        $entity = new ResidenceArea();  // Can't test this method with a mocked entity...

        $billingSetting = $this->getMockBuilder(BillingSetting::class)->getMock();

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

        $access = $this->getMockBuilder(Access::class)->getMock();
        $access->method('getOrganization')->willReturn($organization);

        $utils->billingSettingDefaultValueDefaultValue($entity, $access);

        $this->assertEquals($billingSetting, $entity->getBillingSetting());
    }

    public function testGetEntityNameFromFullName(): void
    {
        $utils = $this->getEntityUtilsMockFor('getEntityNameFromFullName');

        $this->assertEquals(
            'File',
            $utils->getEntityNameFromFullName('\App\Entity\Core\File')
        );
        $this->assertEquals(
            'File',
            $utils->getEntityNameFromFullName('\File')
        );
        $this->assertEquals(
            'File',
            $utils->getEntityNameFromFullName('File')
        );
    }

    public function testGetFullNameFromEntityName(): void
    {
        $utils = $this->getEntityUtilsMockFor('getFullNameFromEntityName');

        $utils->setEntityNamesMappingCache(['File' => '\App\Entity\Core\File']);

        $utils->expects(self::never())->method('populateEntityNamesMappingCache');

        $this->assertEquals(
            '\App\Entity\Core\File',
            $utils->getFullNameFromEntityName('File')
        );
    }

    public function testGetFullNameFromEntityNameEmptyCache(): void
    {
        $utils = $this->getEntityUtilsMockFor('getFullNameFromEntityName');

        $utils->setEntityNamesMappingCache([]);

        $utils
            ->expects(self::once())
            ->method('populateEntityNamesMappingCache')
            ->willReturnCallback(function () use ($utils) {
                $utils->setEntityNamesMappingCache(['File' => '\App\Entity\Core\File']);
            })
        ;

        $this->assertEquals(
            '\App\Entity\Core\File',
            $utils->getFullNameFromEntityName('File')
        );
    }

    public function testGetFullNameFromEntityNameNonExistingEntity(): void
    {
        $utils = $this->getEntityUtilsMockFor('getFullNameFromEntityName');

        $utils->setEntityNamesMappingCache(['File' => '\App\Entity\Core\File']);

        $utils->expects(self::never())->method('populateEntityNamesMappingCache');

        $this->expectException(\LogicException::class);
        $this->expectExceptionMessage('No entity found for name `Foo`');

        $utils->getFullNameFromEntityName('Foo');
    }

    public function testPopulateEntityNamesMappingCache(): void
    {
        $utils = $this->getEntityUtilsMockFor('populateEntityNamesMappingCache');

        $metadataFactory = $this->getMockBuilder(ClassMetadataFactory::class)->getMock();

        $this->entityManager
            ->expects(self::once())
            ->method('getMetadataFactory')
            ->willReturn($metadataFactory);

        $entityMetadata1 = $this->getMockBuilder(ClassMetadata::class)->getMock();
        $entityMetadata1->method('getName')->willReturn('/Namespace/Entity1');

        $entityMetadata2 = $this->getMockBuilder(ClassMetadata::class)->getMock();
        $entityMetadata2->method('getName')->willReturn('/Namespace/Entity2');

        $entityMetadata3 = $this->getMockBuilder(ClassMetadata::class)->getMock();
        $entityMetadata3->method('getName')->willReturn('/Namespace/Entity3');

        $metadataFactory
            ->method('getAllMetadata')
            ->willReturn([$entityMetadata1, $entityMetadata2, $entityMetadata3]);

        $utils->method('getEntityNameFromFullName')->willReturnMap(
            [
                ['/Namespace/Entity1', 'Entity1'],
                ['/Namespace/Entity2', 'Entity2'],
                ['/Namespace/Entity3', 'Entity3'],
            ]
        );

        $this->assertEquals(
            [],
            $utils->getEntityNamesMappingCache()
        );

        $utils->populateEntityNamesMappingCache();

        $this->assertEquals(
            [
                'Entity1' => '/Namespace/Entity1',
                'Entity2' => '/Namespace/Entity2',
                'Entity3' => '/Namespace/Entity3',
            ],
            $utils->getEntityNamesMappingCache()
        );
    }

    public function testPopulateEntityNamesMappingCacheWithExistingCache(): void
    {
        $utils = $this->getEntityUtilsMockFor('populateEntityNamesMappingCache');

        $metadataFactory = $this->getMockBuilder(ClassMetadataFactory::class)->getMock();

        $this->entityManager
            ->expects(self::once())
            ->method('getMetadataFactory')
            ->willReturn($metadataFactory);

        $entityMetadata1 = $this->getMockBuilder(ClassMetadata::class)->getMock();
        $entityMetadata1->method('getName')->willReturn('/Namespace/Entity');

        $metadataFactory
            ->method('getAllMetadata')
            ->willReturn([$entityMetadata1]);

        $utils->method('getEntityNameFromFullName')->willReturnMap(
            [
                ['/Namespace/Entity', 'Entity'],
            ]
        );

        $utils->setEntityNamesMappingCache(['OtherEntity' => '/Namespace/OtherEntity']);

        $this->assertEquals(
            ['OtherEntity' => '/Namespace/OtherEntity'],
            $utils->getEntityNamesMappingCache()
        );

        $utils->populateEntityNamesMappingCache();

        $this->assertEquals(
            ['Entity' => '/Namespace/Entity'],
            $utils->getEntityNamesMappingCache()
        );
    }

    public function testGetNamespaceFromName(): void
    {
        $utils = $this->getEntityUtilsMockFor('getNamespaceFromName');

        $this->assertEquals(
            'App\Entity\Core',
            $utils->getNamespaceFromName('\App\Entity\Core\File')
        );
        $this->assertEquals(
            '',
            $utils->getNamespaceFromName('\File')
        );
    }
}
