<?php declare(strict_types=1);

namespace Newland\Toubiz\Poi\Neos\Tests\Unit\Search\Indexer;

use Neos\Flow\Tests\FunctionalTestCase;
use Newland\Contracts\Neos\Search\IndexRecordModification;
use Newland\Contracts\Neos\Search\SearchBackend;
use Newland\Contracts\Neos\Search\SearchRequest;
use Newland\Contracts\Neos\Search\SearchResultCollection;
use Newland\Toubiz\Api\ObjectAdapter\Concern\ArticleConstants;
use Newland\Toubiz\Poi\Neos\Search\Indexer\ArticleIndexer;
use Newland\Toubiz\Search\Neos\Progress\NullHandler;
use Newland\Toubiz\Sync\Neos\Tests\Factory\ArticleFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\AttributeFactory;

class ArticleIndexerTest extends FunctionalTestCase implements SearchBackend
{
    protected static $testablePersistenceEnabled = true;

    protected $subject;

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

    /** @var IndexRecordModification[] */
    protected $entries = [];

    public function setUp(): void
    {
        parent::setUp();
        $this->subject = $this->objectManager->get(ArticleIndexer::class);
        $this->articleFactory = new ArticleFactory($this->objectManager);
        $this->entries = [];
    }

    public function testIndexesArticle(): void
    {
        $this->articleFactory->create([
            'abstract' => '__SOME_ABSTRACT__',
            'description' => '__SOME_DESCRIPTION__',
            'name' => '__SOME_NAME__',
        ]);
        $this->subject->index(['/sites/foo'], $this, new NullHandler());

        $this->assertCount(1, $this->entries);
        $this->assertContains('__SOME_ABSTRACT__', $this->entries[0]->getContent());
        $this->assertContains('__SOME_DESCRIPTION__', $this->entries[0]->getContent());
        $this->assertContains('__SOME_NAME__', $this->entries[0]->getContent());
    }

    public function testIndexesAdditionalSearchString(): void
    {
        $this->articleFactory->create([
          'attributes' => [
              (new AttributeFactory($this->objectManager))->make([ 'name' => 'additionalSearchString', 'data' => '__ADDITIONAL__' ]),
          ]
      ]);

        $this->subject->index(['/sites/foo'], $this, new NullHandler());

        $this->assertCount(1, $this->entries);
        $this->assertContains('__ADDITIONAL__', $this->entries[0]->getContent());
    }

    public function testIndexesMultipleAdditionalSearchStrings(): void
    {
        $this->articleFactory->create([
          'attributes' => [
              (new AttributeFactory($this->objectManager))->make([ 'name' => 'additionalSearchString', 'data' => '__ADDITIONAL__' ]),
              (new AttributeFactory($this->objectManager))->make([ 'name' => 'additionalSearchString', 'data' => '__ANOTHER__' ]),
          ]
      ]);

        $this->subject->index(['/sites/foo'], $this, new NullHandler());

        $this->assertCount(1, $this->entries);
        $this->assertContains('__ADDITIONAL__', $this->entries[0]->getContent());
        $this->assertContains('__ANOTHER__', $this->entries[0]->getContent());
    }

    public function testUsesAllGivenScopesIfNoClientFilterExists(): void
    {
        $this->inject($this->subject, 'clientFilter', []);
        $this->articleFactory->create([ 'mainType' => ArticleConstants::TYPE_TOUR, 'client' => 'first-client' ]);
        $this->articleFactory->create([ 'mainType' => ArticleConstants::TYPE_TOUR, 'client' => 'second-client' ]);

        $this->subject->index(['/sites/foo', '/sites/bar'], $this, new NullHandler());
        $this->assertCount(2, $this->entries);
        $this->assertContains('/sites/foo', $this->entries[0]->getScopes());
        $this->assertContains('/sites/bar', $this->entries[0]->getScopes());
        $this->assertContains('/sites/foo', $this->entries[1]->getScopes());
        $this->assertContains('/sites/bar', $this->entries[1]->getScopes());
    }

