<?php declare(strict_types=1);

namespace Newland\NeosFiltering\Tests\Unit\Factory;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Neos\Flow\I18n\Translator;
use Neos\FluidAdaptor\View\StandaloneView;
use Newland\Contracts\Neos\Filter\FilterItem;
use Newland\NeosFiltering\Factory\DefaultFilterFactory;
use Newland\NeosFiltering\Items\CheckboxList;
use Newland\NeosFiltering\Items\DirectEquals;
use Newland\NeosFiltering\Items\Group;
use Newland\NeosFiltering\Items\ObstructionWrapper;
use Newland\NeosFiltering\Items\Range;
use Newland\NeosFiltering\Items\Root;
use Newland\NeosFiltering\Items\Section;
use Newland\NeosFiltering\Items\SelectBox;
use Newland\NeosFiltering\RangeSource\DirectRangeSource;
use Newland\NeosFiltering\Tests\BaseTestCase;
use Newland\NeosFiltering\Tests\Factory\ExampleAttributeFactory;
use Newland\NeosFiltering\Tests\Factory\ExampleEntityFactory;
use Newland\NeosFiltering\Tests\Fixture\ClassThatDoesNotImplementFilterInterface;
use Newland\NeosFiltering\Tests\Fixture\DataSource\ExampleDataSource;
use Newland\NeosFiltering\Tests\Fixture\Domain\Model\ExampleEntity;
use Newland\NeosFiltering\Tests\Fixture\ItemMock;
use Newland\NeosFiltering\Tests\Fixture\RangeSource\ExampleRangeSource;

class DefaultFilterFactoryTest extends BaseTestCase
{

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

    /** @var EntityManagerInterface */
    protected $entityManager;

    public function setUp(): void
    {
        parent::setUp();
        $this->subject = new DefaultFilterFactory($this->node);
        $this->entityManager = $this->objectManager->get(EntityManagerInterface::class);
    }

    /**
     * @dataProvider provideShortTypes
     * @param string $type
     * @param string $expectedClass
     */
    public function testInitializesFilterItemsByShortString(string $type, string $expectedClass): void
    {
        $config = [
            'type' => $type,
            'defaultState' => 'HELLO THERE',
            'databaseColumn' => 'entity.test',
            'queryString' => 'test',
            'combine' => 'AND',
            'range' => [ 'min' => 0, 'max' => 100 ],
            'children' => [],
            'dataSource' => 'newland-neosfiltering-tests-example',
            'dataSourceArguments' => [ ],
        ];

        $this->assertInstanceOf($expectedClass, $this->createFilterItem($config));
    }

    public function provideShortTypes(): array
    {
        return [
            [ 'checkbox_list', CheckboxList::class ],
            [ 'group', Group::class ],
            [ 'range', Range::class ],
            [ 'section', Section::class ],
            [ 'select', SelectBox::class ],
        ];
    }

    public function testInitializesCustomFilterItemIfFqnPassedAsType(): void
    {
        $this->assertInstanceOf(
            ItemMock::class,
            $this->createFilterItem(
                [
                    'type' => ItemMock::class,
                    'defaultState' => 'HELLO THERE',
                    'databaseColumn' => 'entity.test',
                    'queryString' => 'test',
                    'combine' => 'AND',
                    'range' => [ 'min' => 0, 'max' => 100 ],
                ]
            )
        );
    }

    public function testThrowsExceptionIfCustomClassNotFound(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessageRegExp('/not a valid `type` attribute/');
        $this->createFilterItem([ 'type' => 'This\Class\Does\Not\Exist' ]);
    }

    public function testThrowsExceptionIfClassDoesNotImplementFilterItemInterface(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessageRegExp('/Class.*does not implement interface/');
        $this->createFilterItem([ 'type' => ClassThatDoesNotImplementFilterInterface::class ]);
    }

    public function testSetsQueryStringOnInitializedItem(): void
    {
        /** @var ItemMock $item */
        $item = $this->createFilterItem(
            [
                'type' => ItemMock::class,
                'queryString' => 'query_string_yo',
                'databaseColumn' => 'entity.test',
                'combine' => 'AND',
                'range' => [ 'min' => 0, 'max' => 100 ],
            ]
        );
        $this->assertEquals('query_string_yo', $item->queryString);
    }

