<?php declare(strict_types=1);

namespace Newland\Toubiz\Sync\Neos\Tests\Unit\Domain\Model;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Neos\Flow\Tests\FunctionalTestCase;
use Newland\Toubiz\Api\ObjectAdapter\Concern\ArticleConstants;
use Newland\Toubiz\Sync\Neos\Domain\Model\Article;
use Newland\Toubiz\Sync\Neos\Domain\Model\Attribute;
use Newland\Toubiz\Sync\Neos\Domain\Model\CityData;
use Newland\Toubiz\Sync\Neos\Domain\Model\File;
use Newland\Toubiz\Sync\Neos\Domain\Repository\ArticleRepository;
use Newland\Toubiz\Sync\Neos\Enum\ArticleType;
use Newland\Toubiz\Sync\Neos\Tests\Factory\ArticleFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\AttributeFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\CityDataFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\FileFactory;

class ArticleTest extends FunctionalTestCase
{
    public static $testablePersistenceEnabled = true;

    /** @var Article */
    protected $article;

    /** @var Article */
    protected $fromDatabase;

    public function setUp()
    {
        parent::setUp();

        $factory = new ArticleFactory($this->objectManager);
        $this->article = $factory->create();

        /** @var ArticleRepository $repository */
        $repository = $this->objectManager->get(ArticleRepository::class);
        $this->fromDatabase = $repository->find($this->article->getPersistenceObjectIdentifier());
    }

    public function testCorrectlySavesBasicColumn(): void
    {
        $this->assertInstanceOf(Article::class, $this->fromDatabase);

        // Note: Not using data provider like a normal person to prevent overhead of setting up &
        //       tearing down the db for every tested column.
        foreach ($this->basicColumnsDataProvider() as [ $columnName ]) {
            $getter = 'get' . ucfirst($columnName);
            $this->assertTrue(
                method_exists($this->article, $getter),
                sprintf('Article should have method %s',  $getter)
            );
            $this->assertTrue(
                method_exists($this->fromDatabase, $getter),
                sprintf('Article from db should have method %s',  $getter)
            );
            $this->assertEquals(
                $this->article->{$getter}(),
                $this->fromDatabase->{$getter}(),
                sprintf('Column %s (getter %s) should be the same after retrieving from database', $columnName, $getter)
            );
        }
    }

    /**
     * @dataProvider provideDeterministicIds
     * @param string $expectedId
     * @param array $properties
     */
    public function testDeterministicIdsAreGeneratedCorrectly(string $expectedId, array $properties): void
    {
        $this->assertEquals(
            $expectedId,
            (new ArticleFactory($this->objectManager))->make($properties)->generateUuid()->toString(),
            'persistence object identifier should be stable: If this test fails then the'
            . ' way ids are being generated has probably changed and old identifiers need to be migrated'
            . ' to this new style.'
        );
    }

    /**
     * @dataProvider provideDeterministicIds
     * @param string $expectedId
     * @param array $properties
     */
    public function testDeterministicIdsAreAutomaticallyGeneratedWhenStoringInDatabase(string $expectedId, array $properties): void
    {
        /** @var Article $article */
        $article = (new ArticleFactory($this->objectManager))->create($properties);

        $this->assertEquals(
            $expectedId,
            $article->getPersistenceObjectIdentifier(),
            'persistence object identifier should be stable: If this test fails then the'
            . ' way ids are being generated has probably changed and old identifiers need to be migrated'
            . ' to this new style.'
        );

        // Article is explicitly deleted to avoid id collisions.
        $this->persistenceManager->remove($article);
        $this->persistenceManager->persistAll();
    }

    public function provideDeterministicIds(): array
    {
        return [
            'baseArticle' => [
                '13596d55-f9ee-5a75-8287-caa4ee3bf501',
                [
                    'mainType' => ArticleConstants::TYPE_ATTRACTION,
                    'language' => 'de',
                    'client' => 'foobar',
                    'originalId' => 'lalala',
                    'name' => 'First name'
                ]
            ],
            'sameAsBaseButDifferentName: Should have same identifier' => [
                '13596d55-f9ee-5a75-8287-caa4ee3bf501',
                [
                    'mainType' => ArticleConstants::TYPE_ATTRACTION,
                    'language' => 'de',
                    'client' => 'foobar',
                    'originalId' => 'lalala',
                    'name' => 'Same basic article as before but with different name.'
                ]
            ],
            'mainTypeChanged' => [
                '70c0cc9e-17ae-52fa-939a-71591d53f482',
                [
                    'mainType' => ArticleConstants::TYPE_CITY,
                    'language' => 'de',
                    'client' => 'foobar',
                    'originalId' => 'lalala',
                ]
            ],
            'languageChanged' => [
                'bbfaacf8-8659-5b9c-a72d-5be8c65d3b8c',
                [
                    'mainType' => ArticleConstants::TYPE_ATTRACTION,
                    'language' => 'en',
                    'client' => 'foobar',
                    'originalId' => 'lalala',
                ]
            ],
            'clientChanged' => [
                '7905042e-884a-5a5a-b925-2754668d59e2',
                [
                    'mainType' => ArticleConstants::TYPE_ATTRACTION,
                    'language' => 'de',
                    'client' => 'nanas-funworld',
                    'originalId' => 'lalala',
                ]
            ],
            'originalIdChanged' => [
                'a5c451cc-baf2-5ec5-81b3-aae4cee5199d',
                [
                    'mainType' => ArticleConstants::TYPE_ATTRACTION,
                    'language' => 'de',
                    'client' => 'foobar',
                    'originalId' => 'blablabla',
                ]
            ],
            'differentPropertiesAllTogether' => [
                'fd80bc48-d8fc-55c9-b140-d5029e3eeeea',
                [
                    'mainType' => ArticleConstants::TYPE_DIRECT_MARKETER,
                    'language' => 'fr',
                    'client' => 'another-client',
                    'originalId' => 'foooooo',
                ]
            ],
        ];
    }

