<?php declare(strict_types=1);

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

use Mockery\Mock;
use Mockery\MockInterface;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Domain\Repository\NodeDataRepository;
use Neos\Flow\Cache\CacheManager;
use Neos\Neos\Service\PublishingService;
use Neos\Flow\Tests\FunctionalTestCase;
use Newland\NeosTestingHelpers\InteractsWithNodes;
use Newland\Toubiz\Map\Neos\Provider\Contract\ProviderContext;
use Newland\Toubiz\Map\Neos\Provider\MapDataProviderService;
use Newland\Toubiz\Map\Neos\Provider\NodeSlot;
use PHPUnit\Framework\MockObject\MockObject;
use Neos\Neos\Ui\ContentRepository\Service\NodeService;
use Ramsey\Uuid\Uuid;
use Neos\ContentRepository\Domain\Model\Node;

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

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

    /** @var NodeInterface */
    protected $node;

    /** @var string */
    protected $randomName;

    /** @var Mock<MapDataProviderService> */
    protected $dataProvider;

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

        $this->subject = $this->objectManager->get(NodeSlot::class);
        $this->randomName = md5(random_bytes(32));

        $this->dataProvider = \Mockery::mock($this->objectManager->get(MapDataProviderService::class));
        $this->objectManager->setInstance(MapDataProviderService::class, $this->dataProvider);

        $this->inject($this->subject, 'mapDataProvider', $this->dataProvider);
    }

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

    public function testCallsSerializerForNodeInTargetWorkspace(): void
    {
        [ $node, $nodeInTargetWorkspace ] = $this->createMapNodes([ 'user-foo', 'live' ]);

        $this->publish($node);

        $this->dataProvider
            ->shouldHaveReceived('getMarkers')
            ->once()
            ->withArgs($this->withContextContainingNode($nodeInTargetWorkspace));
        $this->dataProvider
            ->shouldHaveReceived('getFilterItems')
            ->once()
            ->withArgs($this->withContextContainingNode($nodeInTargetWorkspace));;
        $this->assertTrue(true);
    }

    public function testDoesNothingIfNodeDoesNotExistInTargetWorkspace(): void
    {
        [ $node ] = $this->createMapNodes([ 'user-foo' ]);

        $this->subject->onNodePublished($node, $this->getWorkspace('live'));

        $this->dataProvider->shouldNotHaveReceived('getMarkers');
        $this->dataProvider->shouldNotHaveReceived('getFilterItems');
        $this->assertTrue(true);
    }

    /** @dataProvider provideMapSubNodeTypes */
    public function testCallsSerializerForParentMapNodeIfNotMapDirectly(string $nodeType): void
    {
        [ $mapNode, $mapNodeInTargetWorkspace ] = $this->createMapNodes([ 'user-foo', 'live' ]);
        [ $child, $childNodeInTargetWorkspace ] = $this->createMapChildNodes([ 'user-foo', 'live' ], $nodeType);

        $this->publish($child);

        $this->dataProvider
            ->shouldHaveReceived('getFilterItems')
            ->once()
            ->withArgs($this->withContextContainingNode($mapNodeInTargetWorkspace));
        $this->dataProvider
            ->shouldHaveReceived('getMarkers')
            ->once()
            ->withArgs($this->withContextContainingNode($mapNodeInTargetWorkspace));

        $this->assertTrue(true);
    }

    /** @dataProvider provideMapSubNodeTypes */
    public function testDoesNothingIfNoMapParentExists(string $nodeType): void
    {
        $child = $this->initializeNode('/test-site/testing', 'user-foo', null, [ ], null, $nodeType);

        $this->publish($child);

        $this->dataProvider->shouldNotHaveReceived('getMarkers');
        $this->dataProvider->shouldNotHaveReceived('getFilterItems');
        $this->assertTrue(true);
    }

    public function testAlsoSerializesNewChildrenNode(): void
    {
        $service = new class extends MapDataProviderService {
            public $lastItems;
            public function getFilterItems(ProviderContext $context): array
            {
                $this->lastItems = parent::getFilterItems($context);
                return $this->lastItems;
            }
        };
        $this->inject($service, 'pageSize', 100);
        $this->inject($service, 'cache', $this->objectManager->get(CacheManager::class));
        $this->inject($service, 'objectManager', $this->objectManager);
        $this->inject($this->subject, 'mapDataProvider', $service);

        [ $mapNode, $mapNodeInTargetWorkspace ] = $this->createMapNodes([ 'user-foo', 'live' ]);
        [ $child ] = $this->createMapChildNodes([ 'user-foo' ], 'Newland.Toubiz.Map.Neos:Map.FilterItem');

        $published = $this->publish($mapNode);

        $this->assertCount(1, $service->lastItems);
        $this->assertArrayHasKey($child->getIdentifier(), $service->lastItems);
    }

    public function testDoesNotTaintRepository(): void
    {
        [ $mapNode, $mapNodeInTargetWorkspace ] = $this->createMapNodes([ 'user-foo', 'live' ]);
        [ $child ] = $this->createMapChildNodes([ 'user-foo' ], 'Newland.Toubiz.Map.Neos:Map.FilterItem');

        $this->objectManager->get(NodeDataRepository::class)->remove($child);
        $this->publish($mapNode);
        $this->objectManager->get(NodeDataRepository::class)->remove($child);

        // If this test fails then doctrine throws an exception on the second `remove` call.
        $this->assertTrue(true);
    }

    public function provideMapSubNodeTypes(): array
    {
        return [
            [ 'Newland.Toubiz.Map.Neos:Map.FilterItem' ],
            [ 'Newland.Toubiz.Map.Neos:Map.Markers.FilteredArticles' ],
            [ 'Newland.Toubiz.Map.Neos:Map.Markers.Link' ],
            [ 'Newland.Toubiz.Map.Neos:Map.Markers.SelectedArticles' ],
            [ 'Newland.Toubiz.Map.Neos:Map.Markers.SelectedPages' ],
        ];
    }

    private function publish(NodeInterface $node, string $targetWorkspaceName = 'live'): NodeInterface
    {
        $targetWorkspace = $this->getWorkspace($targetWorkspaceName);
        $workspace = $node->getWorkspace();
        $workspace->setBaseWorkspace($targetWorkspace);

        $nodeService = $this->objectManager->get(NodeService::class);
        $nodeInCorrectContext = $nodeService->getNodeFromContextPath($node->getContextPath(), null, null, true);

        $this->objectManager->get(PublishingService::class)->publishNode($nodeInCorrectContext, $targetWorkspace);
        $this->subject->afterPublishing();

        return $nodeInCorrectContext;
    }

    /**
     * @param string[] $workspaces
     * @return NodeInterface[]
     */
    private function createMapNodes(array $workspaces): array
    {
        $identifier = Uuid::uuid4()->toString();
        return array_map(
            function(string $workspace) use ($identifier) {
                return $this->initializeNode(
                    sprintf('/test-site/%s', $this->randomName),
                    $workspace,
                    null,
                    [ ],
                    null,
                    'Newland.Toubiz.Map.Neos:Map',
                    null,
                    $identifier
                );
            },
            $workspaces
        );
    }

    /** @return NodeInterface[] */
    private function createMapChildNodes(array $workspaces, string $nodeType, string $subPath = 'foo')
    {
        $collectionPath = sprintf('/test-site/%s/some-collection', $this->randomName);
        $collectionIdentifier = Uuid::uuid4()->toString();
        $path = sprintf('/test-site/%s/some-collection/%s', $this->randomName, $subPath);
        $identifier = Uuid::uuid4()->toString();

        return array_map(
            function(string $workspace) use ($path, $collectionPath, $identifier, $collectionIdentifier, $nodeType) {
                $this->initializeNode($collectionPath, $workspace, null, [ ], null, 'Neos.Neos:ContentCollection', null, $collectionIdentifier);
                return $this->initializeNode($path, $workspace, null, [ ], null, $nodeType, null, $identifier);
            },
            $workspaces
        );
    }

    private function withContextContainingNode(Node $node): \Closure
    {
        return function(ProviderContext $context) use ($node) {
            return $context->mapNode()->getContextPath() === $node->getContextPath();
        };
    }
}