    public function testSetsDefaultStateOnInitializedItem(): void
    {
        /** @var ItemMock $item */
        $item = $this->createFilterItem(
            [
                'type' => ItemMock::class,
                'defaultState' => 'HELLO THERE',
                'databaseColumn' => 'entity.test',
                'queryString' => 'test',
                'combine' => 'AND',
                'range' => [ 'min' => 0, 'max' => 100 ],
            ]
        );
        $this->assertEquals('HELLO THERE', $item->state);
    }

    public function testSetsCombinationTypeOnInitializedItem(): void
    {
        /** @var CheckboxList $item */
        $item = $this->createFilterItem(
            [
                'type' => CheckboxList::class,
                'combine' => 'AND',
                'databaseColumn' => 'entity.test',
                'queryString' => 'test',
                'dataSource' => 'newland-neosfiltering-tests-example',
                'dataSourceArguments' => [  ],
            ]
        );
        $this->assertEquals('AND', $item->getCombine());
    }

    public function testSetsDatabaseColumnOnInitializedItem(): void
    {
        /** @var CheckboxList $item */
        $item = $this->createFilterItem(
            [
                'type' => CheckboxList::class,
                'databaseColumn' => 'entity.column',
                'queryString' => 'test',
                'combine' => 'AND',
                'dataSource' => 'newland-neosfiltering-tests-example',
                'dataSourceArguments' => [  ],
            ]
        );
        $this->assertEquals('entity.column', $item->getDatabaseColumn());
    }

    public function testSetsTranslatedTitleOnInitializedItem(): void
    {
        $translator = $this->createMock(Translator::class);
        $translator->expects($this->once())
            ->method('translateById')
            ->with('foo', [], null, null, 'View/Filter', 'Newland.MyTheme')
            ->willReturn('This is a test');

        $this->inject($this->subject, 'translator', $translator);
        $item = $this->createFilterItem(
            [
                'type' => CheckboxList::class,
                'databaseColumn' => 'entity.test',
                'queryString' => 'test',
                'combine' => 'AND',
                'dataSource' => 'newland-neosfiltering-tests-example',
                'dataSourceArguments' => [ ],
                'title' => [
                    'id' => 'foo',
                    'source' => 'View/Filter',
                    'package' => 'Newland.MyTheme',
                ],
            ]
        );

        $this->assertEquals('This is a test', $item->getTitle());
    }

    public function testSetsDataSourceByIdentifierOnInitializedItem(): void
    {
        /** @var CheckboxList $item */
        $item = $this->createFilterItem(
            [
                'type' => CheckboxList::class,
                'dataSource' => 'newland-neosfiltering-tests-example',
                'databaseColumn' => 'entity.test',
                'queryString' => 'test',
                'combine' => 'AND',
            ]
        );

        $this->assertInstanceOf(ExampleDataSource::class, $item->getDataSource());
    }

    public function testPassesDataSourceArguments(): void
    {
        /** @var CheckboxList $item */
        $item = $this->createFilterItem(
            [
                'type' => CheckboxList::class,
                'dataSource' => 'newland-neosfiltering-tests-example',
                'dataSourceArguments' => [ 'foo', 'bar', 'baz' ],
                'databaseColumn' => 'entity.test',
                'queryString' => 'test',
                'combine' => 'AND',
            ]
        );

        $this->assertEquals([ 'foo', 'bar', 'baz' ], $item->getDataSourceArguments());
    }

    public function testInitializesChildrenForItem(): void
    {
        /** @var Group $item */
        $item = $this->createFilterItem(
            [
                'type' => Group::class,
                'combine' => 'OR',
                'children' => [
                    'test' => [
                        'type' => 'select',
                        'databaseColumn' => 'entity.number',
                        'queryString' => 'test',
                        'dataSource' => 'newland-neosfiltering-tests-example',
                        'dataSourceArguments' => [ ],
                    ],
                ],
            ]
        );

        $this->assertCount(1, $item->getChildrenItems());
        $this->assertInstanceOf(SelectBox::class, $item->getChildrenItems()[0]);
    }

