<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\MainBundle\Test\Export;

use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\ExportGenerationContext;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

/**
 *  Helper to test filters.
 */
abstract class AbstractFilterTest extends KernelTestCase
{
    use ProphecyTrait;

    public static function tearDownAfterClass(): void
    {
        if (null !== self::getContainer()) {
            /** @var EntityManagerInterface $em */
            $em = self::getContainer()->get(EntityManagerInterface::class);
            $em->getConnection()->close();
        }
        self::ensureKernelShutdown();
    }

    protected function getUser(): User
    {
        $em = static::getContainer()->get(EntityManagerInterface::class);
        if (null === $user = $em->createQueryBuilder()->select('u')
            ->from(User::class, 'u')
            ->setMaxResults(1)
            ->getQuery()->getOneOrNullResult()) {
            throw new \RuntimeException('User not found');
        }

        return $user;
    }

    /**
     * Create a filter which will be used in tests.
     *
     * This method is always used after an eventuel `setUp` method.
     *
     * @return \Chill\MainBundle\Export\FilterInterface
     */
    abstract public function getFilter();

    /**
     * Create possible combinaison of data (produced by the form).
     *
     * This data will be used to generate data providers using this data.
     *
     * As all data providers, this method is executed **before** calling
     * the `setUp` method.
     *
     * @return array an array of data. Example : `array( array(), array('fields' => array(1,2,3), ...)` where an empty array and `array(1,2,3)` are possible values
     */
    abstract public static function getFormData();

    /**
     * Return an array with different minimal query builders.
     *
     * As all data providers, this method is executed **before** calling
     * the `setUp` method.
     *
     * @return QueryBuilder[] an array of query builder
     */
    abstract public static function getQueryBuilders();

    /**
     * @dataProvider dataProviderFormDataToNormalize
     */
    public function testDataNormalization(array $data, int $version, array $customAssert): void
    {
        $filter = $this->getFilter();

        $normalized = $filter->normalizeFormData($data);
        $actual = $filter->denormalizeFormData($normalized, $version);

        self::assertEqualsCanonicalizing(array_keys($data), array_keys($actual));

        foreach ($data as $key => $value) {
            self::assertArrayHasKey($key, $actual);
            if (array_key_exists($key, $customAssert)) {
                call_user_func($customAssert[$key], $actual[$key], $value);
            } elseif (is_iterable($value)) {
                continue;
            } elseif (is_object($value) && method_exists($value, 'getId')) {
                self::assertEquals($value->getId(), $actual[$key]->getId());
            } else {
                self::assertEquals($value, $actual[$key]);
            }
        }
    }

    /**
     * A list of data to normalize.
     *
     * @return iterable{array}
     */
    public static function dataProviderFormDataToNormalize(): iterable
    {
        foreach (static::getFormData() as $data) {
            yield [$data, 1, []];
        }
    }

    /**
     * Compare aliases array before and after that filter alter query.
     *
     * @dataProvider dataProviderAliasDidNotDisappears
     */
    public function testAliasDidNotDisappears(QueryBuilder $qb, array $data): void
    {
        $aliases = $qb->getAllAliases();

        $this->getFilter()->alterQuery($qb, $data, new ExportGenerationContext($this->getUser()));

        $alteredQuery = $qb->getAllAliases();

        $this->assertGreaterThanOrEqual(\count($aliases), \count($alteredQuery));

        foreach ($aliases as $alias) {
            $this->assertContains($alias, $alteredQuery);
        }
    }

    /**
     * provide data for `testAliasDidNotDisappears`.
     */
    public static function dataProviderAliasDidNotDisappears()
    {
        $datas = static::getFormData();

        foreach (static::getQueryBuilders() as $qb) {
            if ([] === $datas) {
                yield [clone $qb, []];
            } else {
                foreach ($datas as $data) {
                    yield [clone $qb, $data];
                }
            }
        }
    }

