<?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\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Traversable;

/**
 * Helper which creates a set of test for aggregators.
 */
abstract class AbstractAggregatorTest extends KernelTestCase
{
    public static function tearDownAfterClass(): void
    {
        if (null !== self::getContainer()) {
            /** @var EntityManagerInterface $em */
            $em = self::getContainer()->get(EntityManagerInterface::class);
            $em->getConnection()->close();
        }
        self::ensureKernelShutdown();
    }

    /**
     * Create an aggregator instance which will be used in tests.
     *
     * This method is always used after an eventuel `setUp` method.
     *
     * @return \Chill\MainBundle\Export\AggregatorInterface
     */
    abstract public function getAggregator();

    /**
     * Create possible combinaison of data (produced by the form).
     *
     * This data will be used to generate data providers using this data.
     *
     * This method is executed before 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();

    /**
     * get an array of query builders that the aggregator will use.
     *
     * Those query builders will be used to test aggregator behaviour on this
     * query builder.
     *
     * This method is executed before the `setUp` method.
     *
     * @return QueryBuilder[]
     */
    abstract public static function getQueryBuilders();

    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;
    }

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

        $this->getAggregator()->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();

        if (!\is_array($datas)) {
            $datas = iterator_to_array($datas);
        }

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

    /**
     * @dataProvider dataProviderQueryExecution
     *
     * @group dbIntensive
     */
    public function testQueryExecution(QueryBuilder $qb, array $data): void
    {
        $this->getAggregator()->alterQuery($qb, $data, new ExportGenerationContext($this->getUser()));

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

        self::assertIsArray($actual);
    }

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

        if (!\is_array($datas)) {
            $datas = iterator_to_array($datas);
        }

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

    /**
     * test the alteration of query by the filter.
     *
     * @dataProvider dataProviderAlterQuery
     *
     * @param type $data
     */
    public function testAlterQuery(QueryBuilder $query, $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;

        $this->getAggregator()->alterQuery($query, $data, new ExportGenerationContext($this->getUser()));

        $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->assertGreaterThanOrEqual(
            $nbOfSelect,
            null !== $query->getDQLPart('select') ? \count($query->getDQLPart('select')) : 0,
            "Test that the filter has no altered the 'select' part of the query"
        );
    }

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

        if (!\is_array($datas)) {
            $datas = iterator_to_array($datas);
        }

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

    /**
     * Test the `applyOn` method.
     */
    public function testApplyOn()
    {
        $filter = $this->getAggregator();

        $this->assertIsString(
            $filter->applyOn(),
            'test that the internal type of "applyOn" is a string'
        );
        $this->assertNotEmpty(
            $filter->applyOn(),
            'test that the "applyOn" method return a non-empty string'
        );
    }

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

        $normalized = $aggregator->normalizeFormData($data);
        $actual = $aggregator->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, []];
        }
    }

    /**
     * Test that the query keys are strings.
     *
     * @dataProvider dataProviderGetQueryKeys
     */
    public function testGetQueryKeys(array $data)
    {
        $queryKeys = $this->getAggregator()->getQueryKeys($data);

        $this->assertIsArray(
            $queryKeys,
            'test that the query keys returned are an array'
        );
        $this->assertContainsOnly(
            'string',
            $queryKeys,
            true,
            'test that the query keys returned by `getQueryKeys` are only strings'
        );
        $this->assertGreaterThanOrEqual(
            1,
            \count($queryKeys),
            'test that there are at least one query key returned'
        );
    }

    /**
     * prepare data for `testGetQueryKeys`.
     */
    public static function dataProviderGetQueryKeys()
    {
        $datas = static::getFormData();

        if (!\is_array($datas)) {
            $datas = iterator_to_array($datas);
        }

        foreach ($datas as $data) {
            yield [$data];
        }
    }

    /**
     * Test that.
     *
     * - the results have a correct form (are arrays or traversable)
     * - each key in a row are present in getQueryKeys ;
     * - each returned object of the `getLabels` method is callable
     * - each result can be converted to string using this callable
     * - each of this callable can provide a string for '_header'
     *
     * @dataProvider dataProviderGetResultsAndLabels
     */
    public function testGetResultsAndLabels(QueryBuilder $qb, array $data)
    {
        // it is more convenient to  group the `getResult` and `getLabels` test
        // due to the fact that testing both methods use the same tools.

        // limit the result for the query for performance reason
        $qb->setMaxResults(1);

        $queryKeys = $this->getAggregator()->getQueryKeys($data);
        $this->getAggregator()->alterQuery($qb, $data, new ExportGenerationContext($this->getUser()));

        $results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);

        if (0 === \count($results)) {
            $this->markTestIncomplete('The result is empty. We cannot process tests '
                .'on results');
        }

        // testing the result
        $result = $results[0];

        $this->assertTrue(
            is_iterable($result),
            'test that each row in the result is traversable or an array'
        );

        foreach ($queryKeys as $key) {
            $this->assertContains(
                $key,
                array_keys($result),
                'test that each key is present in `getQueryKeys`'
            );

            $closure = $this->getAggregator()->getLabels($key, [$result[$key]], $data);

            $this->assertTrue(
                \is_callable($closure, false),
                'test that the `getLabels` for key is a callable'
            );
            $this->assertTrue(
                \is_string((string) \call_user_func($closure, $result[$key])),
                sprintf('test that the callable return by `getLabels` for key %s '
                    .'is a string or an be converted to a string', $key)
            );

            $head = \call_user_func($closure, '_header');

            self::assertIsString($head);
            self::assertNotEquals('', $head);
            self::assertNotEquals('_header', $head);
        }
    }

    /**
     * prepare date for method `testGetResultsAndLabels`.
     */
    public static function dataProviderGetResultsAndLabels()
    {
        $datas = static::getFormData();

        if (!\is_array($datas)) {
            $datas = iterator_to_array($datas);
        }

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

    /**
     * test the `getTitle` method.
     */
    public function testGetTitle()
    {
        $title = $this->getAggregator()->getTitle();

        $this->assertIsString($title);
        $this->assertNotEmpty(
            $title,
            'test that the title is not empty'
        );
    }
}