    public function testSetsCombinationTypeOnRoot(): void
    {
        $root = $this->subject->createFilter([ 'combine' => 'AND', 'children' => [] ]);
        $this->assertEquals('AND', $root->getCombine());
    }


    public function testInitializesChildrenOnRoot(): void
    {
        $root = $this->subject->createFilter(
            [
                'combine' => 'AND',
                'children' => [
                    'section' => [
                        'type' => 'section',
                        'combine' => 'AND',
                        'children' => [
                            'number' => [
                                'type' => 'select',
                                'databaseColumn' => 'entity.number',
                                'queryString' => 'number',
                                'dataSource' => 'newland-neosfiltering-tests-example',
                                'dataSourceArguments' => [],
                            ],
                        ],
                    ],
                ],
            ]
        );

        $this->assertCount(1, $root->getChildrenItems());
        $this->assertInstanceOf(Section::class, $root->getChildrenItems()[0]);
        $this->assertCount(1, $root->getChildrenItems()[0]->getChildrenItems());
        $this->assertInstanceOf(SelectBox::class, $root->getChildrenItems()[0]->getChildrenItems()[0]);
    }

    public function testInitializesJoinsOnRoot(): void
    {
        $root = $this->subject->createFilter(
            [
                'combine' => 'AND',
                'children' => [],
                'join' => [
                    [ 'column' => 'entity.categories', 'alias' => 'categories' ],
                ],
            ]
        );

        /** @var QueryBuilder $query */
        $query = $this->objectManager->get(EntityManagerInterface::class)->createQueryBuilder();
        $query->select('entity')->from(ExampleEntity::class, 'entity');
        $query = $root->applyToQuery($query);

        $this->assertCount(1, $query->getDQLPart('join')['entity'] ?? []);
    }

    public function testSetsRangeSourceOnItem(): void
    {
        /** @var ItemMock $item */
        $item = $this->createFilterItem(
            [
                'type' => ItemMock::class,
                'queryString' => 'number',
                'rangeSource' => ExampleRangeSource::class,
                'rangeSourceArguments' => [
                    'base' => 10,
                    'delta' => 5,
                ],
            ]
        );

        $this->assertInstanceOf(ExampleRangeSource::class, $item->rangeSource);
        $this->assertEquals(5, $item->rangeSource->min());
        $this->assertEquals(15, $item->rangeSource->max());
    }

    public function testSetsDirectBoundsProviderOnItem(): void
    {
        /** @var ItemMock $item */
        $item = $this->createFilterItem(
            [
                'type' => ItemMock::class,
                'queryString' => 'number',
                'range' => [ 'min' => 5, 'max' => 25 ],
            ]
        );

        $this->assertInstanceOf(DirectRangeSource::class, $item->rangeSource);
        $this->assertEquals(5, $item->rangeSource->min());
        $this->assertEquals(25, $item->rangeSource->max());
    }

    public function testCreatesGroupForWhereStatement(): void
    {
        $item = $this->createFilterItem(
            [
                'type' => ItemMock::class,
                'queryString' => 'number',
                'databaseWhere' => 'entity.number=5',
                'range' => [ 'min' => 0, 'max' => 100 ],
            ]
        );

        $this->assertInstanceOf(Group::class, $item);
        $this->assertCount(2, $item->getChildrenItems());
    }

    public function testAppliesWhereStatementCorrectly(): void
    {
        $first = (new ExampleEntityFactory($this->objectManager))->create([ 'name' => 'hello', 'number' => 2 ]);
        $second = (new ExampleEntityFactory($this->objectManager))->create([ 'name' => 'hello', 'number' => 5 ]);
        $third = (new ExampleEntityFactory($this->objectManager))->create([ 'name' => 'olleh', 'number' => 5 ]);

        $item = $this->createFilterItem(
            [
                'type' => SelectBox::class,
                'databaseColumn' => 'entity.name',
                'queryString' => 'name',
                'defaultState' => 'hello',
                'dataSource' => 'newland-neosfiltering-tests-example',
                'dataSourceArguments' => [
                    [ 'label' => 'Hello', 'value' => 'hello' ],
                ],
                'databaseWhere' => 'entity.number=5',
            ]
        );

        $expression = $item->queryExpression($this->entityManager->getExpressionBuilder());

        $results = $this->entityManager->createQueryBuilder()
            ->select('entity')
            ->from(ExampleEntity::class, 'entity')
            ->where($expression->where)
            ->getQuery()
            ->execute();

        $ids = array_map(
            function (ExampleEntity $item) {
                return $item->uuid;
            },
            $results
        );

        $this->assertNotContains($first->uuid, $ids);
        $this->assertContains($second->uuid, $ids);
        $this->assertNotContains($third->uuid, $ids);
    }

