<?php declare(strict_types=1);


namespace Newland\Toubiz\Poi\Neos\Tests\Unit\Command;


use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Neos\ContentRepository\Domain\Repository\NodeDataRepository;
use Neos\Flow\Tests\FunctionalTestCase;
use Newland\NeosTestingHelpers\ChecksEntities;
use Newland\NeosTestingHelpers\InteractsWithNodes;
use Newland\Toubiz\Api\ObjectAdapter\ArticleAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\Concern\ExternalIdType;
use Newland\Toubiz\Sync\Neos\Command\IdentifierCommandController;
use Newland\Toubiz\Sync\Neos\Domain\Model\Article;
use Newland\Toubiz\Sync\Neos\Domain\Model\Category;
use Newland\Toubiz\Sync\Neos\Domain\Repository\ArticleRepository;
use Newland\Toubiz\Sync\Neos\Service\UrlIdentifierRedirectService;
use Newland\Toubiz\Sync\Neos\Tests\Factory\ArticleFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\AttributeFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\CategoryFactory;
use Ramsey\Uuid\Uuid;

class IdentifierCommandControllerTest extends FunctionalTestCase
{
    use InteractsWithNodes, ChecksEntities;
    protected static $testablePersistenceEnabled = true;

    /** @var IdentifierCommandController */
    private $subject;

    public function setUp(): void
    {
        parent::setUp();
        $this->subject = $this->objectManager->get(IdentifierCommandController::class);

        $entityManager = $this->objectManager->get(EntityManagerInterface::class);
        $entityManager->getConnection()->exec('PRAGMA foreign_keys = OFF');
    }

    public function testUpdatesUnstableIdentifiers(): void
    {
        $unstable = Uuid::uuid4()->toString();
        $entity = $this->getArticle($unstable);
        $shouldBe = $this->generatedIdentifierShouldBe($entity);
        $this->assertNotEquals($shouldBe, $unstable);

        $this->assertEntityExists(Article::class, $unstable, 'Entity with unstable identifier should still be in the database');
        $this->assertEntityNotExists(Article::class, $shouldBe, 'Entity with new, stable identifier should not be in the database yet');

        $this->callRegenerateCommand();

        $this->assertEntityNotExists(Article::class, $unstable, 'Entity with unstable identifier should not be in the database');
        $this->assertEntityExists(Article::class, $shouldBe, 'Entity with new, stable  identifier should be in the database');
    }

    public function testDoesNotUpdateStableIdentifiers(): void
    {
        $entity = $this->getArticle();

        $this->callRegenerateCommand();

        $repository = $this->objectManager->get(ArticleRepository::class);
        $this->assertNotNull($repository->findByIdentifier($entity->getPersistenceObjectIdentifier()));
    }

    public function testUpdatesManyToManyRelationships(): void
    {
        $unstable = Uuid::uuid4()->toString();
        $entity = $this->getArticle($unstable);
        $shouldBe = $this->generatedIdentifierShouldBe($entity);
        $this->assertNotEquals($shouldBe, $unstable);

        $category = (new CategoryFactory($this->objectManager))->create();
        $entity->setCategories(new ArrayCollection([ $category ]));
        $this->persistenceManager->update($entity);
        $this->persistenceManager->persistAll();
        $this->persistenceManager->clearState();

        $repository = $this->objectManager->get(ArticleRepository::class);
        $this->assertCount(1, $repository->findByIdentifier($unstable)->getCategories());

        $this->callRegenerateCommand();

        $repository = $this->objectManager->get(ArticleRepository::class);
        $this->assertCount(1, $repository->findByIdentifier($shouldBe)->getCategories());
    }

    public function testUpdatesOneToManyRelationships()
    {
        $unstable = Uuid::uuid4()->toString();
        $entity = $this->getArticle($unstable);
        $shouldBe = $this->generatedIdentifierShouldBe($entity);
        $this->assertNotEquals($shouldBe, $unstable);

        $attribute = (new AttributeFactory($this->objectManager))->create();
        $attribute->setArticle($entity);
        $this->persistenceManager->update($attribute);
        $this->persistenceManager->persistAll();
        $this->persistenceManager->clearState();

        $repository = $this->objectManager->get(ArticleRepository::class);
        $this->assertCount(1, $repository->findByIdentifier($unstable)->getAttributes());

        $this->callRegenerateCommand();

        $this->assertCount(1, $repository->findByIdentifier($shouldBe)->getAttributes());
    }

