<?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\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\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());
    }


    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();
    }
}