    public function testThrowsExceptionWhenSpecifyingIncorrectCombinationType(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessageRegExp('/Combine option must be one of/');
        $this->createFilterItem(
            [
                'type' => 'section',
                'children' => [],
                'combine' => 'INVALID',
            ]
        );
    }

    public function testAddsFormAttributesFromConfiguration(): void
    {
        $filter = $this->subject->createFilter(
            [
                'children' => [],
                'combine' => 'AND',
                'formAttributes' => [
                    'data-some-attribute-form-tests' => 'exists'
                ]
            ]
        );
        $filter->setActionUri('/foo');

        $this->assertContains('data-some-attribute-form-tests="exists"', $this->render($filter));
    }

    public function testSetsDefaultStateIfOverridenButNotHidden(): void
    {
        $config = [
            'type' => ItemMock::class,
            'databaseColumn' => 'entity.number',
            'queryString' => 'number',
            'range' => [ 'min' => 0, 'max' => 100 ],
            'override' => [
                'property' => 'preselectedNumber',
                'behaviour' => 'show',
            ],
        ];
        /** @var ItemMock $item */
        $item = $this->createFilterItem($config, [ 'preselectedNumber' => 55 ]);

        $this->assertInstanceOf(ItemMock::class, $item);
        $this->assertEquals(55, $item->state);
    }

    public function testSetsDeepDefaultStateForOverrideWithoutHiding(): void
    {
        $config = [
            'combine' => 'AND',
            'children' => [
                'number' => [
                    'type' => ItemMock::class,
                    'databaseColumn' => 'entity.number',
                    'queryString' => 'number',
                    'range' => [ 'min' => 0, 'max' => 100 ],
                    'override' => [
                        'property' => 'preselectedNumber',
                        'behaviour' => 'show',
                    ],
                ],
            ],
        ];

        /** @var Root $item */
        $item = $this->subject->createFilter($config, [ 'preselectedNumber' => 55 ]);

        $this->assertCount(1, $item->getChildrenItems());
        $this->assertInstanceOf(ItemMock::class, $item->getChildrenItems()[0]);
        $this->assertEquals(55, $item->getChildrenItems()[0]->state);
    }

    public function testObstructsItemIfOverriddenWithHiding(): void
    {
        $config = [
            'type' => ItemMock::class,
            'databaseColumn' => 'entity.number',
            'queryString' => 'number',
            'range' => [ 'min' => 0, 'max' => 100 ],
            'override' => [
                'property' => 'preselectedNumber',
                'behaviour' => 'hide',
            ],
        ];
        /** @var DirectEquals $item */
        $item = $this->createFilterItem($config, [ 'preselectedNumber' => 55 ]);

        $this->assertInstanceOf(ObstructionWrapper::class, $item);
    }

    public function testObstructsDeepItemIfOverriddenWithHiding(): void
    {
        $config = [
            'combine' => 'AND',
            'children' => [
                'number' => [
                    'type' => ItemMock::class,
                    'databaseColumn' => 'entity.number',
                    'queryString' => 'number',
                    'range' => [ 'min' => 0, 'max' => 100 ],
                    'override' => [
                        'property' => 'preselectedNumber',
                        'behaviour' => 'hide',
                    ],
                ],
            ],
        ];
        /** @var Root $item */
        $item = $this->subject->createFilter($config, [ 'preselectedNumber' => 55 ]);

        $this->assertCount(1, $item->getChildrenItems());
        $this->assertInstanceOf(ObstructionWrapper::class, $item->getChildrenItems()[0]);
    }