    public function testUpdatesSoftReferencesInNodeTextUsedByLinks(): void
    {
        $unstable = Uuid::uuid4()->toString();
        $entity = $this->getArticle($unstable);
        $shouldBe = $this->generatedIdentifierShouldBe($entity);
        $this->assertNotEquals($shouldBe, $unstable);

        $node = $this->initializeNode('/sites/my-site/node-abc/node-def');
        $node->setProperty(
            'text',
            sprintf('<a href="node:article:%s">My link</a>', $unstable)
        );
        $this->persistNode($node);
        $this->persistenceManager->clearState();

        $this->callRegenerateCommand();

        $repository = $this->objectManager->get(NodeDataRepository::class);
        $fromDb = $repository->findByNodeIdentifier($node->getIdentifier())->getFirst();
        $this->assertNotContains($unstable, $fromDb->getProperty('text'));
        $this->assertContains($shouldBe, $fromDb->getProperty('text'));
    }

    public function testUpdatesSoftReferencesInSimpleNodeProperties(): void
    {
        $unstable = Uuid::uuid4()->toString();
        $entity = $this->getArticle($unstable);
        $shouldBe = $this->generatedIdentifierShouldBe($entity);
        $this->assertNotEquals($shouldBe, $unstable);

        $node = $this->initializeNode('/sites/my-site/node-abc/node-def');
        $node->setProperty('article', $unstable);
        $this->persistNode($node);
        $this->persistenceManager->clearState();

        $this->callRegenerateCommand();

        $repository = $this->objectManager->get(NodeDataRepository::class);
        $fromDb = $repository->findByNodeIdentifier($node->getIdentifier())->getFirst();
        $this->assertNotEquals($unstable, $fromDb->getProperty('article'));
        $this->assertEquals($shouldBe, $fromDb->getProperty('article'));
    }

    public function testUpdatesSoftReferencesInArrayNodeProperties(): void
    {
        $unstable1 = Uuid::uuid4()->toString();
        $entity1 = $this->getArticle($unstable1);
        $shouldBe1 = $this->generatedIdentifierShouldBe($entity1);
        $this->assertNotEquals($shouldBe1, $unstable1);

        $unstable2 = Uuid::uuid4()->toString();
        $entity2 = $this->getArticle($unstable2);
        $shouldBe2 = $this->generatedIdentifierShouldBe($entity2);
        $this->assertNotEquals($shouldBe1, $unstable2);

        $node = $this->initializeNode('/sites/my-site/node-abc/node-def');
        $node->setProperty('articles', [ $unstable1, $unstable2 ]);
        $this->persistenceManager->update($node->getNodeData());
        $this->persistenceManager->persistAll();
        $this->persistenceManager->clearState();

        $this->callRegenerateCommand();

        $repository = $this->objectManager->get(NodeDataRepository::class);
        $fromDb = (array) $repository->findByNodeIdentifier($node->getIdentifier())->getFirst()->getProperty('articles');
        $this->assertNotContains($unstable1, $fromDb);
        $this->assertNotContains($unstable2, $fromDb);
        $this->assertContains($shouldBe1, $fromDb);
        $this->assertContains($shouldBe2, $fromDb);
    }

    public function testRemovesDuplicates(): void
    {
        // These two have different uuids in the beginning but will have the same uuid in the end.
        (new CategoryFactory($this->objectManager))->create([
            'persistenceObjectIdentifier' => Uuid::uuid4()->toString(),
            'originalId' => 'foo',
            'language' => 'de'
        ]);
        (new CategoryFactory($this->objectManager))->create([
             'persistenceObjectIdentifier' => Uuid::uuid4()->toString(),
             'originalId' => 'foo',
             'language' => 'de'
         ]);

        $this->assertEquals(2, $this->persistenceManager->createQueryForType(Category::class)->count());
        $this->callRegenerateCommand('category');
        $this->assertEquals(1, $this->persistenceManager->createQueryForType(Category::class)->count());
    }

