<?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 Service\DocGenerator;

use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
use Chill\DocStoreBundle\Entity\DocumentCategory;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
use Chill\PersonBundle\Service\DocGenerator\AccompanyingPeriodContext;
use Chill\PersonBundle\Service\DocGenerator\PersonContext;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\Argument;
use Prophecy\Exception\Prediction\FailedPredictionException;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * @internal
 *
 * @coversNothing
 */
final class PersonContextTest extends KernelTestCase
{
    use ProphecyTrait;

    /**
     * This test run the methods executed when a document is generated:.
     *
     * - normalized data from the form in a way that they are stored in message queue;
     * - denormalize the data from the message queue,
     * - and get the data, as they will be transmitted to the GeneratorDriver
     *
     * @param array    $options          the options, as they are stored in the DocGeneratorTemplate (the admin form data)
     * @param Person   $entity           The entity from which the data will be extracted
     * @param array    $data             The data, from the public form
     * @param callable $assertionsOnData some test that will be executed on the normalized data
     *
     * @dataProvider provideNormalizedData
     */
    public function testContextGenerationDataNormalizeDenormalizeGetData(
        array $options,
        Person $entity,
        array $data,
        array $expectedNormalized,
        callable $assertionsOnData,
    ): void {
        // we boot kernel only for this test
        self::bootKernel();

        // we create a PersonContext with the minimal dependency injection needed (relying on
        // prophecy for other dependencies)
        $context = $this->buildPersonContext(
            null,
            self::getContainer()->get(BaseContextData::class),
            self::getContainer()->get(CenterResolverManagerInterface::class),
            self::getContainer()->get(DocumentCategoryRepository::class),
            self::getContainer()->get(EntityManagerInterface::class),
            self::getContainer()->get(NormalizerInterface::class),
            new ParameterBag(['chill_main' => ['acl' => ['form_show_scopes' => false]]]),
            null,
            self::getContainer()->get(Security::class),
            null,
            null,
            null,
            self::getContainer()->get(ThirdPartyRepository::class)
        );
        $template = new DocGeneratorTemplate();
        $template->setName(['fr' => 'test'])->setContext(AccompanyingPeriodContext::class)
            ->setDescription('description')->setActive(true)
            ->setOptions($options);

        $normalized = $context->contextGenerationDataNormalize($template, $entity, $data);

        self::assertEquals($expectedNormalized, $normalized);

        $denormalized = $context->contextGenerationDataDenormalize($template, $entity, $normalized);

        $data = $context->getData($template, $entity, $denormalized);

        call_user_func($assertionsOnData, $data);
    }

    public static function provideNormalizedData(): iterable
    {
        self::bootKernel();
        $em = self::getContainer()->get(EntityManagerInterface::class);

        $thirdParty = $em->createQuery('SELECT t FROM '.ThirdParty::class.' t')
            ->setMaxResults(1)
            ->getSingleResult();

        if (null === $thirdParty) {
            throw new \RuntimeException('No thirdparty in database');
        }

        $person = $em->createQuery('SELECT p FROM '.Person::class.' p')
            ->setMaxResults(1)
            ->getSingleResult();

        if (null === $person) {
            throw new \RuntimeException('No confirmed period in database');
        }

        $category = self::getContainer()->get(DocumentCategoryRepository::class)
            ->findAll()[0];

        if (null === $category) {
            throw new \RuntimeException('no document category in database');
        }

        yield [
            [
                'thirdParty' => true,
                'thirdPartyLabel' => '3party',
                'category' => $category,
            ],
            $person,
            [
                'title' => 'test',
                'thirdParty' => $thirdParty,
            ],
            [
                'thirdParty' => $thirdParty->getId(),
                'title' => 'test',
                'scope_id' => null,
            ],
            function ($data) use ($person, $thirdParty) {
                self::assertArrayHasKey('person', $data);
                self::assertEquals($person->getId(), $data['person']['id']);

                self::assertArrayHasKey('thirdParty', $data);
                self::assertEquals($thirdParty->getId(), $data['thirdParty']['id']);
            },
        ];

        yield [
            [
                'thirdParty' => false,
                'thirdPartyLabel' => '3party',
                'category' => $category,
            ],
            $person,
            [
                'title' => 'test',
            ],
            [
                'title' => 'test',
                'scope_id' => null,
                'thirdParty' => null,
            ],
            function ($data) use ($person) {
                self::assertArrayHasKey('person', $data);
                self::assertEquals($person->getId(), $data['person']['id']);
            },
        ];
    }