    public function testChecksIfMoreThanOneItemIsPreselectedForHidingWithPreselectedBehaviour(): void
    {
        $config = [
            'type' => 'checkbox_list',
            'databaseColumn' => 'entity.tags',
            'queryString' => 'tags',
            'combine' => 'AND',
            'dataSource' => 'newland-neosfiltering-tests-example',
            'dataSourceArguments' => [
                [ 'value' => 'foo', 'label' => 'Foo' ],
                [ 'value' => 'bar', 'label' => 'Bar' ],
                [ 'value' => 'baz', 'label' => 'Baz' ],
            ],
            'override' => [
                'property' => 'preselectedTags',
                'behaviour' => 'preselect',
            ],
        ];

        $item = $this->createFilterItem($config, [ 'preselectedTags' => [] ]);
        $this->assertInstanceOf(CheckboxList::class, $item);

        $item = $this->createFilterItem($config, [ 'preselectedTags' => [ 'foo' ] ]);
        $this->assertInstanceOf(ObstructionWrapper::class, $item);
        $this->assertCount(1, $item->getState());
        $this->assertEquals('foo', $item->getState()[0]['value']);

        $item = $this->createFilterItem($config, [ 'preselectedTags' => [ 'foo', 'bar' ] ]);
        $this->assertInstanceOf(CheckboxList::class, $item);
        $this->assertCount(2, $item->getState());
        $this->assertEquals('foo', $item->getState()[0]['value']);
        $this->assertEquals('bar', $item->getState()[1]['value']);
    }

    public function testChecksIfMoreThanOneItemIsPreselectedForDeepHidingWithPreselectedBehaviour(): void
    {
        $config = [
            'combine' => 'AND',
            'children' => [
                'tags' => [
                    'type' => 'checkbox_list',
                    'databaseColumn' => 'entity.tags',
                    'queryString' => 'tags',
                    'combine' => 'AND',
                    'dataSource' => 'newland-neosfiltering-tests-example',
                    'dataSourceArguments' => [
                        [ 'value' => 'foo', 'label' => 'Foo' ],
                        [ 'value' => 'bar', 'label' => 'Bar' ],
                        [ 'value' => 'baz', 'label' => 'Baz' ],
                    ],
                    'override' => [
                        'property' => 'preselectedTags',
                        'behaviour' => 'preselect',
                    ],
                ],
            ],
        ];

        $item = $this->subject->createFilter($config, [ 'preselectedTags' => [] ]);
        $this->assertInstanceOf(CheckboxList::class, $item->getChildrenItems()[0]);

        $item = $this->subject->createFilter($config, [ 'preselectedTags' => [ 'foo' ] ]);
        $this->assertInstanceOf(ObstructionWrapper::class, $item->getChildrenItems()[0]);
        $this->assertCount(1, $item->getChildrenItems()[0]->getState());
        $this->assertEquals('foo', $item->getChildrenItems()[0]->getState()[0]['value']);

        $item = $this->subject->createFilter($config, [ 'preselectedTags' => [ 'foo', 'bar' ] ]);
        $this->assertInstanceOf(CheckboxList::class, $item->getChildrenItems()[0]);
        $this->assertCount(2, $item->getChildrenItems()[0]->getState());
        $this->assertEquals('foo', $item->getChildrenItems()[0]->getState()[0]['value']);
        $this->assertEquals('bar', $item->getChildrenItems()[0]->getState()[1]['value']);
    }


    public function testChecksIfMoreThanOneItemIsPreselectedForHidingWithLimitBehaviour(): void
    {
        $config = [
            'type' => 'checkbox_list',
            'databaseColumn' => 'entity.tags',
            'queryString' => 'tags',
            'combine' => 'AND',
            'dataSource' => 'newland-neosfiltering-tests-example',
            'dataSourceArguments' => [
                [ 'value' => 'foo', 'label' => 'Foo' ],
                [ 'value' => 'bar', 'label' => 'Bar' ],
                [ 'value' => 'baz', 'label' => 'Baz' ],
            ],
            'override' => [
                'property' => 'preselectedTags',
                'behaviour' => 'limit_selectable',
            ],
        ];

        $item = $this->createFilterItem($config, [ 'preselectedTags' => [] ]);
        $this->assertInstanceOf(CheckboxList::class, $item);

        $item = $this->createFilterItem($config, [ 'preselectedTags' => [ 'foo' ] ]);
        $this->assertInstanceOf(ObstructionWrapper::class, $item);
        $values = $this->getStateValuesForQuery($item);
        $this->assertCount(1, $values);
        $this->assertEquals('foo', $values[0]);

        $item = $this->createFilterItem($config, [ 'preselectedTags' => [ 'foo', 'bar' ] ]);
        $this->assertInstanceOf(CheckboxList::class, $item);
        $values = $this->getStateValuesForQuery($item);
        $this->assertCount(2, $values);
        $this->assertEquals('foo', $values[0]);
        $this->assertEquals('bar', $values[1]);
    }