    public function testUsesClientFilterToFilterScopes(): void
    {
        // * /sites/foo only shows first-client
        // * /sites/bar only shows second-client
        // * /sites/baz has no configuration (= this page displays all articles)
        $this->inject($this->subject, 'clientFilter', [
            'foo' => [ 'mainType' => [ 'values' => [ ArticleConstants::TYPE_TOUR => 'first-client' ] ] ],
            'bar' => [ 'mainType' => [ 'values' => [ ArticleConstants::TYPE_TOUR => 'second-client' ] ] ],
        ]);
        $this->articleFactory->create([ 'mainType' => ArticleConstants::TYPE_TOUR, 'client' => 'first-client' ]);
        $this->articleFactory->create([ 'mainType' => ArticleConstants::TYPE_TOUR, 'client' => 'second-client' ]);

        $this->subject->index(['/sites/foo', '/sites/bar', '/sites/baz'], $this, new NullHandler());
        $this->assertCount(2, $this->entries);

        $this->assertContains('/sites/foo', $this->entries[0]->getScopes());
        $this->assertNotContains('/sites/bar', $this->entries[0]->getScopes());
        $this->assertContains('/sites/baz', $this->entries[1]->getScopes());

        $this->assertNotContains('/sites/foo', $this->entries[1]->getScopes());
        $this->assertContains('/sites/bar', $this->entries[1]->getScopes());
        $this->assertContains('/sites/baz', $this->entries[1]->getScopes());
    }

    public function testDoesNotIndexArticleThatWouldHaveNoScopes(): void
    {
        $this->inject($this->subject, 'clientFilter', [
            'foo' => [ 'mainType' => [ 'values' => [ ArticleConstants::TYPE_TOUR => 'first-client' ] ] ],
            'bar' => [ 'mainType' => [ 'values' => [ ArticleConstants::TYPE_TOUR => 'second-client' ] ] ],
        ]);

        // Third item would never be shown since there is no page for it.
        $this->articleFactory->create([ 'mainType' => ArticleConstants::TYPE_TOUR, 'client' => 'first-client' ]);
        $this->articleFactory->create([ 'mainType' => ArticleConstants::TYPE_TOUR, 'client' => 'second-client' ]);
        $this->articleFactory->create([ 'mainType' => ArticleConstants::TYPE_TOUR, 'client' => 'third-client' ]);

        $this->subject->index(['/sites/foo', '/sites/bar'], $this, new NullHandler());
        $this->assertCount(2, $this->entries);
    }

    public function testAppliesClientFilterPerArticleType(): void
    {
        // Site foo should only contain TOURS of first-client, but can contain other article types of other clients.
        // Site bar has no restrictions
        // -> Both articles should be indexed for both pages
        $this->inject($this->subject, 'clientFilter', [
            'foo' => [ 'mainType' => [ 'values' => [ ArticleConstants::TYPE_TOUR => 'first-client' ] ] ],
        ]);

        $this->articleFactory->create([ 'mainType' => ArticleConstants::TYPE_TOUR, 'client' => 'first-client' ]);
        $this->articleFactory->create([ 'mainType' => ArticleConstants::TYPE_CITY, 'client' => 'second-client' ]);

        $this->subject->index(['/sites/foo', '/sites/bar'], $this, new NullHandler());
        $this->assertCount(2, $this->entries);

        $this->assertContains('/sites/foo', $this->entries[0]->getScopes());
        $this->assertContains('/sites/bar', $this->entries[0]->getScopes());

        $this->assertContains('/sites/foo', $this->entries[1]->getScopes());
        $this->assertContains('/sites/bar', $this->entries[1]->getScopes());
    }

    private function isZeroIndexedAndSequential($arr): bool {
        return array_keys($arr) === range(0, count($arr) - 1);
    }

    /** @dataProvider provideScopes */
    public function testFilteredScopesAreZeroIndexedAndSequential(array $scope): void
    {
        $this->articleFactory->create();
        $this->subject->index($scope, $this, new NullHandler());

        foreach ($this->entries as $entry) {
            $this->assertTrue($this->isZeroIndexedAndSequential($entry->getScopes()));
        }
    }

    public function provideScopes(): array
    {
        return [
            "sequential scope" => [['/sites/foo', '/sites/bar', '/sites/baz']],
            "non-sequential scope" => [[0 => '/sites/foo', 2 => '/sites/bar', 5 => '/sites/baz']],
        ];
    }

    // Mock implementation of search backend
    public function setSource(string $source): void
    {
        // Mock implementation of SearchBackend
    }

    public function createOrUpdateIndexEntry(IndexRecordModification $modification): void
    {
        $this->entries[] = $modification;
    }

    public function afterIndexing(): void
    {
        // Mock implementation of SearchBackend
    }


    public function deleteObsoleteIndexEntries(array $identifiersToRetain): void
    {
        // Mock implementation of SearchBackend
    }

    public function search(SearchRequest $request): SearchResultCollection
    {
        // Mock implementation of SearchBackend
    }

    public function initialize(): void
    {
        // Mock implementation of SearchBackend
    }
}