    public function testMovesRelationshipsCorrectlyIfRemovingDuplicates(): void
    {
        $first = (new CategoryFactory($this->objectManager))->create([ 'persistenceObjectIdentifier' => Uuid::uuid4()->toString(), 'originalId' => 'foo', 'language' => 'de' ]);
        $second = (new CategoryFactory($this->objectManager))->create([ 'persistenceObjectIdentifier' => Uuid::uuid4()->toString(), 'originalId' => 'foo', 'language' => 'de' ]);
        $this->assertEquals($first->generateUuid(), $second->generateUuid());
        $targetUuid = $first->generateUuid();

        $relatedToFirst = (new ArticleFactory($this->objectManager))->create([ 'categories' => [ $first ] ]);
        $relatedToSecond = (new ArticleFactory($this->objectManager))->create([ 'categories' => [ $second ] ]);

        $this->callRegenerateCommand('category');

        $relatedToFirstFromDb = $this->persistenceManager->getObjectByIdentifier($relatedToFirst->getPersistenceObjectIdentifier(), Article::class);
        $relatedToSecondFromDb = $this->persistenceManager->getObjectByIdentifier($relatedToSecond->getPersistenceObjectIdentifier(), Article::class);
        $this->assertEquals($targetUuid, $relatedToFirstFromDb->getCategories()[0]->getPersistenceObjectIdentifier());
        $this->assertEquals($targetUuid, $relatedToSecondFromDb->getCategories()[0]->getPersistenceObjectIdentifier());
    }

    public function testDeduplicatesRelationshipsIfRemovingDuplicates(): void
    {
        $first = (new CategoryFactory($this->objectManager))->create([ 'persistenceObjectIdentifier' => Uuid::uuid4()->toString(), 'originalId' => 'foo', 'language' => 'de' ]);
        $second = (new CategoryFactory($this->objectManager))->create([ 'persistenceObjectIdentifier' => Uuid::uuid4()->toString(), 'originalId' => 'foo', 'language' => 'de' ]);
        $this->assertEquals($first->generateUuid(), $second->generateUuid());
        $targetUuid = $first->generateUuid();

        $relatedToBoth = (new ArticleFactory($this->objectManager))->create([ 'categories' => [ $first, $second ] ]);

        $this->callRegenerateCommand('category');

        $fromDb = $this->persistenceManager->getObjectByIdentifier($relatedToBoth->getPersistenceObjectIdentifier(), Article::class);
        $this->assertEquals(1, \count($fromDb->getCategories()));
        $this->assertEquals($targetUuid, $fromDb->getCategories()[0]->getPersistenceObjectIdentifier());
    }

    public function testMigratesOldArticlesWithExternalIdsPointingToNewOnes(): void
    {
        $toubizId = Uuid::uuid4()->toString();
        $new = (new ArticleFactory($this->objectManager))->create([
            'language' => 'de',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ,
            'originalId' => $toubizId
        ]);
        $old = (new ArticleFactory($this->objectManager))->create([
            'language' => 'de',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ_LEGACY,
            'externalIds' => [
                [ 'type' => ExternalIdType::TOUBIZ, 'id' => $toubizId ]
            ]
        ]);


        $node = $this->initializeNode('/sites/my-site/node-abc/node-def');
        $node->setProperty(
            'text',
            sprintf('<a href="node:article:%s">My link</a>', $old->getPersistenceObjectIdentifier())
        );
        $this->persistNode($node);
        $this->persistenceManager->clearState();

        $this->callMigrationCommand();

        $repository = $this->objectManager->get(NodeDataRepository::class);
        $fromDb = $repository->findByNodeIdentifier($node->getIdentifier())->getFirst();
        $this->assertStringContainsString($new->getPersistenceObjectIdentifier(), $fromDb->getProperty('text'));
        $this->assertStringNotContainsString($old->getPersistenceObjectIdentifier(), $fromDb->getProperty('text'));
    }

