<?php

namespace NoahVet\Reef\Test\A_Unit\Repository;

use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use NoahVet\Reef\Entity\EntityInterface;
use NoahVet\Reef\Repository\AbstractBaseRepository;
use NoahVet\Reef\Traits\Entity\Required\IdTrait;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\Uuid;

final class AbstractBaseRepositoryTest extends TestCase
{
    private AbstractBaseRepository $subject;

    protected function setUp(): void
    {
        parent::setUp();
        $this->subject = new class extends AbstractBaseRepository {
            public function __construct()
            {
                $registry = new class implements ManagerRegistry {
                    public function getDefaultConnectionName(): void
                    {
                    }

                    public function getConnection(?string $name = null): void
                    {
                    }

                    public function getConnections(): void
                    {
                    }

                    public function getConnectionNames(): void
                    {
                    }

                    public function getDefaultManagerName(): void
                    {
                    }

                    public function getManager(?string $name = null): void
                    {
                    }

                    public function getManagers(): void
                    {
                    }

                    public function resetManager(?string $name = null): void
                    {
                    }

                    public function getManagerNames(): void
                    {
                    }

                    public function getRepository(string $persistentObject, ?string $persistentManagerName = null): void
                    {
                    }

                    public function getManagerForClass(string $class): void
                    {
                    }
                };
                $entity = new class implements EntityInterface {
                    use IdTrait;
                };
                parent::__construct(
                    $registry,
                    $entity::class,
                );
            }

            public function setFilters(array $filters): void
            {
                $this->filters = $filters;
            }

            public function getFilters(): array
            {
                return $this->filters;
            }

            public function getQbFilterConditions(): array
            {
                return $this->qbFilterConditions;
            }
        };
    }

    public function testConvertUuidFilterValuesReturnsOriginalValuesWhenFilterIsNotId(): void
    {
        $this->assertSame(
            ['foo', 123, false],
            $this->subject->convertUuidFilterValues(
                'name',
                ['foo', 123, false],
            ),
        );
    }

    /**
     * @dataProvider idFilterKeysProvider
     */
    public function testConvertUuidFilterValuesConvertsValidUuidStrings(string $filterKey): void
    {
        $validUuid = '1f0a296c-38c2-6d70-8f34-91024dd16617';
        $binaryUuid = Uuid::fromRfc4122($validUuid)->toBinary();

        $values = [$validUuid, 'not-an-uuid', 42];

        $result = $this->subject->convertUuidFilterValues($filterKey, $values);

        $this->assertSame([$binaryUuid, 'not-an-uuid', 42], $result);
    }

    /**
     * @return iterable<array<int, string>>
     */
    public static function idFilterKeysProvider(): iterable
    {
        yield ['ownerId'];
        yield ['ownerIds'];
    }

    public function testFilterByTranslatableJsonReturnsQbUnchangedWhenFieldNotInFilters(): void
    {
        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects($this->never())->method('expr');
        $qb->expects($this->never())->method('setParameter');

        $this->subject->setLocale('fr');

        $result = $this->subject->filterByTranslatableJson($qb, 'entitled');

        $this->assertSame($qb, $result);
        $this->assertEmpty($this->subject->getQbFilterConditions());
    }

    public function testFilterByTranslatableJsonAddsConditionForSingleValue(): void
    {
        $expectedCondition = 'JSON_EXTRACT(e.translations, :entitledPath) = :entitledValue0';

        $expr = $this->createMock(Expr::class);
        $expr->expects($this->once())
            ->method('eq')
            ->with('JSON_EXTRACT(e.translations, :entitledPath)', ':entitledValue0')
            ->willReturn($expectedCondition)
        ;

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->method('setParameter')->willReturnSelf();

        $this->subject->setLocale('fr');
        $this->subject->setFilters(['entitled' => ['Mon titre']]);

        $result = $this->subject->filterByTranslatableJson($qb, 'entitled');

        $this->assertSame($qb, $result);

        $conditions = $this->subject->getQbFilterConditions();
        $this->assertArrayHasKey('entitled', $conditions);
        $this->assertCount(1, $conditions['entitled']);
        $this->assertSame($expectedCondition, $conditions['entitled'][0]);
    }

    public function testFilterByTranslatableJsonSetsCorrectPathAndValueParameters(): void
    {
        $capturedParameters = [];

        $expr = $this->createMock(Expr::class);
        $expr->method('eq')->willReturn('condition');

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->expects($this->exactly(2))
            ->method('setParameter')
            ->willReturnCallback(static function (string $name, mixed $value) use ($qb, &$capturedParameters) {
                $capturedParameters[$name] = $value;

                return $qb;
            })
        ;

        $this->subject->setLocale('fr');
        $this->subject->setFilters(['entitled' => ['Mon titre']]);

        $this->subject->filterByTranslatableJson($qb, 'entitled');

        $this->assertSame('$.fr.entitled', $capturedParameters['entitledPath']);
        $this->assertSame('Mon titre', $capturedParameters['entitledValue0']);
    }

