<?php declare(strict_types=1);

namespace Newland\NeosFiltering\Tests\Unit\Items;

use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\Query\Expr\Comparison;
use Doctrine\ORM\Query\Expr\Orx;
use Newland\Contracts\Neos\Filter\Expression;
use Newland\NeosFiltering\Items\Group;
use Newland\NeosFiltering\Tests\Factory\ExampleEntityFactory;
use Newland\NeosFiltering\Tests\Fixture\ItemMock;

class GroupTest extends ItemTestCase
{

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

    public function setUp(): void
    {
        parent::setUp();
        $this->subject = new Group();
    }

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

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

    public function testDoesNotCreateExpressionIfNoChildrenSupplied(): void
    {
        $expression = $this->subject->queryExpression($this->entityManager->getExpressionBuilder());
        $this->assertNull($expression->where);
        $this->assertNull($expression->having);
    }

    public function testReturnsSimpleExpressionIfOneChildSupplied(): void
    {
        $expression = Expression::where(new Comparison('foo', 'bar', 'baz'));
        $this->subject->addItem(new ItemMock('', function() use ($expression) { return $expression; }));

        $this->assertEquals(
            $expression,
            $this->subject->queryExpression($this->entityManager->getExpressionBuilder())
        );
    }

    public function testReturnsSimpleExpressionIfOnlyOneChildReturnsNonNull(): void
    {
        $expression = Expression::where(new Comparison('foo', 'bar', 'baz'));
        $this->subject->addItem(new ItemMock('', function() use ($expression) { return $expression; }));
        $this->subject->addItem(new ItemMock('', function() { return Expression::empty(); }));

        $this->assertEquals(
            $expression,
            $this->subject->queryExpression($this->entityManager->getExpressionBuilder())
        );
    }

    public function testCreatesOrExpression(): void
    {
        $this->subject->addItem(new ItemMock());
        $this->subject->addItem(new ItemMock('', function() { return Expression::empty(); }));
        $this->subject->addItem(new ItemMock());

        $this->subject->setCombine(Group::$OR);
        $expression = $this->subject->queryExpression($this->entityManager->getExpressionBuilder());

        $this->assertInstanceOf(Orx::class, $expression->where);
        $this->assertCount(2, $expression->where->getParts());
    }

    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')));
        }));
        $this->subject->addItem(new ItemMock('', function($expr) {
            return Expression::where($expr->eq('entity.name', $expr->literal('bar')));
        }));

        $this->subject->setCombine(Group::$AND);
        $ids = array_map(
            function($item) { return $item->uuid; },
            $this->queryForSubject()->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 testCreatesAndExpression(): void
    {
        $this->subject->addItem(new ItemMock());
        $this->subject->addItem(new ItemMock('', function() { return Expression::empty(); }));
        $this->subject->addItem(new ItemMock());

        $this->subject->setCombine(Group::$AND);
        $expression = $this->subject->queryExpression($this->entityManager->getExpressionBuilder());

        $this->assertInstanceOf(Andx::class, $expression->where);
        $this->assertCount(2, $expression->where->getParts());
    }

    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')));
        }));
        $this->subject->addItem(new ItemMock('', function($expr) {
            return Expression::where($expr->eq('entity.name', $expr->literal('bar')));
        }));

        $this->subject->setCombine(Group::$OR);
        $ids = array_map(
            function($item) { return $item->uuid; },
            $this->queryForSubject()->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 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]);
    }

}
