<?php

namespace Velis\Test\Db;

use PHPUnit\Framework\TestCase;
use ReflectionException;
use Velis\App;
use Velis\Bpm\Ticket\Category;
use Velis\Bpm\Ticket\Classification;
use Velis\Bpm\Ticket\Status;
use Velis\Bpm\Ticket\Ticket;
use Velis\Db\EntityRepository;
use Velis\Model\DataObject;
use Velis\User;

class EntityRepositoryTest extends TestCase
{
    /**
     * @param class-string<DataObject> $entityClass
     * @throws ReflectionException
     *
     * @dataProvider provideTestDataForFindMethod
     */
    public function testFind(string $entityClass, int|string $id): void
    {
        $repository = $this->getRepository($entityClass);

        $entity = $repository->find($id);

        $this->assertInstanceOf($entityClass, $entity);
        $this->assertEquals($id, $entity->id());
    }

    /**
     * @param class-string $entityClass
     */
    private function getRepository(string $entityClass): EntityRepository
    {
        return App::$di['db']->getRepository($entityClass);
    }

    public function provideTestDataForFindMethod(): array
    {
        return [
            [User::class, 1],
            [Status::class, 'Closed'],
        ];
    }

    /**
     * @throws ReflectionException
     */
    public function testFindWhenIdDoesNotExist(): void
    {
        $repository = $this->getRepository(User::class);
        $result = $repository->find(-1);

        $this->assertNull($result);
    }

    public function testFindWhenIdIsOutOfRange(): void
    {
        $repository = $this->getRepository(User::class);
        $result = $repository->find(999999999999);

        $this->assertNull($result);
    }

    /**
     * @param class-string $entityClass
     * @throws ReflectionException
     *
     * @dataProvider provideTestDataForFindAllMethod
     */
    public function testFindAll(string $entityClass): void
    {
        $repository = $this->getRepository($entityClass);

        $entities = $repository->findAll();

        $this->assertNotEmpty($entities);
        $this->assertInstanceOf($entityClass, reset($entities));
    }

    public function provideTestDataForFindAllMethod(): array
    {
        return [
            [Status::class],
        ];
    }

    /**
     * @param class-string $entityClass
     * @param array<string,mixed> $criteria
     * @throws ReflectionException
     *
     * @dataProvider provideTestDataForFindByMatchesCriteria
     */
    public function testFindByMatchesCriteria(string $entityClass, array $criteria): void
    {
        $repository = $this->getRepository($entityClass);

        $entities = $repository->findBy($criteria);

        $this->assertNotEmpty($entities);
        $entity = reset($entities);

        foreach ($criteria as $key => $value) {
            $this->assertArrayHasKey($key, $entity);
            $this->assertEquals($value, $entity[$key]);
        }
    }

    public function provideTestDataForFindByMatchesCriteria(): array
    {
        return [
            [
                Classification::class,
                [
                    'internal_classification' => 1,
                ],
            ],
        ];
    }

    /**
     * @param class-string<DataObject> $entityClass
     * @param array<int|string> $values
     * @throws ReflectionException
     *
     * @dataProvider provideTestDataForFindByMatchesArrayCriteria
     */
    public function testFindByMatchesArrayCriteria(string $entityClass, string $fieldName, array $values): void
    {
        $repository = $this->getRepository($entityClass);

        $entities = $repository->findBy([
            $fieldName => $values,
        ]);

        $this->assertNotEmpty($entities);

        if (empty($values)) {
            return;
        }

        $foundValues = [];

        foreach ($entities as $entity) {
            $value = $entity[$fieldName];
            $this->assertContains($value, $values);

            if (!in_array($value, $foundValues)) {
                $foundValues[] = $value;
            }
        }

        foreach ($values as $value) {
            $this->assertContains($value, $foundValues);
        }
    }

    public function provideTestDataForFindByMatchesArrayCriteria(): array
    {
        return [
            [Status::class, 'ticket_status_id', ['Done', 'Closed']],
            [User::class, 'user_id', [1]],
        ];
    }

    /**
     * @param class-string<DataObject> $entityClass
     * @throws ReflectionException
     *
     * @dataProvider provideFindByWhenCriterionIsAnEmptyArray
     */
    public function testFindByWhenCriterionIsAnEmptyArray(string $entityClass, string $fieldName): void
    {
        $repository = $this->getRepository($entityClass);

        $entities = $repository->findBy([
            $fieldName => [],
        ]);

        $this->assertEmpty($entities);
    }

    /**
     * @return array<array{0: class-string<DataObject>, 1: string}>
     */
    public function provideFindByWhenCriterionIsAnEmptyArray(): array
    {
        return [
            [Status::class, 'ticket_status_id'],
        ];
    }