    public function testLimitSelectableBehaviourDoesNotDisplayStatusIndicatorsByDefault(): void
    {
        $config = [
            'type' => 'checkbox_list',
            'databaseColumn' => 'entity.tags',
            'queryString' => 'tags',
            'combine' => 'AND',
            'dataSource' => 'newland-neosfiltering-tests-example',
            'dataSourceArguments' => [
                [ 'value' => 'foo', 'label' => 'Foo' ],
                [ 'value' => 'bar', 'label' => 'Bar' ],
                [ 'value' => 'baz', 'label' => 'Baz' ],
            ],
            'override' => [
                'property' => 'preselectedTags',
                'behaviour' => 'limit_selectable',
            ],
        ];

        // Default state: Should use all for filter but none for indicators
        $item = $this->createFilterItem($config, [ 'preselectedTags' => [ 'foo', 'bar' ] ]);
        $this->assertInstanceOf(CheckboxList::class, $item);
        $this->assertCount(2, $this->getStateValuesForQuery($item));
        $this->assertCount(0, $item->getStatusIndicators());

        // Now a filter has been selected.
        $item->setState([ 'foo' ]);
        $this->assertCount(1, $this->getStateValuesForQuery($item));
        $this->assertCount(1, $item->getStatusIndicators());
    }

    public function testChecksIfMoreThanOneItemIsPreselectedForDeepHidingWithLimitBehaviour(): void
    {
        $config = [
            'combine' => 'AND',
            'children' => [
                'tags' => [
                    'type' => 'checkbox_list',
                    'databaseColumn' => 'entity.tags',
                    'queryString' => 'tags',
                    'combine' => 'AND',
                    'dataSource' => 'newland-neosfiltering-tests-example',
                    'dataSourceArguments' => [
                        [ 'value' => 'foo', 'label' => 'Foo' ],
                        [ 'value' => 'bar', 'label' => 'Bar' ],
                        [ 'value' => 'baz', 'label' => 'Baz' ],
                    ],
                    'override' => [
                        'property' => 'preselectedTags',
                        'behaviour' => 'limit_selectable',
                    ],
                ],
            ],
        ];

        $item = $this->subject->createFilter($config, [ 'preselectedTags' => [] ]);
        $this->assertInstanceOf(CheckboxList::class, $item->getChildrenItems()[0]);

        $item = $this->subject->createFilter($config, [ 'preselectedTags' => [ 'foo' ] ]);
        $this->assertInstanceOf(ObstructionWrapper::class, $item->getChildrenItems()[0]);
        $values = $this->getStateValuesForQuery($item->getChildrenItems()[0]);
        $this->assertCount(1, $values);
        $this->assertEquals('foo', $values[0]);

        $item = $this->subject->createFilter($config, [ 'preselectedTags' => [ 'foo', 'bar' ] ]);
        $this->assertInstanceOf(CheckboxList::class, $item->getChildrenItems()[0]);
        $values = $this->getStateValuesForQuery($item->getChildrenItems()[0]);
        $this->assertCount(2, $values);
        $this->assertEquals('foo', $values[0]);
        $this->assertEquals('bar', $values[1]);
    }


