<?php

namespace Newland\Toubiz\Map\Neos\Tests\Unit\Serialization;

use Neos\Cache\Frontend\FrontendInterface;
use Neos\Cache\Frontend\VariableFrontend;
use Neos\ContentRepository\Domain\Model\Node;
use Neos\Flow\Cache\CacheManager;
use Neos\Flow\Reflection\ReflectionService;
use Neos\Flow\Tests\FunctionalTestCase;
use Neos\Neos\Domain\Model\Site;
use Newland\NeosTestingHelpers\InteractsWithNodes;
use Newland\Toubiz\Api\ObjectAdapter\Concern\ArticleConstants;
use Newland\Toubiz\Map\Neos\Provider\Contract\FilterItem;
use Newland\Toubiz\Map\Neos\Provider\Contract\Marker;
use Newland\Toubiz\Map\Neos\Provider\Contract\ProviderContext;
use Newland\Toubiz\Map\Neos\Provider\MapDataProviderService;
use Newland\Toubiz\Map\Neos\Tests\Unit\Serialization\Mocks\CacheManagerMock;
use Newland\Toubiz\Map\Neos\Tests\Unit\Serialization\Mocks\MockProvider;
use Newland\Toubiz\Map\Neos\Tests\Unit\Serialization\Mocks\TransientMemoryBackend;
use Newland\Toubiz\Sync\Neos\Tests\Factory\ArticleFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\AttributeFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\CategoryFactory;

class MapDataProviderServiceTest extends FunctionalTestCase
{
    protected static $testablePersistenceEnabled = true;
    use InteractsWithNodes;

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

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

    /** @var AttributeFactory */
    protected $attributeFactory;

    /** @var CategoryFactory */
    protected $categoryFactory;

    /** @var Node[] */
    protected $nodes;

    /** @var Site */
    protected $site;

    /** @var Node */
    protected $mapNode;

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

        $this->subject = $this->objectManager->get(MapDataProviderService::class);

        $this->articleFactory = new ArticleFactory($this->objectManager);
        $this->attributeFactory = new AttributeFactory($this->objectManager);
        $this->categoryFactory = new CategoryFactory($this->objectManager);

        $this->site = $this->initializeSite('foo');
        $this->nodes = $this->nodes(
            [
                '/sites/foo' => [ 'nodeType' => 'Neos.NodeTypes:Page' ],
                '/sites/foo/main' => [ 'nodeType' => 'Neos.Neos:ContentCollection' ],
                '/sites/foo/main/map' => [ 'nodeType' => 'Newland.Toubiz.Map.Neos:Map' ],
                '/sites/foo/main/map/children' => [ 'nodeType' => 'Neos.Neos:ContentCollection' ],
                '/sites/foo/main/map/children/first-filter-item' => [
                    'nodeType' => 'Newland.Toubiz.Map.Neos:Map.FilterItem',
                    'properties' => [ 'title' => 'First Title' ],
                ],
                '/sites/foo/main/map/children/first-filter-item/children' => [ 'nodeType' => 'Neos.Neos:ContentCollection' ],
                '/sites/foo/main/map/children/second-filter-item' => [
                    'nodeType' => 'Newland.Toubiz.Map.Neos:Map.FilterItem',
                    'properties' => [ 'title' => 'Second Title' ],
                ],
                '/sites/foo/main/map/children/second-filter-item/children' => [ 'nodeType' => 'Neos.Neos:ContentCollection' ],
                '/sites/foo/main/map/children/first-filter-item/children/deep-filter-item' => [
                    'nodeType' => 'Newland.Toubiz.Map.Neos:Map.FilterItem',
                    'properties' => [ 'title' => 'Deep Title' ],
                ],
                '/sites/foo/main/map/children/first-filter-item/children/deep-filter-item/children' => [ 'nodeType' => 'Neos.Neos:ContentCollection' ],
            ],
            [
                'site' => $this->site,
                'workspaceName' => 'live',
            ]
        );
        $this->mapNode = $this->nodes['/sites/foo/main/map'];