    /**
     * Test that the build person context works in the case when 'form_show_scope' is false.
     */
    public function testScopeDoNotShowScopeInForms()
    {
        $person = new Person();
        $docGen = (new DocGeneratorTemplate())
            ->setName(['fr' => 'template']);

        $parameter = new ParameterBag(['chill_main' => ['acl' => ['form_show_scopes' => false]]]);
        $em = $this->prophesize(EntityManagerInterface::class);
        $em->persist(Argument::type(PersonDocument::class))
            ->should(static function ($calls, $object, $method) {
                if (1 !== \count($calls)) {
                    throw new FailedPredictionException(sprintf('the persist should be called exactly once, %d receivved', \count($calls)));
                }

                /** @var PersonDocument $personDocument */
                $personDocument = $calls[0]->getArguments()[0];

                if (null !== $personDocument->getScope()) {
                    throw new FailedPredictionException('the person document should not have any scope');
                }
            });

        $personContext = $this->buildPersonContext(
            null,
            null,
            null,
            null,
            $em->reveal(),
            null,
            $parameter
        );

        $personContext->buildPublicForm($this->buildFormBuilder(false), $docGen, $person);

        $personContext->storeGenerated(
            $docGen,
            new StoredObject(),
            $person,
            []
        );
    }

    public function testScopeScopeMustBeShownInFormsAndUserAccessMultipleScope()
    {
        $person = new Person();
        $docGen = (new DocGeneratorTemplate())
            ->setName(['fr' => 'template']);
        $scope = new Scope();

        $em = $this->prophesize(EntityManagerInterface::class);
        $em->persist(Argument::type(PersonDocument::class))
            ->should(static function ($calls, $object, $method) use ($scope) {
                if (1 !== \count($calls)) {
                    throw new FailedPredictionException(sprintf('the persist should be called exactly once, %d receivved', \count($calls)));
                }

                /** @var PersonDocument $personDocument */
                $personDocument = $calls[0]->getArguments()[0];

                if ($personDocument->getScope() !== $scope) {
                    throw new FailedPredictionException('the person document should show the exactly prepared scope');
                }
            });

        $authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
        $authorizationHelper->getReachableScopes(Argument::type(UserInterface::class), PersonDocumentVoter::CREATE, Argument::type('array'))
            ->willReturn([$scope, new Scope()]);

        $personContext = $this->buildPersonContext(
            $authorizationHelper->reveal(),
            null,
            null,
            null,
            $em->reveal(),
        );

        $personContext->buildPublicForm($this->buildFormBuilder(true), $docGen, $person);

        $personContext->storeGenerated(
            $docGen,
            new StoredObject(),
            $person,
            ['scope' => $scope]
        );
    }

    public function testScopeScopeMustBeShownInFormsAndUserAccessOneScope()
    {
        $person = new Person();
        $docGen = (new DocGeneratorTemplate())
            ->setName(['fr' => 'template']);
        $scope = new Scope();

        $em = $this->prophesize(EntityManagerInterface::class);
        $em->persist(Argument::type(PersonDocument::class))
            ->should(static function ($calls, $object, $method) use ($scope) {
                if (1 !== \count($calls)) {
                    throw new FailedPredictionException(sprintf('the persist should be called exactly once, %d received', \count($calls)));
                }

                /** @var PersonDocument $personDocument */
                $personDocument = $calls[0]->getArguments()[0];

                if ($personDocument->getScope() !== $scope) {
                    throw new FailedPredictionException('the person document should show the exactly prepared scope');
                }
            });

        $authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
        $authorizationHelper->getReachableScopes(Argument::type(UserInterface::class), PersonDocumentVoter::CREATE, Argument::type('array'))
            ->willReturn([$scope]);

        $personContext = $this->buildPersonContext(
            $authorizationHelper->reveal(),
            null,
            null,
            null,
            $em->reveal(),
        );

        $personContext->buildPublicForm($this->buildFormBuilder(false), $docGen, $person);

        $personContext->storeGenerated(
            $docGen,
            new StoredObject(),
            $person,
            ['scope' => $scope]
        );
    }

    private function buildFormBuilder(bool $withScope): FormBuilderInterface
    {
        $builder = $this->prophesize(FormBuilderInterface::class);

        $builder->add('title', TextType::class, Argument::type('array'))->shouldBeCalled();

        if ($withScope) {
            $builder->add('scope', ScopePickerType::class, Argument::type('array'))
                ->shouldBeCalled();
        } else {
            $builder->add('scope', ScopePickerType::class, Argument::type('array'))
                ->shouldNotBeCalled();
        }

        return $builder->reveal();
    }

