<?php declare(strict_types=1);

namespace Newland\NeosFiltering\Tests\Unit\Items;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Newland\NeosFiltering\Items\CheckboxList;
use Newland\NeosFiltering\Items\Root;
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\Domain\Model\ExampleEntity;

class CheckboxListTest extends ItemTestCase
{
    /** @var CheckboxList */
    protected $subject;

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

        $this->subject = new CheckboxList();
        $this->subject->setRoot(new Root());
        $this->subject->setDatabaseColumn('entity.database_column');
        $this->subject->setQueryString('query_string');
        $this->subject->setTitle('Title');
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'Foo', 'value' => 'foo' ],
                [ 'label' => 'Bar', 'value' => 'bar' ],
                [ 'label' => 'Baz', 'value' => 'baz' ],
            ]
        );
    }

    public function testRendersCheckboxesForEachOption(): void
    {
        $result = $this->renderSubject();
        $this->assertContainsCount('type="checkbox"', 3, $result);
        $this->assertContainsCount('name="query_string[]"', 3, $result);
        $this->assertContainsCount('value="foo"', 1, $result);
        $this->assertContainsCount('value="bar"', 1, $result);
        $this->assertContainsCount('value="baz"', 1, $result);
    }

    public function testChecksSelectedCheckboxes(): void
    {
        $this->subject->setState([ 'foo', 'bar' ]);
        $result = $this->renderSubject();
        $this->assertContainsCount('checked="checked"', 2, $result);

        $this->subject->setState([ 'foo' ]);
        $result = $this->renderSubject();
        $this->assertContainsCount('checked="checked"', 1, $result);

        $this->subject->setState([]);
        $result = $this->renderSubject();
        $this->assertNotContains('checked="checked"', $result);
    }

    public function testUsesValuesFromDataSource(): void
    {
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'First', 'value' => 'first' ],
                [ 'label' => 'Second', 'value' => 'second' ],
                [ 'label' => 'Third', 'value' => 'third' ],
            ]
        );

        $rendered = $this->renderSubject();
        $this->assertContains('value="first"', $rendered);
        $this->assertContains('value="second"', $rendered);
        $this->assertContains('value="third"', $rendered);
    }

    public function testUsesLabelsFromDataSource(): void
    {
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'LabelFirst', 'value' => 'first' ],
                [ 'label' => 'LabelSecond', 'value' => 'second' ],
                [ 'label' => 'LabelThird', 'value' => 'third' ],
            ]
        );

        $rendered = $this->renderSubject();
        $this->assertContains('LabelFirst', $rendered);
        $this->assertContains('LabelSecond', $rendered);
        $this->assertContains('LabelThird', $rendered);
    }

    public function testRendersTitle(): void
    {
        $this->subject->setTitle('THIS IS A TITLE');
        $this->assertContains('THIS IS A TITLE', $this->renderSubject());
    }

    public function testDoesNotGenerateExpressionIfNothingSelected(): void
    {
        $this->subject->setState([]);

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

        $this->assertNull($expr->where);
        $this->assertNull($expr->having);
    }

    public function testFiltersForItemsIn(): void
    {
        [ $foo1, $foo2 ] = (new ExampleEntityFactory($this->objectManager))->createMultiple(2, [ 'tag' => 'foo' ]);
        $bar = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'bar' ]);
        $baz = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'baz' ]);

        $this->subject->setDatabaseColumn('entity.tag');
        $this->subject->setState([ 'foo', 'baz' ]);

        $result = $this->queryForSubject()->getQuery()->execute();
        $ids = array_map(
            function (ExampleEntity $entity) {
                return $entity->uuid;
            },
            $result
        );

        $this->assertContains($foo1->uuid, $ids);
        $this->assertContains($foo2->uuid, $ids);
        $this->assertNotContains($bar->uuid, $ids);
        $this->assertContains($baz->uuid, $ids);
    }

    public function testFiltersForRelationsAnd(): void
    {
        [ $cat1, $cat2 ] = (new ExampleCategoryFactory($this->objectManager))->createMultiple(2);
        $relatedToFirst = (new ExampleEntityFactory($this->objectManager))->create([ 'categories' => [ $cat1 ] ]);
        $relatedToSecond = (new ExampleEntityFactory($this->objectManager))->create([ 'categories' => [ $cat2 ] ]);
        $relatedToBoth = (new ExampleEntityFactory($this->objectManager))->create([ 'categories' => [ $cat1, $cat2 ] ]);
        $relatedToNone = (new ExampleEntityFactory($this->objectManager))->create([ 'categories' => [] ]);

        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'first', 'value' => $cat1->uuid ],
                [ 'label' => 'second', 'value' => $cat2->uuid ],
            ]
        );
        $this->subject->setDatabaseColumn('categories');
        $this->subject->setState([ $cat1->uuid, $cat2->uuid ]);
        $this->subject->setCombine(CheckboxList::$AND);

        $query = $this->queryForSubject()->join('entity.categories', 'categories');
        $result = $query->getQuery()->execute();
        $ids = array_map(
            function (ExampleEntity $entity) {
                return $entity->uuid;
            },
            $result
        );

        $this->assertNotContains($relatedToFirst->uuid, $ids);
        $this->assertNotContains($relatedToSecond->uuid, $ids);
        $this->assertContains($relatedToBoth->uuid, $ids);
        $this->assertNotContains($relatedToNone->uuid, $ids);
    }

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

        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'first', 'value' => $cat1->uuid ],
                [ 'label' => 'second', 'value' => $cat2->uuid ],
            ]
        );
        $this->subject->setDatabaseColumn('categories');
        $this->subject->setState([ $cat1->uuid, $cat2->uuid ]);
        $this->subject->setCombine(CheckboxList::$AND);

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

        // The entity created above should not be found: It only has one of the two categories
        $this->assertCount(0, $result);
    }

    public function testInvalidStateIsNotAppliedToQuery(): void
    {
        $first = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'first' ]);
        $second = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'second' ]);
        $third = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'third' ]);
        $fourth = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'fourth' ]);

        // Notice how tag=fourth exists but is not in the data source
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDatabaseColumn('entity.tag');
        $this->subject->setCombine('OR');
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'First tag', 'value' => 'first' ],
                [ 'label' => 'Second tag', 'value' => 'second' ],
                [ 'label' => 'Third tag', 'value' => 'third' ],
            ]
        );

        $this->subject->setState([ 'first', 'fourth' ]);
        $ids = array_map(
            function ($item) {
                return $item->uuid;
            },
            $this->queryForSubject()->getQuery()->execute()
        );

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

    public function testUsesCustomQueryStringInRenderedOutput(): void
    {
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDatabaseColumn('entity.tag');
        $this->subject->setCombine('OR');
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'First tag', 'value' => 'first', 'queryString' => '__first__' ],
                [ 'label' => 'Second tag', 'value' => 'second', 'queryString' => '__second__' ],
                [ 'label' => 'Third tag', 'value' => 'third', 'queryString' => '__third__' ],
            ]
        );

        $this->subject->setState([ '__first__' ]);
        $this->assertContains('value="__first__"', $this->renderSubject());
        $this->assertContains('value="__second__"', $this->renderSubject());
        $this->assertContains('value="__third__"', $this->renderSubject());
    }

    public function testAllowsSettingStateUsingValueEventIfUsingCustomQueryString(): void
    {
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'First tag', 'value' => 'first', 'queryString' => '__first__' ],
                [ 'label' => 'Second tag', 'value' => 'second', 'queryString' => '__second__' ],
                [ 'label' => 'Third tag', 'value' => 'third', 'queryString' => '__third__' ],
            ]
        );

        $this->subject->setState([ 'first' ]);
        $this->assertEquals('__first__', $this->subject->getState()[0]['queryString'] ?? null);
    }

    public function testCorrectlyGetsResultsWhenUsingCustomQueryStrings(): void
    {
        $first = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'first' ]);
        $second = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'second' ]);
        $third = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'third' ]);
        $fourth = (new ExampleEntityFactory($this->objectManager))->create([ 'tag' => 'fourth' ]);

        // Notice how tag=fourth exists but is not in the data source
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDatabaseColumn('entity.tag');
        $this->subject->setCombine('OR');
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'First tag', 'value' => 'first', 'queryString' => '__first__' ],
                [ 'label' => 'Second tag', 'value' => 'second', 'queryString' => '__second__' ],
                [ 'label' => 'Third tag', 'value' => 'third', 'queryString' => '__third__' ],
            ]
        );

        $this->subject->setState([ '__first__', 'second' ]);
        $ids = array_map(
            function ($entity) {
                return $entity->uuid;
            },
            $this->queryForSubject()->getQuery()->execute()
        );

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

    public function testSuppliesNoStatusIndicatorsIfNothingInState(): void
    {
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'First tag', 'value' => 'first', 'queryString' => '__first__' ],
                [ 'label' => 'Second tag', 'value' => 'second', 'queryString' => '__second__' ],
                [ 'label' => 'Third tag', 'value' => 'third', 'queryString' => '__third__' ],
            ]
        );
        $this->assertEmpty($this->subject->getStatusIndicators());
    }

    public function testSuppliesStatusIndicatorsContainingLabel(): void
    {
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'First tag', 'value' => 'first', 'queryString' => '__first__' ],
                [ 'label' => 'Second tag', 'value' => 'second', 'queryString' => '__second__' ],
                [ 'label' => 'Third tag', 'value' => 'third', 'queryString' => '__third__' ],
            ]
        );
        $this->subject->setState([ '__first__', 'second' ]);

        $indicators = $this->subject->getStatusIndicators();
        $this->assertCount(2, $indicators);
        $this->assertEquals('First tag', $indicators[0]->getTitle());
        $this->assertEquals('Second tag', $indicators[1]->getTitle());
    }

    public function testUsesSameIdsForCheckboxesAsForStatusIndicators(): void
    {
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'First tag', 'value' => 'first', 'queryString' => '__first__' ],
                [ 'label' => 'Second tag', 'value' => 'second', 'queryString' => '__second__' ],
                [ 'label' => 'Third tag', 'value' => 'third', 'queryString' => '__third__' ],
            ]
        );
        $this->subject->setState([ '__first__', 'second' ]);

        $indicators = $this->subject->getStatusIndicators();
        $rendered = $this->renderSubject();

        $this->assertContains($indicators[0]->getId(), $rendered);
        $this->assertContains($indicators[1]->getId(), $rendered);
    }

    public function testDoesNotRenderSameItemTwiceIfInMultipleGroups(): void
    {
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'First tag', 'value' => 'first', 'group' => 'foo' ],
                [ 'label' => 'First tag', 'value' => 'first', 'group' => 'bar' ],
                [ 'label' => 'Second tag', 'value' => 'second', 'group' => 'foo' ],
            ]
        );

        $rendered = $this->renderSubject();
        $this->assertContainsCount('value="first"', 1, $rendered);
    }

    public function testDoesNotProvideMultipleFitlerItemsForItemsInSameGroup(): void
    {
        $this->subject->setDataSource(new ExampleDataSource());
        $this->subject->setDataSourceArguments(
            [
                [ 'label' => 'First tag', 'value' => 'first', 'group' => 'foo' ],
                [ 'label' => 'First tag', 'value' => 'first', 'group' => 'bar' ],
                [ 'label' => 'Second tag', 'value' => 'second', 'group' => 'foo' ],
            ]
        );


        $this->subject->setState([ 'first' ]);
        $indicators = $this->subject->getStatusIndicators();
        $this->assertCount(1, $indicators);
    }
}
