<?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\Tests\Export;

use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Test\PrepareCenterTrait;
use Chill\MainBundle\Test\PrepareScopeTrait;
use Chill\MainBundle\Test\PrepareUserTrait;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Prophecy\Argument;
use Prophecy\Prophet;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Test the export manager.
 *
 * @internal
 *
 * @coversNothing
 */
final class ExportManagerTest extends KernelTestCase
{
    use PrepareCenterTrait;

    use PrepareScopeTrait;

    use PrepareUserTrait;

    private Prophet $prophet;

    protected function setUp(): void
    {
        self::bootKernel();

        $this->prophet = new Prophet();
    }

    protected function tearDown(): void
    {
        $this->prophet->checkPredictions();
    }

    public function testAggregatorsApplyingOn()
    {
        $centers = [$center = new Center()];
        $user = $this->prepareUser([]);

        $authorizationChecker = $this->prophet->prophesize();
        $authorizationChecker->willImplement(AuthorizationCheckerInterface::class);
        $authorizationChecker->isGranted('CHILL_STAT_DUMMY', $center)
            ->willReturn(true);

        $exports = [];
        $aggregators = [];
        $filters = [];

        $exports[] = $exportFooBar = new DummyExport('CHILL_STAT_DUMMY', ['foo', 'bar']);

        $aggregatorBar = $this->prophet->prophesize();
        $aggregatorBar->willImplement(AggregatorInterface::class);
        $aggregatorBar->applyOn()->willReturn('bar');
        $aggregatorBar->addRole()->willReturn(null);
        $aggregators['bar'] = $aggregatorBar->reveal();

        $exports[] = $exportBar = new DummyExport('CHILL_STAT_DUMMY', ['bar']);

        $aggregatorFoo = $this->prophet->prophesize();
        $aggregatorFoo->willImplement(AggregatorInterface::class);
        $aggregatorFoo->applyOn()->willReturn('foo');
        $aggregatorFoo->addRole()->willReturn(null);
        $aggregators['foo'] = $aggregatorFoo->reveal();

        $exports[] = $exportFoo = new DummyExport('CHILL_STAT_DUMMY', ['foo']);

        $exportManager = $this->createExportManager(
            null,
            null,
            $authorizationChecker->reveal(),
            null,
            $user,
            $exports,
            $aggregators,
            $filters
        );

        $obtained = iterator_to_array($exportManager->getAggregatorsApplyingOn($exportFoo, $centers));
        $this->assertEquals(1, \count($obtained));
        $this->assertContains('foo', array_keys($obtained));

        $obtained = iterator_to_array($exportManager->getAggregatorsApplyingOn($exportBar, $centers));
        $this->assertEquals(1, \count($obtained));
        $this->assertContains('bar', array_keys($obtained));

        $obtained = iterator_to_array($exportManager->getAggregatorsApplyingOn($exportFooBar, $centers));
        $this->assertEquals(2, \count($obtained));
        $this->assertContains('bar', array_keys($obtained));
        $this->assertContains('foo', array_keys($obtained));

        // test with empty centers
        $obtained = iterator_to_array($exportManager->getAggregatorsApplyingOn($exportFooBar, []));
        $this->assertEquals(0, \count($obtained));
    }

    public function testFiltersApplyingOn()
    {
        $centers = [$center = new Center()];
        $user = $this->prepareUser([]);

        $authorizationChecker = $this->prophet->prophesize();
        $authorizationChecker->willImplement(AuthorizationCheckerInterface::class);
        $authorizationChecker->isGranted(Argument::type('string'), $center)
            ->willReturn(true);

        $filters = [];
        $exports = [];

        $exports['fooBar'] = $exportFooBar = new DummyExport('CHILL_STAT_DUMMY', ['foo', 'bar']);
        $filters['bar'] = $filterBar = new DummyFilterWithApplying(null, 'bar');
        $exports['bar'] = $exportBar = new DummyExport('CHILL_STAT_DUMMY', ['bar']);
        $filters['foo'] = $filterFoo = new DummyFilterWithApplying(null, 'foo');
        $exports['foo'] = $exportFoo = new DummyExport('CHILL_STAT_DUMMY', ['foo']);

        $exportManager = $this->createExportManager(
            null,
            null,
            $authorizationChecker->reveal(),
            null,
            $user,
            $exports,
            [],
            $filters
        );

        $obtained = iterator_to_array($exportManager->getFiltersApplyingOn($exportFoo, $centers));
        $this->assertEquals(1, \count($obtained));
        $this->assertContains('foo', array_keys($obtained));

        $obtained = iterator_to_array($exportManager->getFiltersApplyingOn($exportBar, $centers));
        $this->assertEquals(1, \count($obtained));
        $this->assertContains('bar', array_keys($obtained));

        $obtained = iterator_to_array($exportManager->getFiltersApplyingOn($exportFooBar, $centers));
        $this->assertEquals(2, \count($obtained));
        $this->assertContains('bar', array_keys($obtained));
        $this->assertContains('foo', array_keys($obtained));

        $obtained = iterator_to_array($exportManager->getFiltersApplyingOn($exportFooBar, []));
        $this->assertEquals(0, \count($obtained));
    }

