<?php declare(strict_types=1);

namespace Newland\Toubiz\Sync\Neos\Tests\Unit\Domain\Repository;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Neos\Flow\Tests\FunctionalTestCase;
use Newland\Toubiz\Api\ObjectAdapter\Concern\ArticleConstants;
use Newland\Toubiz\Sync\Neos\Domain\Filter\ArticleFilter;
use Newland\Toubiz\Sync\Neos\Domain\Model\Article;
use Newland\Toubiz\Sync\Neos\Domain\Repository\ArticleRepository;
use Newland\Toubiz\Sync\Neos\Tests\Factory\ArticleFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\CategoryFactory;
use Ramsey\Uuid\Uuid;

class ArticleRepositoryTest extends FunctionalTestCase
{
    protected static $testablePersistenceEnabled = true;

    /** @var ArticleFactory */
    protected $factory;

    /** @var ArticleRepository */
    protected $subject;

    public function setUp(): void
    {
        parent::setUp();
        $this->factory = new ArticleFactory($this->objectManager);
        $this->subject = $this->objectManager->get(ArticleRepository::class);


        $categoryFactory = new CategoryFactory($this->objectManager);
        $categoryOne = $categoryFactory->create();
        $categoryTwo = $categoryFactory->create();
        $categoryThree = $categoryFactory->create();

        $articleFactory = new ArticleFactory($this->objectManager);
        $articleFactory->create();
        $articleFactory->create(
            [
                'categories' => new ArrayCollection(
                    [
                        $categoryOne,
                        $categoryTwo,
                        $categoryThree,
                    ]
                ),
            ]
        );

        $this->subject = $this->objectManager->get(ArticleRepository::class);
        $this->subject->setStrictLanguageHandling(false);
        $this->subject->setLanguage('de');
    }

    public function testFindOneByOriginalIdAndClientRespectsLanguages(): void
    {
        $german = $this->factory->create([ 'language' => 'de', 'client' => 'foo', 'originalId' => 'bar' ]);
        $english = $this->factory->create([ 'language' => 'en', 'client' => 'foo', 'originalId' => 'bar' ]);

        $this->subject->setLanguage('de');
        $result = $this->subject->findOneByOriginalIdAndClient('bar', 'foo');
        $this->assertNotNull($result);
        $this->assertEquals($german->getPersistenceObjectIdentifier(), $result->getPersistenceObjectIdentifier());

        $this->subject->setLanguage('en');
        $result = $this->subject->findOneByOriginalIdAndClient('bar', 'foo');
        $this->assertNotNull($result);
        $this->assertEquals($english->getPersistenceObjectIdentifier(), $result->getPersistenceObjectIdentifier());
    }

    /** @dataProvider provideEmptyClients */
    public function testFindOneByOriginalIdAndClientIgnoresEmptyClientValues($client)
    {
        $this->subject->setLanguage('de');
        $article = $this->factory->create([ 'language' => 'de', 'originalId' => 'bar' ]);
        $article->setClient($client);
        $this->objectManager->get(EntityManagerInterface::class)->persist($article);
        $this->objectManager->get(EntityManagerInterface::class)->flush($article);

        $result = $this->subject->findOneByOriginalIdAndClient('bar', 'someclientstring');
        $this->assertNotNull($result);
        $this->assertEquals($article->getPersistenceObjectIdentifier(), $result->getPersistenceObjectIdentifier());
    }

    /** @dataProvider provideEmptyClients */
    public function testFindOneByOriginalIdPrefersArticleWithClientMatchOverEmptyClientValue($client): void
    {
        $clientMatch =
            $this->factory->create([ 'language' => 'de', 'originalId' => 'bar', 'client' => 'sometestclient' ]);
        $this->subject->setLanguage('de');
        $withEmptyClient = $this->factory->create([ 'language' => 'de', 'originalId' => 'bar' ]);
        $withEmptyClient->setClient($client);
        $this->objectManager->get(EntityManagerInterface::class)->persist($withEmptyClient);
        $this->objectManager->get(EntityManagerInterface::class)->flush($withEmptyClient);

        $result = $this->subject->findOneByOriginalIdAndClient('bar', 'sometestclient');
        $this->assertNotNull($result);
        $this->assertEquals($clientMatch->getPersistenceObjectIdentifier(), $result->getPersistenceObjectIdentifier());
    }

