<?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\DirectExportInterface;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Test\PrepareClientTrait;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

/**
 * This class provide a set of tests for exports.
 *
 * The tests provided by this class will check basic things, like
 * the type of value are conform to the expected, etc.
 */
abstract class AbstractExportTest extends WebTestCase
{
    use PrepareClientTrait;

    /**
     * Return an array usable as ACL.
     *
     * If this method is overridden, the returned result must be an array
     * with this form :
     *
     * ```
     * array(
     *      array(
     *          'center' =>  //center instance
     *          'circles' => array(// array of circles instances )
     *     )
     * );
     * ```
     */
    public static function getACL()
    {
        if (null === static::$kernel) {
            static::bootKernel();
        }

        $em = static::getContainer()->get(EntityManagerInterface::class);

        $centers = $em->getRepository(\Chill\MainBundle\Entity\Center::class)
            ->findAll();
        $circles = $em->getRepository(\Chill\MainBundle\Entity\Scope::class)
            ->findAll();

        if (0 === \count($centers)) {
            throw new \RuntimeException('No center found. Did you forget to run `doctrine:fixtures:load` command before ?');
        }

        if (0 === \count($circles)) {
            throw new \RuntimeException('No circle found. Did you forget to run `doctrine:fixtures:load` command before ?');
        }

        return [[
            'center' => $centers[0],
            'circles' => [
                $circles,
            ], ]];
    }

    /**
     * Create an instance of the report to test.
     *
     * @return ExportInterface|DirectExportInterface|iterable<ExportInterface>|iterable<DirectExportInterface> an instance of the export to test
     */
    abstract public function getExport();

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

    /**
     * get the possible modifiers which could apply in combination to this
     * export.
     * .
     *
     * @return list<list<string>> of string[] an array which contains an array of possible modifiers. Example : `array( array('modifier_1', 'modifier_2'), array('modifier_1'), ...)`
     */
    abstract public static function getModifiersCombination(): array;

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

    protected function getParameters(bool $filterStatsByCenter): ParameterBagInterface
    {
        return new ParameterBag(['chill_main' => ['acl' => ['filter_stats_by_center' => $filterStatsByCenter]]]);
    }

    /**
     * wrap the results of @see{self::getExports()}, which may be an iterable or an export into an iterble.
     */
    private function getExports(): iterable
    {
        $exports = $this->getExport();

        if (is_iterable($exports)) {
            return $exports;
        }

        return [$exports];
    }

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