    public function testFormattersByTypes()
    {
        $exportManager = $this->createExportManager();

        // create a formatter
        $formatterFoo = $this->prophet->prophesize();
        $formatterFoo->willImplement(\Chill\MainBundle\Export\FormatterInterface::class);
        $formatterFoo->getType()->willReturn('foo');
        $formatterBar = $this->prophet->prophesize();
        $formatterBar->willImplement(\Chill\MainBundle\Export\FormatterInterface::class);
        $formatterBar->getType()->willReturn('bar');
        $exportManager->addFormatter($formatterFoo->reveal(), 'foo');
        $exportManager->addFormatter($formatterBar->reveal(), 'bar');

        $obtained = $exportManager->getFormattersByTypes(['foo']);

        $this->assertContains($formatterFoo->reveal(), $obtained);
        $this->assertNotContains($formatterBar->reveal(), $obtained);
    }

    public function testIsGrantedForElementWithExportAndUserIsGranted()
    {
        $center = $this->prepareCenter(100, 'center A');
        $user = $this->prepareUser([]);

        $authorizationChecker = $this->prophet->prophesize();
        $authorizationChecker->willImplement(AuthorizationCheckerInterface::class);
        $authorizationChecker->isGranted('CHILL_STAT_DUMMY', $center)
            ->willReturn(true);

        $export = $this->prophet->prophesize();
        $export->willImplement(ExportInterface::class);
        $export->requiredRole()->willReturn('CHILL_STAT_DUMMY');

        $exportManager = $this->createExportManager(
            null,
            null,
            $authorizationChecker->reveal(),
            null,
            $user,
            ['dummy_export' => $export->reveal()],
        );

        $result = $exportManager->isGrantedForElement($export->reveal(), null, [$center]);

        $this->assertTrue($result);
    }

    public function testIsGrantedForElementWithExportAndUserIsGrantedNotForAllCenters()
    {
        $center = $this->prepareCenter(100, 'center A');
        $centerB = $this->prepareCenter(102, 'center B');
        $user = $this->prepareUser([]);

        $authorizationChecker = $this->prophet->prophesize();
        $authorizationChecker->willImplement(AuthorizationCheckerInterface::class);
        $authorizationChecker->isGranted('CHILL_STAT_DUMMY', $center)
            ->willReturn(true);
        $authorizationChecker->isGranted('CHILL_STAT_DUMMY', $centerB)
            ->willReturn(false);

        $exportManager = $this->createExportManager(
            null,
            null,
            $authorizationChecker->reveal(),
            null,
            $user
        );

        $export = $this->prophet->prophesize();
        $export->willImplement(ExportInterface::class);
        $export->requiredRole()->willReturn('CHILL_STAT_DUMMY');

        $result = $exportManager->isGrantedForElement($export->reveal(), null, [$center, $centerB]);

        $this->assertFalse($result);
    }

    public function testIsGrantedForElementWithExportEmptyCenters()
    {
        $user = $this->prepareUser([]);

        $exportManager = $this->createExportManager(
            null,
            null,
            null,
            null,
            $user
        );

        $export = $this->prophet->prophesize();
        $export->willImplement(ExportInterface::class);
        $export->requiredRole()->willReturn('CHILL_STAT_DUMMY');

        $result = $exportManager->isGrantedForElement($export->reveal(), null, []);

        $this->assertFalse($result);
    }