    public function provideEmptyClients(): array
    {
        return [
            [ null ],
            [ '' ],
            [ 'default' ],
        ];
    }


    public function testQueryForOldRecordsRespectsLanguages(): void
    {
        $german = $this->factory->create([ 'language' => 'de', 'updatedAt' => new \DateTime('2019-01-01T13:00:00') ]);
        $english = $this->factory->create([ 'language' => 'en', 'updatedAt' => new \DateTime('2019-01-01T13:00:00') ]);

        $this->subject->setLanguage('de');
        $result = $this->subject->queryForOldRecords(
            new \DateInterval('PT2H'),
            null,
            new \DateTimeImmutable('2019-01-01T19:00:00')
        )->delete()->getQuery()->execute();

        $this->assertEquals(1, $result);
        $this->subject->setLanguage('de');
        $germanFromDatabase = $this->subject->findByIdentifier($german->getPersistenceObjectIdentifier());
        $this->assertTrue($germanFromDatabase === null, 'German should have been deleted');

        $this->subject->setLanguage('en');
        $englishFromDatabase = $this->subject->findByIdentifier($english->getPersistenceObjectIdentifier());
        $this->assertNotNull($englishFromDatabase, 'English should have not been deleted');
    }

    public function testOrphanFindingRespectsLanguages(): void
    {
        // Both are made orphans by having no client
        $german = $this->factory->create([ 'language' => 'de' ]);
        $german->setClient(null);
        $this->subject->update($german);
        $english = $this->factory->create([ 'language' => 'en' ]);
        $english->setClient(null);
        $this->subject->update($english);
        $this->persistenceManager->persistAll();

        $this->subject->setLanguage('de');
        $result = $this->subject->orphanQuery()->getQuery()->execute();
        $ids = array_map(
            function (Article $article) {
                return $article->getPersistenceObjectIdentifier();
            },
            (array) $result
        );

        $this->assertContains($german->getPersistenceObjectIdentifier(), $ids, 'Should have found german');
        $this->assertNotContains($english->getPersistenceObjectIdentifier(), $ids, 'Should not have found english');
    }

    public function testRemoveByIdsShouldRespectLanguages(): void
    {
        $german = $this->factory->create([ 'language' => 'de' ]);
        $english = $this->factory->create([ 'language' => 'en' ]);

        $this->subject->setLanguage('en');
        $this->subject->removeByIds(
            [ $german->getPersistenceObjectIdentifier(), $english->getPersistenceObjectIdentifier() ]
        );

        $this->subject->setLanguage('de');
        $germanFromDatabase = $this->subject->findByIdentifier($german->getPersistenceObjectIdentifier());
        $this->assertNotNull($germanFromDatabase, 'German article should not have been deleted');

        $this->subject->setLanguage('en');
        $englishFromDatabase = $this->subject->findByIdentifier($english->getPersistenceObjectIdentifier());
        $this->assertNull($englishFromDatabase, 'English article should have been deleted');
    }


    public function testFilterFindsArticlesWithNoCategory(): void
    {
        $result = $this->subject->countByFilter(new ArticleFilter(), 10);
        $this->assertEquals(2, $result['items']);
    }

    public function testDataSourceListsEveryOptionOnlyUnderMainCategoryOrDefaultGroupIfNoCategoryAttached(): void
    {
        $this->assertCount(2, $this->subject->findAllForDataSource(new ArticleFilter()));
    }