    /**
     * @param class-string $entityClass
     * @param array<string,string> $orderBy
     * @throws ReflectionException
     *
     * @dataProvider provideTestDataForFindByAppliesOrdering
     */
    public function testFindByAppliesOrdering(string $entityClass, array $orderBy): void
    {
        $repository = $this->getRepository($entityClass);

        $entities = $repository->findBy([], $orderBy);

        $firstEntity = reset($entities);
        $secondEntity = next($entities);

        $orderByColumns = array_keys($orderBy);
        $orderByColumn = $orderByColumns[0];
        $orderDirection = $orderBy[$orderByColumn];

        if (strtolower($orderDirection) == 'asc') {
            $this->assertGreaterThanOrEqual($firstEntity[$orderByColumn], $secondEntity[$orderByColumn]);
        } else {
            $this->assertLessThanOrEqual($firstEntity[$orderByColumn], $secondEntity[$orderByColumn]);
        }
    }

    public function provideTestDataForFindByAppliesOrdering(): array
    {
        return [
            [Category::class, ['ticket_category_id' => 'asc']],
            [Category::class, ['ticket_category_id' => 'desc']],
            [Status::class, ['ticket_status_id' => 'asc']],
        ];
    }

    /**
     * @param class-string $entityClass
     * @throws ReflectionException
     *
     * @dataProvider provideTestDataForFindByAppliesLimit
     */
    public function testFindByAppliesLimit(string $entityClass, int $limit): void
    {
        $repository = $this->getRepository($entityClass);

        $entities = $repository->findBy([], null, $limit);

        $this->assertLessThanOrEqual($limit, count($entities));
    }

    public function provideTestDataForFindByAppliesLimit(): array
    {
        return [
            [Category::class, 5],
            [Ticket::class, 5],
        ];
    }

    /**
     * @param class-string $entityClass
     * @throws ReflectionException
     *
     * @dataProvider provideTestDataForFindByAppliesOffset
     */
    public function testFindByAppliesOffset(string $entityClass, string $primaryKey, int $offset)
    {
        $repository = $this->getRepository($entityClass);

        $orderBy = [];
        $orderBy[$primaryKey] = 'asc';

        $entities1 = $repository->findBy([], $orderBy, 1, $offset);
        $entities2 = $repository->findBy([], $orderBy, $offset + 1);

        $entity1 = reset($entities1);
        $ids = array_keys($entities2);
        $entity2 = $entities2[$ids[$offset]];

        $this->assertEquals($entity1[$primaryKey], $entity2[$primaryKey]);
    }

    public function provideTestDataForFindByAppliesOffset(): array
    {
        return [
            [Ticket::class, 'ticket_id', 2],
            [Status::class, 'ticket_status_id', 10],
        ];
    }

    /**
     * @param class-string $entityClass
     * @param array<string,mixed> $criteria
     * @throws ReflectionException
     *
     * @dataProvider provideTestDataForFindOneByMatchesCriteria
     */
    public function testFindOneByMatchesCriteria(string $entityClass, array $criteria): void
    {
        $repository = $this->getRepository($entityClass);

        $entity = $repository->findOneBy($criteria);

        $this->assertInstanceOf($entityClass, $entity);

        foreach ($criteria as $key => $value) {
            $this->assertArrayHasKey($key, $entity);
            $this->assertEquals($value, $entity[$key]);
        }
    }

    public function provideTestDataForFindOneByMatchesCriteria(): array
    {
        return [
            [
                Status::class,
                [
                    'name_en' => 'Closed',
                ],
            ],
        ];
    }

    /**
     * @throws ReflectionException
     */
    public function testFindOneByReturnsNullWhenNotFound(): void
    {
        $repository = $this->getRepository(Status::class);

        $result = $repository->findOneBy([
            'ticket_status_id' => 'IncorrectStatus',
        ]);

        $this->assertNull($result);
    }

    /**
     * @param class-string $entityClass
     * @param array<string,mixed> $criteria
     * @throws ReflectionException
     *
     * @dataProvider provideTestDataForFindOneByAppliesOrdering
     */
    public function testFindOneByAppliesOrdering(string $entityClass, array $criteria, string $orderField): void
    {
        $repository = $this->getRepository($entityClass);

        $firstEntity = $repository->findOneBy($criteria, [$orderField => 'asc']);
        $lastEntity = $repository->findOneBy($criteria, [$orderField => 'desc']);

        $this->assertLessThan($lastEntity[$orderField], $firstEntity[$orderField]);
    }

    public function provideTestDataForFindOneByAppliesOrdering(): array
    {
        return [
            [
                Classification::class,
                [
                    'ticket_type_id' => 'Building',
                ],
                'ticket_classification_id',
            ],
        ];
    }

    /**
     * @throws ReflectionException
     */
    public function testCount(): void
    {
        $repository = $this->getRepository(Ticket::class);

        $count = $repository->count([
            'ticket_type_id' => 'Building',
        ]);

        $this->assertGreaterThan(0, $count);
    }
}