        MockProvider::reset();
        $this->objectManager->setInstance(ReflectionService::class, new class {
            public function getClassNamesByAnnotation() {
                return [ MockProvider::class ];
            }
        });
    }

    public function tearDown(): void
    {
        $this->objectManager->forgetInstance(ReflectionService::class);
        parent::tearDown();
    }

    public function testCallsAnnotatedMarkerProviders(): void
    {
        $marker = new Marker('foo', [ '/foo' ], 0, 0, 'Foo');
        MockProvider::$markers = [ $marker ];
        $markers = $this->subject->getMarkers(new ProviderContext($this->mapNode, null))->get(1);
        $this->assertSame($marker, array_values($markers)[0]);
    }

    public function testCallsAnnotatedFilterItemProviders(): void
    {
        $item = new FilterItem('foo', '/foo', 'Foo');
        MockProvider::$filterItems = [ $item ];
        $items = $this->subject->getFilterItems(new ProviderContext($this->mapNode, null));
        $this->assertSame($item, array_values($items)[0]);
    }

    public function testDeduplicatesFilterItemsById(): void
    {
        $itemOne = new FilterItem('foo', '/foo', 'Foo');
        $itemTwo = new FilterItem('foo', '/foo', 'Foo');
        MockProvider::$filterItems = [ $itemOne, $itemTwo ];
        $items = $this->subject->getFilterItems(new ProviderContext($this->mapNode, null));
        $this->assertCount(1, $items);
    }

    public function testKeysFilterItemsById(): void
    {
        $item = new FilterItem('foo', '/foo', 'Foo');
        MockProvider::$filterItems = [ $item ];
        $items = $this->subject->getFilterItems(new ProviderContext($this->mapNode, null));
        $this->assertSame($item, $items['foo']);
    }

    public function testCachesFilterItemsUnderConfigurredCacheKey(): void
    {
        $key = md5(random_bytes(32));
        $item = new FilterItem('foo', '/foo', 'Foo');
        MockProvider::$filterItems = [ $item ];
        MockProvider::$filterItemCacheKey = $key;

        $backend = new TransientMemoryBackend();
        $frontend = new VariableFrontend('foo', $backend);
        $backend->setCache($frontend);
        $this->inject($this->subject, 'cache', new CacheManagerMock($frontend));

        $this->subject->getFilterItems(new ProviderContext($this->mapNode, null));
        $this->assertTrue($frontend->has($key));
        $this->assertCount(1, $frontend->get($key));
        $this->assertEquals('/foo', array_values($frontend->get($key))[0]->path);
    }

    public function testDoesNotCacheFilterItemsIfNoCacheKeySpecified(): void
    {
        $item = new FilterItem('foo', '/foo', 'Foo');
        MockProvider::$filterItems = [ $item ];
        MockProvider::$filterItemCacheKey = null;

        $backend = new TransientMemoryBackend();
        $frontend = new VariableFrontend('foo', $backend);
        $backend->setCache($frontend);
        $this->inject($this->subject, 'cache', new CacheManagerMock($frontend));

        $this->subject->getFilterItems(new ProviderContext($this->mapNode, null));
        $this->assertCount(0, $backend->getEntries());
    }

    public function testCachesMarkersUnderConfigurredCacheKey(): void
    {
        $key = md5(random_bytes(32));
        $marker = new Marker('foo', [ '/foo' ], 0, 0, 'Foo');
        MockProvider::$markers = [ $marker ];
        MockProvider::$markerCacheKey = $key;

        $backend = new TransientMemoryBackend();
        $frontend = new VariableFrontend('foo', $backend);
        $backend->setCache($frontend);
        $this->inject($this->subject, 'cache', new CacheManagerMock($frontend));
        $this->inject($this->subject, 'pageSize', 100);

        $keyWithPagination = $key . '__offset-0__limit-1';
        $this->subject->getMarkers(new ProviderContext($this->mapNode, null))->get(1);
        $this->assertTrue($frontend->has($keyWithPagination));
        $this->assertCount(1, $frontend->get($keyWithPagination));
        $this->assertEquals('/foo', array_values($frontend->get($keyWithPagination))[0]->paths[0]);
    }

    public function testDoesNotCacheMarkersIfNoCacheKeySpecified(): void
    {
        $marker = new Marker('foo', [ '/foo' ], 0, 0, 'Foo');
        MockProvider::$markers = [ $marker ];
        MockProvider::$markerCacheKey = null;

        $backend = new TransientMemoryBackend();
        $frontend = new VariableFrontend('foo', $backend);
        $backend->setCache($frontend);
        $this->inject($this->subject, 'cache', new CacheManagerMock($frontend));

        $this->subject->getMarkers(new ProviderContext($this->mapNode, null))->get(1);
        $this->assertCount(0, $backend->getEntries());
    }

    public function testOnlyASingleFilterItemCanBeOpenedByDefault(): void
    {
        $itemOne = new FilterItem('foo', '/foo', 'Foo');
        $itemOne->openByDefault = true;
        $itemTwo = new FilterItem('bar', '/bar', 'Foo');
        $itemTwo->openByDefault = true;

        MockProvider::$filterItems = [ $itemOne, $itemTwo ];
        $items = $this->subject->getFilterItems(new ProviderContext($this->mapNode, null));

        $this->assertTrue($items['foo']->openByDefault);
        $this->assertFalse($items['bar']->openByDefault);
    }
}