    public function testDataSourceSortsByName(): void
    {
        // We use our own articles for this test
        $this->subject->removeAll();
        $this->persistenceManager->persistAll();

        $categoryA = (new CategoryFactory($this->objectManager))->create([ 'title' => 'AAA' ]);
        $categoryB = (new CategoryFactory($this->objectManager))->create([ 'title' => 'BBB' ]);
        (new ArticleFactory($this->objectManager))->create([ 'mainCategory' => $categoryA, 'name' => 'AAA' ]);
        (new ArticleFactory($this->objectManager))->create([ 'mainCategory' => $categoryA, 'name' => 'BBB' ]);
        (new ArticleFactory($this->objectManager))->create([ 'mainCategory' => $categoryA, 'name' => 'CCC' ]);
        (new ArticleFactory($this->objectManager))->create([ 'mainCategory' => $categoryB, 'name' => 'AAA' ]);
        (new ArticleFactory($this->objectManager))->create([ 'mainCategory' => $categoryB, 'name' => 'BBB' ]);
        (new ArticleFactory($this->objectManager))->create([ 'mainCategory' => $categoryB, 'name' => 'CCC' ]);

        $filter = new ArticleFilter();
        $filter->setCategories(
            [ $categoryA->getPersistenceObjectIdentifier(), $categoryB->getPersistenceObjectIdentifier() ]
        );
        $result = $this->subject->findAllForDataSource($filter);

        // Data source should be sorted by article name
        $this->assertEquals([ 'AAA', 'AAA', 'BBB', 'BBB', 'CCC', 'CCC' ], array_column($result, 'label'));

        // And not by category
        $this->assertEquals([ 'AAA', 'BBB', 'AAA', 'BBB', 'AAA', 'BBB' ], array_column($result, 'group'));
    }

    /**
     * @dataProvider provideArticleTypes
     * @param int $type
     */
    public function testEmptyFilterFindsArticlesOfAllTypes(int $type): void
    {
        $article = (new ArticleFactory($this->objectManager))->create([ 'mainType' => $type ]);
        $ids = array_map(
            function (Article $article) {
                return $article->getPersistenceObjectIdentifier();
            },
            $this->subject->findByFilter(new ArticleFilter())
        );
        $this->assertContains($article->getPersistenceObjectIdentifier(), $ids);
    }


    /**
     * @dataProvider provideArticleTypes
     * @param int $type
     */
    public function testFilterForArticleTypeFindsArticlesOfThatType(int $type): void
    {
        $article = (new ArticleFactory($this->objectManager))->create([ 'mainType' => $type ]);

        $filter = new ArticleFilter();
        $filter->setMainType($type);
        $ids = array_map(
            function (Article $article) {
                return $article->getPersistenceObjectIdentifier();
            },
            $this->subject->findByFilter($filter)
        );
        $this->assertContains($article->getPersistenceObjectIdentifier(), $ids);
    }

    public function provideArticleTypes(): array
    {
        return array_map(
            function (int $type) {
                return [ $type ];
            },
            ArticleConstants::ALL_TYPES
        );
    }

    public function testFindAllDoesNotFindHidden(): void
    {
        [ $visible, $hidden ] = $this->createHiddenAndRegular();

        $ids = [];
        foreach ($this->subject->findAll() as $article) {
            $ids[] = $article->getPersistenceObjectIdentifier();
        }

        $this->assertContains($visible->getPersistenceObjectIdentifier(), $ids);
        $this->assertNotContains($hidden->getPersistenceObjectIdentifier(), $ids);
    }

    public function testFindByIdentifierDoesNotFindHidden(): void
    {
        [ $visible, $hidden ] = $this->createHiddenAndRegular();
        $this->assertFalse(
            null === $this->subject->findByIdentifier($visible->getPersistenceObjectIdentifier()),
            'visible should be found'
        );
        $this->assertTrue(
            null === $this->subject->findByIdentifier($hidden->getPersistenceObjectIdentifier()),
            'hidden should not be found'
        );
    }

    public function testMagicFindByMethodsDoNotFindHidden(): void
    {
        [ $visible, $hidden ] = $this->createHiddenAndRegular();

        $result = $this->subject->findByName($visible->getName());
        $ids = array_map(
            function (Article $article) {
                return $article->getPersistenceObjectIdentifier();
            },
            (array) $result
        );
        $this->assertContains($visible->getPersistenceObjectIdentifier(), $ids);

        $result = $this->subject->findByName($hidden->getName());
        $ids = array_map(
            function (Article $article) {
                return $article->getPersistenceObjectIdentifier();
            },
            (array) $result
        );
        $this->assertNotContains($hidden->getPersistenceObjectIdentifier(), $ids);
    }