    /**
     * test the alteration of query by the filter.
     *
     * @dataProvider dataProviderAlterQuery
     *
     * @group dbIntensive
     */
    public function testAlterQuery(QueryBuilder $query, array $data)
    {
        // retains informations about query
        $nbOfFrom = null !== $query->getDQLPart('from') ?
            \count($query->getDQLPart('from')) : 0;
        $nbOfWhere = null !== $query->getDQLPart('where') ?
            $query->getDQLPart('where')->count() : 0;
        $nbOfSelect = null !== $query->getDQLPart('select') ?
            \count($query->getDQLPart('select')) : 0;

        $context = new ExportGenerationContext($this->getUser());

        $this->getFilter()->alterQuery($query, $data, $context);

        $this->assertGreaterThanOrEqual(
            $nbOfFrom,
            null !== $query->getDQLPart('from') ? \count($query->getDQLPart('from')) : 0,
            "Test that there are equal or more 'from' clause after that the filter has
                altered the query"
        );
        $this->assertGreaterThanOrEqual(
            $nbOfWhere,
            null !== $query->getDQLPart('where') ? $query->getDQLPart('where')->count() : 0,
            "Test that there are equal or more 'where' clause after that the filter has"
            .'altered the query'
        );
        $this->assertEquals(
            $nbOfSelect,
            null !== $query->getDQLPart('select') ? \count($query->getDQLPart('select')) : 0,
            "Test that the filter has no altered the 'select' part of the query"
        );
    }

    public static function dataProviderAlterQuery()
    {
        $datas = static::getFormData();

        foreach (static::getQueryBuilders() as $qb) {
            if ([] === $datas) {
                yield [clone $qb, []];
            } else {
                foreach ($datas as $data) {
                    yield [clone $qb, $data];
                }
            }
        }
    }

    /**
     * @dataProvider dataProvideQueryExecution
     */
    public function testQueryExecution(QueryBuilder $qb, mixed $data): void
    {
        $this->getFilter()->alterQuery($qb, $data, new ExportGenerationContext($this->getUser()));

        $actual = $qb->getQuery()->getResult();

        self::assertIsArray($actual);
    }

    public static function dataProvideQueryExecution(): iterable
    {
        $datas = static::getFormData();

        foreach (static::getQueryBuilders() as $qb) {
            if ([] === $datas) {
                yield [clone $qb, []];
            } else {
                foreach ($datas as $data) {
                    yield [clone $qb, $data];
                }
            }
        }
    }

    public function testApplyOn()
    {
        $filter = $this->getFilter();

        $this->assertIsString($filter->applyOn());
    }

    /**
     * @dataProvider dataProviderDescriptionAction
     *
     * @param array $data
     */
    public function testDescriptionAction($data)
    {
        $context = new ExportGenerationContext((new User())->setLabel('test user'));
        $description = $this->getFilter()->describeAction($data, $context);

        $this->assertTrue(
            \is_string($description) || \is_array($description),
            'test that the description is a string or an array'
        );

        if (\is_string($description)) {
            $this->assertNotEmpty(
                $description,
                'test that the description is not empty'
            );
        } elseif (\is_array($description)) {
            $this->assertIsString(
                $description[0],
                'test that the first element in the description array is a string'
            );

            // test that the message is translated
            try {
                if (null === static::$kernel) {
                    static::bootKernel();
                }

                $catalogue = static::$kernel->getContainer()
                    ->get('translator')
                    ->getCatalogue();
            } catch (\Exception $ex) {
                $this->markTestIncomplete(
                    sprintf(
                        "This test is incomplete due to %s thrown by 'translator' : %s, "
                        .'complete stack : %s',
                        $ex::class,
                        $ex->getMessage(),
                        $ex->getTraceAsString()
                    )
                );
            }
            $this->assertTrue(
                $catalogue->has(
                    $description[0],
                    $description[2] ?? 'messages'
                ),
                sprintf('Test that the message returned by getDescriptionAction is '
                .'present in the catalogue of translations. HINT : check that "%s" '
                .'is correctly translated', $description[0])
            );
        }
    }

    public static function dataProviderDescriptionAction()
    {
        foreach (static::getFormData() as $data) {
            yield [$data];
        }
    }
}
