<?php declare(strict_types=1);

namespace Newland\NeosFiltering\Tests\Unit\Items;

use Doctrine\ORM\Query\Expr\Join;
use Neos\Flow\Http\Request;
use Neos\Flow\Http\Uri;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Newland\Contracts\Neos\Filter\Expression;
use Newland\NeosFiltering\Items\CheckboxList;
use Newland\NeosFiltering\Items\Group;
use Newland\NeosFiltering\Items\Root;
use Newland\NeosFiltering\Items\Section;
use Newland\NeosFiltering\Items\SelectBox;
use Newland\NeosFiltering\Tests\Factory\ExampleAttributeFactory;
use Newland\NeosFiltering\Tests\Factory\ExampleCategoryFactory;
use Newland\NeosFiltering\Tests\Factory\ExampleEntityFactory;
use Newland\NeosFiltering\Tests\Fixture\DataSource\ExampleDataSource;
use Newland\NeosFiltering\Tests\Fixture\ItemMock;
use Newland\NeosTestingHelpers\InteractsWithNodes;

class RootTest extends ItemTestCase
{
    use InteractsWithNodes;

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

    public function setUp(): void
    {
        parent::setUp();
        $this->subject = new Root([], $this->initializeNode('/sites/foo-bar'));
        $this->subject->setActionUri('https://action.uri');
    }

    public function testRendersChildren(): void
    {
        $this->subject->addItem(new ItemMock('Foo', null, 'foo'));
        $this->subject->addItem(new ItemMock('Bar', null, 'bar'));
        $this->subject->addItem(new ItemMock('Baz', null, 'baz'));

        $rendered = $this->renderSubject();
        $this->assertContains('Foo', $rendered);
        $this->assertContains('Bar', $rendered);
        $this->assertContains('Baz', $rendered);
    }

    public function testSetsStateOnDirectChildren(): void
    {
        $withQueryString = new ItemMock();
        $withQueryString->setQueryString('test_query_string');
        $withoutQueryString = new ItemMock();

        $this->subject->addItem($withQueryString);
        $this->subject->addItem($withoutQueryString);

        $this->subject->setState([ 'test_query_string' => 'yeaaaaaah' ]);
        $this->assertEquals('yeaaaaaah', $withQueryString->state);
    }

    public function testSetsStateOnDeepChildren(): void
    {
        $someItem = new ItemMock();
        $someItem->setQueryString('hello');
        $group = new Group([ $someItem ]);
        $section = new Section([ $group ]);

        $this->subject->addItem($section);

        $this->subject->setState([ 'hello' => 'from the other side' ]);
        $this->assertEquals('from the other side', $someItem->state);
    }

    public function testAppliesJoinsToQuery(): void
    {
        $this->subject->setJoins(
            [
                [ 'alias' => 'foo', 'column' => 'entity.tag' ],
            ]
        );
        $query = $this->subject->applyToQuery($this->initializeEmptyQuery());

        $matchingCount = 0;
        foreach ($query->getDQLPart('join')['entity'] as $join) {
            /** @var Join $join */
            if ($join->getJoin() === 'entity.tag' && $join->getAlias() === 'foo') {
                $matchingCount++;
            }
        }
        $this->assertEquals(1, $matchingCount);
    }

    public function testDoesNotApplyJoinToQueryIfAlreadyDefinedOnQuery(): void
    {
        $this->subject->setJoins(
            [
                [ 'alias' => 'foo', 'column' => 'entity.tag' ],
            ]
        );

        $query = $this->initializeEmptyQuery();
        $query->join('entity.tag', 'foo');
        $query = $this->subject->applyToQuery($query);

        $matchingCount = 0;
        foreach ($query->getDQLPart('join')['entity'] as $join) {
            /** @var Join $join */
            if ($join->getJoin() === 'entity.tag' && $join->getAlias() === 'foo') {
                $matchingCount++;
            }
        }
        $this->assertEquals(1, $matchingCount);
    }