    public function testMagicFindOneByMethodsDoNotFindHidden(): void
    {
        [ $visible, $hidden ] = $this->createHiddenAndRegular();
        $this->assertFalse(null === $this->subject->findOneByName($visible->getName()), 'visible should be found');
        $this->assertTrue(null === $this->subject->findOneByName($hidden->getName()), 'hidden should not be found');
    }

    public function testUsingQueryBuilderManuallyDoesNotFindHidden(): void
    {
        [ $visible, $hidden ] = $this->createHiddenAndRegular();

        $query = $this->subject->createQueryBuilder('article');
        $query->where(
            $query->expr()->orX(
                $query->expr()->eq('article.name', ':visible'),
                $query->expr()->eq('article.name', ':hidden')
            )
        );
        $query->setParameters(
            [
                'visible' => $visible->getName(),
                'hidden' => $hidden->getName(),
            ]
        );

        $result = $query->getQuery()->execute();
        $ids = array_map(
            function (Article $article) {
                return $article->getPersistenceObjectIdentifier();
            },
            (array) $result
        );

        $this->assertContains($visible->getPersistenceObjectIdentifier(), $ids);
        $this->assertNotContains($hidden->getPersistenceObjectIdentifier(), $ids);
    }

    public function testDisablingLanguageHandlingDoesNotFindHidden(): void
    {
        [ $visible, $hidden ] = $this->createHiddenAndRegular();
        $result = $this->subject->withoutLanguageHandling(function () {
            return $this->subject->findAll();
        });

        $ids = [];
        foreach ($result as $article) {
            $ids[] = $article->getPersistenceObjectIdentifier();
        }

        $this->assertContains($visible->getPersistenceObjectIdentifier(), $ids);
        $this->assertNotContains($hidden->getPersistenceObjectIdentifier(), $ids);
    }

    /** @return Article[] */
    private function createHiddenAndRegular(): array
    {
        return [
            (new ArticleFactory($this->objectManager))->create(
                [
                    'name' => 'visible_' . md5(random_bytes(32)),
                    'language' => 'de',
                    'hidden' => false,
                ]
            ),
            (new ArticleFactory($this->objectManager))->create(
                [
                    'name' => 'hidden_' . md5(random_bytes(32)),
                    'language' => 'de',
                    'hidden' => true,
                ]
            ),
        ];
    }

    public function testCanFindLanguageVariants(): void
    {
        $de = (new ArticleFactory($this->objectManager))->create(
            [ 'language' => 'de', 'client' => 'foo', 'languageGrouping' => 'foo-bar' ]
        );
        $en = (new ArticleFactory($this->objectManager))->create(
            [ 'language' => 'en', 'client' => 'foo', 'languageGrouping' => 'foo-bar' ]
        );
        $fr = (new ArticleFactory($this->objectManager))->create(
            [ 'language' => 'fr', 'client' => 'foo', 'languageGrouping' => 'foo-bar' ]
        );

        $variants = $this->subject->findLanguageVariants($de);

        $this->assertArrayHasKey('de', $variants);
        $this->assertArrayHasKey('en', $variants);
        $this->assertArrayHasKey('fr', $variants);

        $this->assertEquals($de->getPersistenceObjectIdentifier(), $variants['de']->getPersistenceObjectIdentifier());
        $this->assertEquals($en->getPersistenceObjectIdentifier(), $variants['en']->getPersistenceObjectIdentifier());
        $this->assertEquals($fr->getPersistenceObjectIdentifier(), $variants['fr']->getPersistenceObjectIdentifier());
    }

    public function testDoesNotFindEmptyLanguageAsVariant(): void
    {
        $de = (new ArticleFactory($this->objectManager))->create(
            [ 'language' => 'de', 'client' => 'foo', 'languageGrouping' => 'foo-bar' ]
        );
        $en = (new ArticleFactory($this->objectManager))->create(
            [ 'language' => 'en', 'client' => 'foo', 'languageGrouping' => 'foo-bar' ]
        );
        $empty = (new ArticleFactory($this->objectManager))->create(
            [ 'language' => null, 'client' => 'foo', 'languageGrouping' => 'foo-bar' ]
        );

        $variants = $this->subject->findLanguageVariants($de);
        $this->assertCount(2, $variants);
        $this->assertArrayHasKey('de', $variants);
        $this->assertArrayHasKey('en', $variants);
    }
}