    public function testMigratesOldArticlesWithExternalIdsOnNewOnesPointingBack(): void
    {
        $legacyId = Uuid::uuid4()->toString();
        $new = (new ArticleFactory($this->objectManager))->create([
            'language' => 'de',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ,
            'externalIds' => [
                [ 'type' => ExternalIdType::TOUBIZ_LEGACY_REMOTE_ID, 'id' => $legacyId ]
            ]
        ]);
        $old = (new ArticleFactory($this->objectManager))->create([
            'language' => 'de',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ_LEGACY,
            'originalId' => $legacyId
        ]);


        $node = $this->initializeNode('/sites/my-site/node-abc/node-def');
        $node->setProperty(
            'text',
            sprintf('<a href="node:article:%s">My link</a>', $old->getPersistenceObjectIdentifier())
        );
        $this->persistNode($node);
        $this->persistenceManager->clearState();

        $this->callMigrationCommand();

        $repository = $this->objectManager->get(NodeDataRepository::class);
        $fromDb = $repository->findByNodeIdentifier($node->getIdentifier())->getFirst();
        $this->assertStringContainsString($new->getPersistenceObjectIdentifier(), $fromDb->getProperty('text'));
        $this->assertStringNotContainsString($old->getPersistenceObjectIdentifier(), $fromDb->getProperty('text'));
    }

    /** @dataProvider provideExternalIdTypes */
    public function testMigratesOldArticlesToNewOnesThatHaveTheSameExternalId(string $externalIdType): void
    {
        $externalId = Uuid::uuid4()->toString();
        $new = (new ArticleFactory($this->objectManager))->create([
            'language' => 'de',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ,
            'externalIds' => [ [ 'type' => $externalIdType, 'id' => $externalId ] ],
        ]);
        $old = (new ArticleFactory($this->objectManager))->create([
            'language' => 'de',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ_LEGACY,
            'externalIds' => [ [ 'type' => $externalIdType, 'id' => $externalId ] ],
        ]);

        $node = $this->initializeNode('/sites/my-site/node-abc/node-def');
        $node->setProperty(
            'text',
            sprintf('<a href="node:article:%s">My link</a>', $old->getPersistenceObjectIdentifier())
        );
        $this->persistNode($node);
        $this->persistenceManager->clearState();

        $this->callMigrationCommand();

        $repository = $this->objectManager->get(NodeDataRepository::class);
        $fromDb = $repository->findByNodeIdentifier($node->getIdentifier())->getFirst();
        $this->assertStringContainsString($new->getPersistenceObjectIdentifier(), $fromDb->getProperty('text'));
        $this->assertStringNotContainsString($old->getPersistenceObjectIdentifier(), $fromDb->getProperty('text'));
    }


    public function testRespectsLanguageWhenMigratingReferencedCategories(): void
    {
        $legacyId = Uuid::uuid4()->toString();
        $new = (new ArticleFactory($this->objectManager))->create([
            'language' => 'en',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ,
            'externalIds' => [ [ 'type' => ExternalIdType::TOUBIZ_LEGACY_REMOTE_ID, 'id' => $legacyId ] ]
        ]);
        $newInOtherLanguage = (new ArticleFactory($this->objectManager))->create([
            'language' => 'fr',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ,
            'externalIds' => [ [ 'type' => ExternalIdType::TOUBIZ_LEGACY_REMOTE_ID, 'id' => $legacyId ] ]
        ]);
        $old = (new ArticleFactory($this->objectManager))->create([
            'language' => 'en',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ_LEGACY,
            'originalId' => $legacyId
        ]);


        $node = $this->initializeNode('/sites/my-site/node-abc/node-def');
        $node->setProperty(
            'text',
            sprintf('<a href="node:article:%s">My link</a>', $old->getPersistenceObjectIdentifier())
        );
        $this->persistNode($node);
        $this->persistenceManager->clearState();

        $this->callMigrationCommand();

        $repository = $this->objectManager->get(NodeDataRepository::class);
        $fromDb = $repository->findByNodeIdentifier($node->getIdentifier())->getFirst();
        $this->assertStringContainsString($new->getPersistenceObjectIdentifier(), $fromDb->getProperty('text'));
        $this->assertStringNotContainsString($old->getPersistenceObjectIdentifier(), $fromDb->getProperty('text'));
        $this->assertStringNotContainsString($newInOtherLanguage->getPersistenceObjectIdentifier(), $fromDb->getProperty('text'));
    }


    public function provideExternalIdTypes(): array
    {
        return array_map(function($type) {
            return [ $type ];
        }, ExternalIdType::values());
    }