    public function testOverridenItemIsAppliedToQueryWhenHidden(): void
    {
        [ $first, $second, $third ] = (new ExampleAttributeFactory($this->objectManager))->createMultiple(3);
        $withFirst = (new ExampleEntityFactory($this->objectManager))->create([ 'attributes' => [ $first ] ]);
        $withSecond = (new ExampleEntityFactory($this->objectManager))->create([ 'attributes' => [ $second ] ]);
        $withBoth = (new ExampleEntityFactory($this->objectManager))->create([ 'attributes' => [ $first, $second ] ]);

        $config = [
            'combine' => 'AND',
            'join' => [
                [ 'column' => 'entity.attributes', 'alias' => 'attributes' ],
            ],
            'children' => [
                'attributes' => [
                    'type' => 'checkbox_list',
                    'databaseColumn' => 'attributes',
                    'queryString' => 'attributes',
                    'combine' => 'AND',
                    'dataSource' => 'newland-neosfiltering-tests-example',
                    'dataSourceArguments' => [
                        [ 'label' => 'First', 'value' => $first->uuid ],
                        [ 'label' => 'Second', 'value' => $second->uuid ],
                        [ 'label' => 'Third', 'value' => $third->uuid ],
                    ],
                    'override' => [
                        'property' => 'preselectedAttributes',
                        'behaviour' => 'hide',
                    ],
                ],
            ],
        ];

        $item = $this->subject->createFilter($config, [ 'preselectedAttributes' => [ $first->uuid ] ]);
        $query = $this->entityManager->createQueryBuilder()->select('entity')->from(ExampleEntity::class, 'entity');
        $ids = array_map(
            function (ExampleEntity $entity) {
                return $entity->uuid;
            },
            $item->applyToQuery($query)->getQuery()->execute()
        );
        $this->assertCount(2, $ids);
        $this->assertContains($withFirst->uuid, $ids);
        $this->assertContains($withBoth->uuid, $ids);


        $item = $this->subject->createFilter($config, [ 'preselectedAttributes' => [ $first->uuid, $second->uuid ] ]);
        $query = $this->entityManager->createQueryBuilder()->select('entity')->from(ExampleEntity::class, 'entity');
        $ids = array_map(
            function (ExampleEntity $entity) {
                return $entity->uuid;
            },
            $item->applyToQuery($query)->getQuery()->execute()
        );
        $this->assertCount(1, $ids);
        $this->assertContains($withBoth->uuid, $ids);
    }

    public function testOverridenItemIsAppliedToQueryWhenNotHidden(): void
    {

        [ $first, $second, $third ] = (new ExampleAttributeFactory($this->objectManager))->createMultiple(3);
        $withFirst = (new ExampleEntityFactory($this->objectManager))->create([ 'attributes' => [ $first ] ]);
        $withSecond = (new ExampleEntityFactory($this->objectManager))->create([ 'attributes' => [ $second ] ]);
        $withBoth = (new ExampleEntityFactory($this->objectManager))->create([ 'attributes' => [ $first, $second ] ]);

        $config = [
            'combine' => 'AND',
            'join' => [
                [ 'column' => 'entity.attributes', 'alias' => 'attributes' ],
            ],
            'children' => [
                'attributes' => [
                    'type' => 'checkbox_list',
                    'databaseColumn' => 'attributes',
                    'queryString' => 'attributes',
                    'combine' => 'AND',
                    'dataSource' => 'newland-neosfiltering-tests-example',
                    'dataSourceArguments' => [
                        [ 'label' => 'First', 'value' => $first->uuid ],
                        [ 'label' => 'Second', 'value' => $second->uuid ],
                        [ 'label' => 'Third', 'value' => $third->uuid ],
                    ],
                    'override' => [
                        'property' => 'preselectedAttributes',
                        'behaviour' => 'show',
                    ],
                ],
            ],
        ];

        $item = $this->subject->createFilter($config, [ 'preselectedAttributes' => [ $first->uuid ] ]);
        $query = $this->entityManager->createQueryBuilder()->select('entity')->from(ExampleEntity::class, 'entity');
        $ids = array_map(
            function (ExampleEntity $entity) {
                return $entity->uuid;
            },
            $item->applyToQuery($query)->getQuery()->execute()
        );
        $this->assertCount(2, $ids);
        $this->assertContains($withFirst->uuid, $ids);
        $this->assertContains($withBoth->uuid, $ids);


        $item = $this->subject->createFilter($config, [ 'preselectedAttributes' => [ $first->uuid, $second->uuid ] ]);
        $query = $this->entityManager->createQueryBuilder()->select('entity')->from(ExampleEntity::class, 'entity');
        $ids = array_map(
            function (ExampleEntity $entity) {
                return $entity->uuid;
            },
            $item->applyToQuery($query)->getQuery()->execute()
        );
        $this->assertCount(1, $ids);
        $this->assertContains($withBoth->uuid, $ids);
    }