    public function basicColumnsDataProvider(): array
    {
        return [
            [ 'mainType' ],
            [ 'name' ],
            [ 'abstract' ],
            [ 'description' ],
            [ 'client' ],
            [ 'facebookUri' ],
            [ 'instagramUri' ],
            [ 'youtubeUri' ],
            [ 'flickrUri' ],
            [ 'wikipediaUri' ],
            [ 'bookingUri' ],
            [ 'detailUri' ],
            [ 'starClassification' ],
            [ 'averageRating' ],
            [ 'numberOfRatings' ],
            [ 'updatedAt' ],
            [ 'sourceName' ],
            [ 'authorName' ],
        ];
    }

    private function fetchEntityFromDatabase(string $className, string $id)
    {
        return $this->objectManager->get(EntityManagerInterface::class)->find($className, $id);
    }

    public function testSavesCityDataWithArticle(): void
    {
        $article = $this->createArticleWithCityData();
        $cityDataId = $article->getCityData()->getPersistenceObjectIdentifier();

        $fromDb = $this->fetchEntityFromDatabase(CityData::class, $cityDataId);

        $this->assertNotNull($fromDb);
        $this->assertInstanceOf(CityData::class, $fromDb);
        $this->assertEquals($fromDb->getPersistenceObjectIdentifier(), $cityDataId);
    }

    public function testDeletesCityDataWhenDeletingArticle(): void
    {
        $article = $this->createArticleWithCityData();
        $cityDataId = $article->getCityData()->getPersistenceObjectIdentifier();
        $this->persistenceManager->remove($article);
        $this->persistenceManager->persistAll();
        $this->persistenceManager->clearState();

        $fromDb = $this->fetchEntityFromDatabase(CityData::class, $cityDataId);
        $this->assertNull($fromDb);
    }

    public function testDoesNotRemoveArticleWhenRemovingCityData()
    {
        $article = $this->createArticleWithCityData();
        $articleId = $article->getPersistenceObjectIdentifier();
        $this->persistenceManager->remove($article->getCityData());
        $this->persistenceManager->persistAll();
        $this->persistenceManager->clearState();

        $fromDb = $this->fetchEntityFromDatabase(Article::class, $articleId);
        $this->assertNotNull($fromDb);
    }

    private function createArticleWithCityData(): Article
    {
        /** @var Article $article */
        $article = (new ArticleFactory($this->objectManager))->create();
        $article->setCityData((new CityDataFactory($this->objectManager))->make());
        $this->persistenceManager->update($article);
        $this->persistenceManager->persistAll();
        return $article;
    }

    public function testSavesFilesWithArticle(): void
    {
        $article = $this->createArticleWithFile();
        $fileId = $article->getFiles()[0]->getPersistenceObjectIdentifier();

        $fromDb = $this->fetchEntityFromDatabase(File::class, $fileId);

        $this->assertNotNull($fromDb);
        $this->assertInstanceOf(File::class, $fromDb);
        $this->assertEquals($fromDb->getPersistenceObjectIdentifier(), $fileId);
    }

    public function testDeletesFilesWhenDeletingArticle(): void
    {
        $article = $this->createArticleWithFile();
        $fileId = $article->getFiles()[0]->getPersistenceObjectIdentifier();
        $this->persistenceManager->remove($article);
        $this->persistenceManager->persistAll();
        $this->persistenceManager->clearState();

        $fromDb = $this->fetchEntityFromDatabase(File::class, $fileId);
        $this->assertNull($fromDb);
    }

    private function createArticleWithFile(): Article
    {
        /** @var Article $article */
        $article = (new ArticleFactory($this->objectManager))->create();
        $file = (new FileFactory($this->objectManager))->make();
        $article->setFiles(new ArrayCollection([ $file ]));
        $this->persistenceManager->update($article);
        $this->persistenceManager->persistAll();
        return $article;
    }


    public function testSavesAttributesWithArticle(): void
    {
        $article = $this->createArticleWithAttribute();
        $attributeId = $article->getAttributes()[0]->getPersistenceObjectIdentifier();

        $fromDb = $this->fetchEntityFromDatabase(Attribute::class, $attributeId);

        $this->assertNotNull($fromDb);
        $this->assertInstanceOf(Attribute::class, $fromDb);
        $this->assertEquals($fromDb->getPersistenceObjectIdentifier(), $attributeId);
    }

    public function testDeletesAttributesWhenDeletingArticle(): void
    {
        $article = $this->createArticleWithAttribute();
        $attributeId = $article->getAttributes()[0]->getPersistenceObjectIdentifier();
        $this->persistenceManager->remove($article);
        $this->persistenceManager->persistAll();
        $this->persistenceManager->clearState();

        $fromDb = $this->fetchEntityFromDatabase(Attribute::class, $attributeId);
        $this->assertNull($fromDb);
    }

    private function createArticleWithAttribute(): Article
    {
        /** @var Article $article */
        $article = (new ArticleFactory($this->objectManager))->create();
        $attribute = (new AttributeFactory($this->objectManager))->make();
        $article->setAttributes(new ArrayCollection([ $attribute ]));
        $this->persistenceManager->update($article);
        $this->persistenceManager->persistAll();
        return $article;
    }
}