    public function testIsGrantedForElementWithModifierFallbackToExport()
    {
        $center = $this->prepareCenter(100, 'center A');
        $centerB = $this->prepareCenter(102, 'center B');
        $user = $this->prepareUser([]);

        $authorizationChecker = $this->prophet->prophesize();
        $authorizationChecker->willImplement(AuthorizationCheckerInterface::class);
        $authorizationChecker->isGranted('CHILL_STAT_DUMMY', $center)
            ->willReturn(true);
        $authorizationChecker->isGranted('CHILL_STAT_DUMMY', $centerB)
            ->willReturn(false);

        $exportManager = $this->createExportManager(
            null,
            null,
            $authorizationChecker->reveal(),
            null,
            $user
        );

        $modifier = $this->prophet->prophesize();
        $modifier->willImplement(\Chill\MainBundle\Export\ModifierInterface::class);
        $modifier->addRole()->willReturn(null);

        $export = $this->prophet->prophesize();
        $export->willImplement(ExportInterface::class);
        $export->requiredRole()->willReturn('CHILL_STAT_DUMMY');

        $result = $exportManager->isGrantedForElement(
            $modifier->reveal(),
            $export->reveal(),
            [$center, $centerB]
        );

        $this->assertFalse($result);
    }

    public function testNonExistingFormatter()
    {
        $this->expectException(\RuntimeException::class);

        $exportManager = $this->createExportManager();

        $exportManager->getFormatter('non existing');
    }

    /**
     *  Create an ExportManager where every element may be replaced by a double.
     *
     * If null is provided for an element, this is replaced by the equivalent
     * from the container; if the user provided is null, this is replaced by the
     * user 'center a_social' from database.
     */
    protected function createExportManager(
        ?LoggerInterface $logger = null,
        ?EntityManagerInterface $em = null,
        ?AuthorizationCheckerInterface $authorizationChecker = null,
        ?AuthorizationHelper $authorizationHelper = null,
        ?UserInterface $user = null,
        array $exports = [],
        array $aggregators = [],
        array $filters = [],
        array $formatters = [],
    ): ExportManager {
        $localUser = $user ?? self::getContainer()->get(
            UserRepositoryInterface::class
        )
            ->findOneBy(['username' => 'center a_social']);
        $token = new UsernamePasswordToken($localUser, 'password', 'provider');
        $tokenStorage = new TokenStorage();
        $tokenStorage->setToken($token);

        return new ExportManager(
            $logger ?? self::getContainer()->get(LoggerInterface::class),
            $authorizationChecker ?? self::getContainer()->get('security.authorization_checker'),
            $authorizationHelper ?? self::getContainer()->get('chill.main.security.authorization.helper'),
            $tokenStorage,
            $exports,
            $aggregators,
            $filters,
            $formatters,
        );
    }
}

class DummyFilterWithApplying implements FilterInterface
{
    public function __construct(
        private readonly ?string $role,
        private readonly string $applyOn,
    ) {}

    public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface
    {
        return 'dummy';
    }

    public function buildForm(FormBuilderInterface $builder): void {}

    public function getNormalizationVersion(): int
    {
        return 1;
    }

    public function normalizeFormData(array $formData): array
    {
        return [];
    }

    public function denormalizeFormData(array $formData, int $fromVersion): array
    {
        return [];
    }

    public function getFormDefaultData(): array
    {
        return [];
    }

    public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array
    {
        return ['dummy filter', []];
    }

    public function addRole(): ?string
    {
        return $this->role;
    }

    public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void {}

    public function applyOn(): string
    {
        return $this->applyOn;
    }
}

class DummyExport implements ExportInterface
{
    public function __construct(
        private readonly string $role,
        /**
         * @var array<string>
         */
        private readonly array $supportedModifiers,
    ) {}

    public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface
    {
        return 'dummy';
    }

    public function buildForm(FormBuilderInterface $builder) {}

    public function getNormalizationVersion(): int
    {
        return 1;
    }

    public function normalizeFormData(array $formData): array
    {
        return [];
    }

    public function denormalizeFormData(array $formData, int $fromVersion): array
    {
        return [];
    }

    public function getFormDefaultData(): array
    {
        return [];
    }

    public function getAllowedFormattersTypes()
    {
        return ['test'];
    }

    public function getDescription()
    {
        return 'dummy export';
    }

    public function getLabels($key, array $values, mixed $data)
    {
        return [];
    }

    public function getQueryKeys($data): array
    {
        return [];
    }

    public function getResult($query, $data, ExportGenerationContext $context): array
    {
        return [];
    }

    public function getType(): string
    {
        return 'dummy';
    }

    public function initiateQuery(array $requiredModifiers, array $acl, array $data, ExportGenerationContext $context): QueryBuilder
    {
        throw new \RuntimeException('not implemented');
    }

    public function requiredRole(): string
    {
        return $this->role;
    }

    public function supportsModifiers()
    {
        return $this->supportedModifiers;
    }
}