    public function testCombinesFiltersWithAnd(): void
    {
        $fooFoo = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'foo', 'name' => 'foo' ]);
        $fooBar = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'foo', 'name' => 'bar' ]);
        $barFoo = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'bar', 'name' => 'foo' ]);
        $barBar = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'bar', 'name' => 'bar' ]);

        $this->subject->addItem(
            new ItemMock(
                '', function ($expr) {
                return Expression::where($expr->eq('entity.tag', $expr->literal('foo')));
            }, 'foo'
            )
        );
        $this->subject->addItem(
            new ItemMock(
                '', function ($expr) {
                return Expression::where($expr->eq('entity.name', $expr->literal('bar')));
            }, 'bar'
            )
        );

        $this->subject->setCombine(Group::$AND);
        $ids = array_map(
            function ($item) {
                return $item->uuid;
            },
            $this->subject->applyToQuery($this->initializeEmptyQuery())->getQuery()->execute()
        );

        $this->assertCount(1, $ids);
        $this->assertNotContains(
            $fooFoo->uuid,
            $ids,
            '(entity.tag = "foo" AND entity.name = "bar") should not contain { "tag": "foo", "name": "foo" }'
        );
        $this->assertContains(
            $fooBar->uuid,
            $ids,
            '(entity.tag = "foo" AND entity.name = "bar") should contain { "tag": "foo", "name": "bar" }'
        );
        $this->assertNotContains(
            $barFoo->uuid,
            $ids,
            '(entity.tag = "foo" AND entity.name = "bar") should not contain { "tag": "bar", "name": "foo" }'
        );
        $this->assertNotContains(
            $barBar->uuid,
            $ids,
            '(entity.tag = "foo" AND entity.name = "bar") should not contain { "tag": "bar", "name": "bar" }'
        );
    }

    public function testCombinesFiltersWithOr(): void
    {
        $fooFoo = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'foo', 'name' => 'foo' ]);
        $fooBar = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'foo', 'name' => 'bar' ]);
        $barFoo = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'bar', 'name' => 'foo' ]);
        $barBar = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'bar', 'name' => 'bar' ]);
        $bazBaz = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'baz', 'name' => 'baz' ]);

        $this->subject->addItem(
            new ItemMock(
                '',
                function ($expr) {
                    return Expression::where($expr->eq('entity.tag', $expr->literal('foo')));
                },
                'foo'
            )
        );
        $this->subject->addItem(
            new ItemMock(
                '',
                function ($expr) {
                    return Expression::where($expr->eq('entity.name', $expr->literal('bar')));
                },
                'bar'
            )
        );

        $this->subject->setCombine(Group::$OR);
        $ids = array_map(
            function ($item) {
                return $item->uuid;
            },
            $this->subject->applyToQuery($this->initializeEmptyQuery())->getQuery()->execute()
        );

        $this->assertCount(3, $ids);
        $this->assertContains(
            $fooFoo->uuid,
            $ids,
            '(entity.tag = "foo" OR entity.name = "bar") should contain { "tag": "foo", "name": "foo" }'
        );
        $this->assertContains(
            $fooBar->uuid,
            $ids,
            '(entity.tag = "foo" OR entity.name = "bar") should contain { "tag": "foo", "name": "bar" }'
        );
        $this->assertNotContains(
            $barFoo->uuid,
            $ids,
            '(entity.tag = "foo" OR entity.name = "bar") should not contain { "tag": "bar", "name": "foo" }'
        );
        $this->assertContains(
            $barBar->uuid,
            $ids,
            '(entity.tag = "foo" OR entity.name = "bar") should contain { "tag": "bar", "name": "bar" }'
        );
        $this->assertNotContains(
            $bazBaz->uuid,
            $ids,
            '(entity.tag = "foo" OR entity.name = "bar") should not contain { "tag": "baz", "name": "baz" }'
        );
    }

    public function testResolvesComplexQueryCorrectly(): void
    {
        [ $cat1, $cat2, $cat3 ] = (new ExampleCategoryFactory($this->objectManager))->createMultiple(3);
        $factory = new ExampleEntityFactory($this->objectManager);

        $shouldMatch = [];
        $shouldNotMatch = [];
        $shouldNotMatch[] = $factory->create([ 'tag' => 'foo', 'name' => 'not special', 'categories' => [ $cat3 ] ]);
        $shouldMatch[] = $factory->create([ 'tag' => 'foo', 'name' => 'special', 'categories' => [ $cat3 ] ]);
        $shouldMatch[] = $factory->create([ 'tag' => 'foo', 'name' => 'not special', 'categories' => [ $cat2 ] ]);
        $shouldMatch[] = $factory->create(
            [ 'tag' => 'bar', 'name' => 'not special', 'categories' => [ $cat3, $cat1 ] ]
        );
        $shouldNotMatch[] = $factory->create([ 'tag' => 'baz', 'name' => 'special', 'categories' => [ $cat3 ] ]);

        // Building a filter that will create queries such as the following:
        // WHERE ( entity.tag IN ("foo", "bar) AND ( entity.name = "special" OR entity.category IN (cat1, cat2) ) )
        $this->subject->setJoins([ [ 'alias' => 'categories', 'column' => 'entity.categories' ] ]);
        $category = new CheckboxList();
        $category->setRoot($this->subject);
        $category->setQueryString('category');
        $category->setDatabaseColumn('categories');
        $category->setDataSource(new ExampleDataSource());
        $category->setDataSourceArguments(
            [
                [ 'label' => 'cat1', 'value' => $cat1->uuid ],
                [ 'label' => 'cat2', 'value' => $cat2->uuid ],
                [ 'label' => 'cat3', 'value' => $cat2->uuid ],
            ]
        );
        $name = new SelectBox();
        $name->setRoot($this->subject);
        $name->setQueryString('name');
        $name->setDatabaseColumn('entity.name');
        $name->setDataSource(new ExampleDataSource());
        $name->setDataSourceArguments(
            [
                [ 'label' => 'Something', 'value' => 'something' ],
                [ 'label' => 'Special', 'value' => 'special' ],
            ]
        );
        $group = new Group([ $name, $category ]);
        $group->setCombine(Group::$OR);
        $group->setRoot($this->subject);
        $tag = new CheckboxList();
        $tag->setRoot($this->subject);
        $tag->setQueryString('tag');
        $tag->setDatabaseColumn('entity.tag');
        $tag->setDataSource(new ExampleDataSource());
        $tag->setDataSourceArguments(
            [
                [ 'label' => 'Foo', 'value' => 'foo' ],
                [ 'label' => 'Bar', 'value' => 'bar' ],
                [ 'label' => 'Baz', 'value' => 'baz' ],
            ]
        );
        $this->subject->addItem($tag);
        $this->subject->addItem($group);
        $this->subject->setCombine(Root::$AND);

        $this->subject->setState(
            [
                'category' => [ $cat1->uuid, $cat2->uuid ],
                'name' => 'special',
                'tag' => [ 'foo', 'bar' ],
            ]
        );

        $ids = array_map(
            function ($item) {
                return $item->uuid;
            },
            $this->subject->applyToQuery($this->initializeEmptyQuery())->getQuery()->execute()
        );
        foreach ($shouldMatch as $entity) {
            $this->assertContains($entity->uuid, $ids);
        }
        foreach ($shouldNotMatch as $entity) {
            $this->assertNotContains($entity->uuid, $ids);
        }
    }

    public function testFindsResultWithMultipleRelationsOnlyOnce(): void
    {
        $attributes = (new ExampleAttributeFactory($this->objectManager))->createMultiple(2);
        $categories = (new ExampleCategoryFactory($this->objectManager))->createMultiple(2);
        $entity = (new ExampleEntityFactory($this->objectManager))->create(
            [ 'categories' => $categories, 'attributes' => $attributes ]
        );

        $query = $this->subject->applyToQuery($this->initializeEmptyQuery())
            ->join('entity.attributes', 'attributes')
            ->join('entity.categories', 'categories');
        $result = $query->getQuery()->execute();

        $this->assertCount(1, $result);
    }

    public function testUsesActionUri(): void
    {
        $this->subject->setActionUri('https://foo.com/bar/baz');
        $this->assertContains('action="https://foo.com/bar/baz"', $this->renderSubject());
    }

    public function testUsesUriBuilderToResolveUrl(): void
    {
        $request = new ActionRequest(Request::create(new Uri('https://test.com')));

        $uriBuilder = $this->createMock(UriBuilder::class);
        $uriBuilder->expects($this->once())
            ->method('uriFor')
            ->willReturn('https://test.com/foo/bar');
        $this->inject($this->subject, 'uriBuilder', $uriBuilder);

        $this->subject->setAction([], $request);
        $this->assertContains('action="https://test.com/foo/bar', $this->renderSubject());
    }

    public function testClonesChildrenItemsWhenCloningRoot(): void
    {
        $foo = new ItemMock('Foo', null, 'foo');
        $this->subject->addItem($foo);

        $this->assertSame($foo, $this->subject->getChildrenItems()[0]);
        $this->assertNotSame($foo, (clone $this->subject)->getChildrenItems()[0]);
    }

}