    private function buildPersonContext(
        ?AuthorizationHelperInterface $authorizationHelper = null,
        ?BaseContextData $baseContextData = null,
        ?CenterResolverManagerInterface $centerResolverManager = null,
        ?DocumentCategoryRepository $documentCategoryRepository = null,
        ?EntityManagerInterface $em = null,
        ?NormalizerInterface $normalizer = null,
        ?ParameterBagInterface $parameterBag = null,
        ?ScopeRepositoryInterface $scopeRepository = null,
        ?Security $security = null,
        ?TranslatorInterface $translator = null,
        ?TranslatableStringHelperInterface $translatableStringHelper = null,
        ?ThirdPartyRender $thirdPartyRender = null,
        ?ThirdPartyRepository $thirdPartyRepository = null,
        ?ResidentialAddressRepository $residentialAddressRepository = null,
    ): PersonContext {
        if (null === $authorizationHelper) {
            $authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class)->reveal();
        }

        if (null === $baseContextData) {
            $baseContextData = $this->prophesize(BaseContextData::class)->reveal();
        }

        if (null === $centerResolverManager) {
            $centerResolverManager = $this->prophesize(CenterResolverManagerInterface::class);
            $centerResolverManager->resolveCenters(Argument::any(), Argument::any())
                ->willReturn([new Center()]);
            $centerResolverManager = $centerResolverManager->reveal();
        }

        if (null === $documentCategoryRepository) {
            $documentCategoryRepository = $this->prophesize(DocumentCategoryRepository::class);
            $documentCategoryRepository->find(Argument::type('integer'))->willReturn(
                new DocumentCategory(PersonDocument::class, 1)
            );
            $documentCategoryRepository = $documentCategoryRepository->reveal();
        }

        if (null === $em) {
            $em = $this->prophesize(EntityManagerInterface::class)->reveal();
        }

        if (null === $normalizer) {
            $normalizer = $this->prophesize(NormalizerInterface::class);
            $normalizer->normalize(Argument::type(Person::class), 'docgen', Argument::any())
                ->willReturn(['type' => 'person']);
            $normalizer = $normalizer->reveal();
        }

        if (null === $parameterBag) {
            $parameterBag = new ParameterBag(['chill_main' => ['acl' => ['form_show_scopes' => true]]]);
        }

        if (null === $scopeRepository) {
            $scopeRepository = $this->prophesize(ScopeRepositoryInterface::class);
            $scopeRepository = $scopeRepository->reveal();
        }

        if (null === $security) {
            $security = $this->prophesize(Security::class);
            $security->getUser()->willReturn(new User());
            $security = $security->reveal();
        }

        if (null === $translator) {
            $translator = $this->prophesize(TranslatorInterface::class)->reveal();
        }

        if (null === $translatableStringHelper) {
            $translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class);
            // return only the 'fr' key
            $translatableStringHelper->localize(Argument::type('array'))->will(static fn ($args) => $args[0]['fr']);
            $translatableStringHelper = $translatableStringHelper->reveal();
        }

        if (null === $thirdPartyRender) {
            $thirdPartyRender = $this->prophesize(ThirdPartyRender::class);
            $thirdPartyRender = $thirdPartyRender->reveal();
        }

        if (null === $thirdPartyRepository) {
            $thirdPartyRepository = $this->prophesize(ThirdPartyRepository::class);
            $thirdPartyRepository = $thirdPartyRepository->reveal();
        }

        if (null === $residentialAddressRepository) {
            $residentialAddressRepository = $this->prophesize(ResidentialAddressRepository::class);
            $residentialAddressRepository->findCurrentResidentialAddressByPerson(Argument::type(Person::class), Argument::any())
                ->willReturn([
                    (new Person\ResidentialAddress())
                        ->setAddress((new Address())
                            ->setStreet('test street')
                            ->setPostcode(new PostalCode())),
                ]);
            $residentialAddressRepository = $residentialAddressRepository->reveal();
        }

        return new PersonContext(
            $authorizationHelper,
            $baseContextData,
            $centerResolverManager,
            $documentCategoryRepository,
            $em,
            $normalizer,
            $parameterBag,
            $scopeRepository,
            $security,
            $translator,
            $translatableStringHelper,
            $thirdPartyRender,
            $thirdPartyRepository,
            $residentialAddressRepository
        );
    }
}