    public function testAddsRedirectForUrlIdentifier(): void
    {
        $legacyId = Uuid::uuid4()->toString();
        $new = (new ArticleFactory($this->objectManager))->create([
            'language' => 'de',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ,
            'externalIds' => [
                [ 'type' => ExternalIdType::TOUBIZ_LEGACY_REMOTE_ID, 'id' => $legacyId ]
            ],
            'urlIdentifier' => Uuid::uuid4()->toString(),
        ]);
        $old = (new ArticleFactory($this->objectManager))->create([
            'language' => 'de',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ_LEGACY,
            'originalId' => $legacyId,
            'urlIdentifier' => Uuid::uuid4()->toString(),
        ]);

        $newIdentifier = $new->getUrlIdentifier();
        $oldIdentifier = $old->getUrlIdentifier();

        $this->callMigrationCommand();

        $service = $this->objectManager->get(UrlIdentifierRedirectService::class);
        $ids = $service->getRedirects($oldIdentifier);

        $this->assertCount(2, $ids);
        $this->assertContains($newIdentifier, $ids);
    }

    /** @dataProvider provideCategoryPairs */
    public function testMigratesReferencedCategories(string $oldOriginalId, string $newOriginalId): void
    {
        $toubizOld = (new CategoryFactory($this->objectManager))->create([ 'originalId' => $oldOriginalId, 'language' => 'de' ]);
        $toubizNew = (new CategoryFactory($this->objectManager))->create([ 'originalId' => $newOriginalId, 'language' => 'de' ]);
        (new ArticleFactory($this->objectManager))->create([
            'categories' => [ $toubizNew ],
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ,
            'language' => 'de'
        ]);
        (new ArticleFactory($this->objectManager))->create([
            'categories' => [ $toubizOld ],
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ_LEGACY,
            'language' => 'de'
        ]);

        $node = $this->initializeNode('/sites/my-site/node-abc/node-def');
        $node->setProperty(
            'preselectedCategories',
            [ $toubizOld->getPersistenceObjectIdentifier() ]
        );
        $this->persistNode($node);
        $this->persistenceManager->clearState();

        $this->callMigrationCommand();

        $repository = $this->objectManager->get(NodeDataRepository::class);
        $fromDb = $repository->findByNodeIdentifier($node->getIdentifier())->getFirst();
        $this->assertNotContains($toubizOld->getPersistenceObjectIdentifier(), $fromDb->getProperty('preselectedCategories'));
        $this->assertContains($toubizNew->getPersistenceObjectIdentifier(), $fromDb->getProperty('preselectedCategories'));
    }

    public function provideCategoryPairs(): array
    {
        return [
            [ 'f9a7ee5922dee863a260c8d6f1c96c38' ,'85e2e4b4-e187-4e27-b4f3-fc9278a51b86' ],
            [ '8c50c4c7a0d79315ab98fecd2b3adb4d' ,'f52db48f-8b1c-380e-a658-5bf38f2afd7f' ],
            [ 'd814015c5c89dfb2b877922f4c2153f0', 'df9deb59-c6d6-44a2-8a3c-94b547b2364b' ],
            [ '5a9d22e544fd01fce89a9f7ebe38bf7f', '8b4f7afc-c1fe-34d4-91df-3d5a44ff417c' ],
        ];
    }

    private function getArticle(string $identifier = null): Article
    {
        $entity = (new ArticleFactory($this->objectManager))->make();
        if ($identifier) {
            $entity->setPersistenceObjectIdentifier($identifier);
        }
        $this->persistenceManager->add($entity);
        $this->persistenceManager->persistAll();
        return $entity;
    }

    private function generatedIdentifierShouldBe(Article $article): string
    {
        return $article->generateUuid()->toString();
    }

    private function callRegenerateCommand(string $type = 'article'): void
    {
        $this->subject->regenerateCommand($type, true);
        $this->persistenceManager->persistAll();
        $this->persistenceManager->clearState();
    }

    private function callMigrationCommand(): void
    {
        $this->subject->migrateArticlesToToubizNewCommand(true, true);
        $this->persistenceManager->persistAll();
        $this->persistenceManager->clearState();
    }
}