    public function testOverriddenItemIsChangegableThroughQueryStringWhenNotHidden(): void
    {
        $config = [
            'combine' => 'AND',
            'children' => [
                'tags' => [
                    'type' => 'checkbox_list',
                    'databaseColumn' => 'entity.tags',
                    'queryString' => 'tags',
                    'combine' => 'AND',
                    'defaultState' => [ 'foo' ],
                    'dataSource' => 'newland-neosfiltering-tests-example',
                    'dataSourceArguments' => [
                        [ 'value' => 'foo', 'label' => 'Foo' ],
                        [ 'value' => 'bar', 'label' => 'Bar' ],
                        [ 'value' => 'baz', 'label' => 'Baz' ],
                    ],
                    'override' => [
                        'property' => 'preselectedTags',
                        'behaviour' => 'show',
                    ],
                ],
            ],
        ];

        // No override: Should be able to change.
        $root = $this->subject->createFilter($config);
        $this->assertCount(1, $root->getChildrenItems()[0]->getState());
        $this->assertEquals('foo', $root->getChildrenItems()[0]->getState()[0]['value']);
        $root->setState([ 'tags' => [ 'bar' ] ]);
        $this->assertCount(1, $root->getChildrenItems()[0]->getState());
        $this->assertEquals('bar', $root->getChildrenItems()[0]->getState()[0]['value']);

        // Override: Should still be able to change.
        $root = $this->subject->createFilter($config, [ 'preselectedTags' => [ 'baz' ] ]);
        $this->assertCount(1, $root->getChildrenItems()[0]->getState());
        $this->assertEquals('baz', $root->getChildrenItems()[0]->getState()[0]['value']);
        $root->setState([ 'tags' => [ 'bar' ] ]);
        $this->assertCount(1, $root->getChildrenItems()[0]->getState());
        $this->assertEquals('bar', $root->getChildrenItems()[0]->getState()[0]['value']);
    }

    public function testOverridingIgnoresDefaultState(): void
    {
        $config = [
            'type' => ItemMock::class,
            'databaseColumn' => 'entity.tags',
            'queryString' => 'tags',
            'combine' => 'AND',
            'defaultState' => [ 'foo' ],
            'range' => [ 'min' => 0, 'max' => 100 ],
            'override' => [
                'property' => 'preselectedTags',
                'behaviour' => 'show',
            ],
        ];

        $item = $this->createFilterItem($config);
        $this->assertEquals([ 'foo' ], $item->getState());

        $item = $this->createFilterItem($config, [ 'preselectedTags' => [ 'bar' ] ]);
        $this->assertEquals([ 'bar' ], $item->getState());
    }

    public function testIsHiddenInFrontend(): void
    {
        $config = [
            'type' => ItemMock::class,
            'databaseColumn' => 'entity.tags',
            'queryString' => 'tags',
            'combine' => 'AND',
            'defaultState' => [ 'foo' ],
            'range' => [ 'min' => 0, 'max' => 100 ],
            'hideInFrontend' => true
        ];

        $this->assertInstanceOf(ObstructionWrapper::class, $this->createFilterItem($config));
    }

    protected function render(FilterItem $item): ?string
    {
        $view = new StandaloneView();
        $view->assign('item', $item);
        $view->setTemplateSource('<f:render renderable="{item}" />');
        return $view->render();
    }


    private function createFilterItem(array $config, array $overrides = [], Root $root = null): FilterItem
    {
        $root = $root ?? new Root();
        return $this->subject->createFilterItem($config, $overrides, $root);
    }

    private function getStateValuesForQuery($item): array
    {
        if ($item instanceof ObstructionWrapper) {
            $item = $item->getObstructed();
        }
        $class = new \ReflectionClass(get_class($item));
        if ($class->hasMethod('getStateValuesForQuery')) {
            $method = $class->getMethod('getStateValuesForQuery');
        } else {
            $method = $class->getMethod('getStateValues');
        }
        $method->setAccessible(true);
        return $method->getClosure($item)();
    }

}