    public function testFilterByTranslatableJsonRemovesFieldFromFiltersAfterProcessing(): void
    {
        $expr = $this->createMock(Expr::class);
        $expr->method('eq')->willReturn('condition');

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->method('setParameter')->willReturnSelf();

        $this->subject->setLocale('fr');
        $this->subject->setFilters(['entitled' => ['Mon titre'], 'other' => ['value']]);

        $this->subject->filterByTranslatableJson($qb, 'entitled');

        $filters = $this->subject->getFilters();
        $this->assertArrayNotHasKey('entitled', $filters);
        $this->assertArrayHasKey('other', $filters);
    }

    public function testFilterByTranslatableJsonSkipsNullStringValues(): void
    {
        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects($this->never())->method('expr');
        $qb->expects($this->never())->method('setParameter');

        $this->subject->setLocale('fr');
        $this->subject->setFilters(['entitled' => ['null', 'null']]);

        $result = $this->subject->filterByTranslatableJson($qb, 'entitled');

        $this->assertSame($qb, $result);
        $this->assertArrayNotHasKey('entitled', $this->subject->getQbFilterConditions());
        $this->assertArrayNotHasKey('entitled', $this->subject->getFilters());
    }

    public function testFilterByTranslatableJsonSkipsNullValuesInMixedList(): void
    {
        $expr = $this->createMock(Expr::class);
        $expr->expects($this->once())
            ->method('eq')
            ->with('JSON_EXTRACT(e.translations, :titlePath)', ':titleValue0')
            ->willReturn('condition')
        ;

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->method('setParameter')->willReturnSelf();

        $this->subject->setLocale('fr');
        $this->subject->setFilters(['title' => ['null', 'Real value', 'null']]);

        $this->subject->filterByTranslatableJson($qb, 'title');

        $conditions = $this->subject->getQbFilterConditions();
        $this->assertArrayHasKey('title', $conditions);
        $this->assertCount(1, $conditions['title']);
    }

    public function testFilterByTranslatableJsonCreatesOneConditionPerNonNullValue(): void
    {
        $expr = $this->createMock(Expr::class);
        $expr->expects($this->exactly(2))
            ->method('eq')
            ->willReturn('condition')
        ;

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->method('setParameter')->willReturnSelf();

        $this->subject->setLocale('en');
        $this->subject->setFilters(['name' => ['Value 1', 'Value 2']]);

        $this->subject->filterByTranslatableJson($qb, 'name');

        $conditions = $this->subject->getQbFilterConditions();
        $this->assertArrayHasKey('name', $conditions);
        $this->assertCount(2, $conditions['name']);
        $this->assertArrayNotHasKey('name', $this->subject->getFilters());
    }

    public function testFilterByTranslatableJsonIncrementsParameterNameForEachValue(): void
    {
        $capturedParameters = [];

        $expr = $this->createMock(Expr::class);
        $expr->method('eq')->willReturn('condition');

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->method('setParameter')
            ->willReturnCallback(static function (string $name, mixed $value) use ($qb, &$capturedParameters) {
                $capturedParameters[$name] = $value;

                return $qb;
            })
        ;

        $this->subject->setLocale('en');
        $this->subject->setFilters(['name' => ['First', 'Second']]);

        $this->subject->filterByTranslatableJson($qb, 'name');

        $this->assertSame('First', $capturedParameters['nameValue0']);
        $this->assertSame('Second', $capturedParameters['nameValue1']);
        $this->assertSame('$.en.name', $capturedParameters['namePath']);
    }

    public function testFilterByJsonEntitledDelegatesToFilterByTranslatableJsonWithEntitledField(): void
    {
        $capturedParameters = [];

        $expr = $this->createMock(Expr::class);
        $expr->method('eq')->willReturn('condition');

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->method('setParameter')
            ->willReturnCallback(static function (string $name, mixed $value) use ($qb, &$capturedParameters) {
                $capturedParameters[$name] = $value;

                return $qb;
            })
        ;

        $this->subject->setLocale('en');
        $this->subject->setFilters(['entitled' => ['Test title']]);

        $result = $this->subject->filterByJsonEntitled($qb);

        $this->assertSame($qb, $result);
        $this->assertArrayHasKey('entitled', $this->subject->getQbFilterConditions());
        $this->assertSame('$.en.entitled', $capturedParameters['entitledPath']);
        $this->assertSame('Test title', $capturedParameters['entitledValue0']);
        $this->assertArrayNotHasKey('entitled', $this->subject->getFilters());
    }

    public function testFilterByJsonEntitledReturnsQbUnchangedWhenEntitledNotInFilters(): void
    {
        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects($this->never())->method('expr');
        $qb->expects($this->never())->method('setParameter');

        $this->subject->setLocale('fr');

        $result = $this->subject->filterByJsonEntitled($qb);

        $this->assertSame($qb, $result);
        $this->assertEmpty($this->subject->getQbFilterConditions());
    }
}