        if (is_iterable($export)) {
            foreach ($export as $e) {
                $this->testOneDataNormalization($e, $data, $version, $customAssert);
            }
        } else {
            $this->testOneDataNormalization($export, $data, $version, $customAssert);
        }
    }

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

    private function testOneDataNormalization(ExportInterface|DirectExportInterface $export, array $data, int $version, array $customAssert): void
    {
        $normalized = $export->normalizeFormData($data);
        $actual = $export->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]);
            }
        }
    }

    /**
     * Test the formatters type are string.
     */
    public function testGetAllowedFormattersType()
    {
        foreach ($this->getExports() as $export) {
            $formattersTypes = $export->getAllowedFormattersTypes();

            $this->assertContainsOnly(
                'string',
                $formattersTypes,
                true,
                'Test that the method `getAllowedFormattersTypes` returns an array of string'
            );
        }
    }

    /**
     * Test that the description is not empty.
     */
    public function testGetDescription()
    {
        foreach ($this->getExports() as $export) {
            $this->assertIsString(
                $export->getDescription(),
                'Assert that the `getDescription` method return a string'
            );
            $this->assertNotEmpty(
                $export->getDescription(),
                'Assert that the `getDescription` method does not return an empty '
                .'string.'
            );
        }
    }

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

            $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'
            );
        }
    }

    public static function dataProviderGetQueryKeys()
    {
        foreach (static::getFormData() 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'
     *
     * @param string[] $modifiers
     * @param array    $acl
     *
     * @dataProvider dataProviderInitiateQuery
     */
    public function testGetResultsAndLabels($modifiers, $acl, array $data)
    {
        foreach ($this->getExports() as $export) {
            // it is more convenient to  group the `getResult` and `getLabels` test
            // due to the fact that testing both methods use the same tools.

            $queryKeys = $export->getQueryKeys($data);
            $query = $export->initiateQuery($modifiers, $acl, $data, $exportGenerationContext = new ExportGenerationContext($this->getUser()));

            // limit the result for the query for performance reason (only for QueryBuilder,
            // not possible in NativeQuery)
            if ($query instanceof QueryBuilder) {
                $query->setMaxResults(1);
            }

            $results = $export->getResult($query, $data, $exportGenerationContext);

            $this->assertIsArray(
                $results,
                'assert that the returned result is an 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'
            );

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

                $closure = $export->getLabels($key, [$value], $data);

                $this->assertTrue(
                    \is_callable($closure, false),
                    'test that the `getLabels` for key is a callable'
                );

                $this->assertTrue(
                    // conditions
                    \is_string((string) \call_user_func($closure, '_header'))
                    && !empty(\call_user_func($closure, '_header'))
                    && '_header' !== \call_user_func($closure, '_header'),
                    // message
                    sprintf('Test that the callable return by `getLabels` for key %s '
                        .'can provide an header', $key)
                );
                ++$i;

                if ($i > 15) {
                    // do not iterate on each result
                    break;
                }
            }
        }
    }

    /**
     * Test that the getType method return a string.
     */
    public function testGetType()
    {
        foreach ($this->getExports() as $export) {
            $this->assertIsString(
                $export->getType(),
                'Assert that the `getType` method return a string'
            );
            $this->assertNotEmpty($export->getType(), 'Assert that the `getType` method'
                .' does not return an empty string.');
        }
    }

    /**
     * test that the query returned is a QueryBuilder or a NativeQuery.
     *
     * If the query is a QueryBuilder, test that select and from is not empty.
     *
     * If the query is a native sql, test the query is not empty (length is
     * > 0).
     *
     * @dataProvider dataProviderInitiateQuery
     */
    public function testInitiateQuery(mixed $modifiers, mixed $acl, mixed $data)
    {
        foreach ($this->getExports() as $export) {
            $query = $export->initiateQuery($modifiers, $acl, $data, new ExportGenerationContext($this->getUser()));

            $this->assertTrue(
                $query instanceof QueryBuilder || $query instanceof NativeQuery,
                sprintf(
                    'Assert that the returned query is an instance of %s or %s',
                    QueryBuilder::class,
                    NativeQuery::class
                )
            );

            if ($query instanceof QueryBuilder) {
                $this->assertGreaterThanOrEqual(
                    1,
                    \count($query->getDQLPart('select')),
                    "assert there is at least one 'select' part"
                );

                $this->assertGreaterThanOrEqual(
                    1,
                    \count($query->getDQLPart('from')),
                    "assert there is at least one 'from' part"
                );
            } elseif ($query instanceof NativeQuery) {
                $this->assertNotEmpty(
                    $query->getSQL(),
                    'check that the SQL query is not empty'
                );
            }
        }
    }

    /**
     * Test required role is an instance of Role.
     */
    public function testRequiredRole()
    {
        foreach ($this->getExports() as $export) {
            $role = $export->requiredRole();

            self::assertIsString($role);
        }
    }

    /**
     * Test that supportsModifier return :.
     *
     * - an array of string, if the query is a QueryBuilder ;
     * - nothing, if the query is a native SQL
     *
     * @dataProvider dataProviderInitiateQuery
     */
    public function testSupportsModifier(mixed $modifiers, mixed $acl, mixed $data)
    {
        foreach ($this->getExports() as $export) {
            $query = $export->initiateQuery($modifiers, $acl, $data, new ExportGenerationContext($this->getUser()));

            if ($query instanceof QueryBuilder) {
                $this->assertContainsOnly(
                    'string',
                    $export->supportsModifiers(),
                    true,
                    'Test that the `supportsModifiers` method returns only strings'
                );
            } elseif ($query instanceof NativeQuery) {
                $this->assertTrue(
                    null === $export->supportsModifiers()
                    || 0 === \count($export->supportsModifiers()),
                    'Test that the `supportsModifier` methods returns null or an empty array'
                );
            }
        }
    }

    /**
     * create data for `ìnitiateQuery` method.
     */
    public static function dataProviderInitiateQuery()
    {
        $acl = static::getAcl();

        foreach (static::getModifiersCombination() as $modifiers) {
            foreach (static::getFormData() as $data) {
                yield [$modifiers, $acl, $data];
            }
        }
    }
}
