<?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 Serializer\Normalizer;

use Chill\BudgetBundle\Service\Summary\SummaryBudgetInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Relationships\Relation;
use Chill\PersonBundle\Entity\Relationships\Relationship;
use Chill\PersonBundle\Repository\Relationships\RelationshipRepository;
use Chill\PersonBundle\Serializer\Normalizer\PersonDocGenNormalizer;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

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

    private const BLANK = [
        'id' => '',
        'firstName' => '',
        'lastName' => '',
        'altNames' => '',
        'text' => '',
        'isNull' => true,
        'type' => 'person',
        'birthdate' => ['short' => '', 'long' => ''],
        'deathdate' => ['short' => '', 'long' => ''],
        'gender' => '',
        'genderEntity' => '',
        'civility' => '@ignored',
        'address' => '@ignored',
        'maritalStatus' => '',
        'maritalStatusDate' => ['short' => '', 'long' => ''],
        'maritalStatusComment' => '',
        'email' => '',
        'firstPhoneNumber' => '',
        'fixPhoneNumber' => '',
        'mobilePhoneNumber' => '',
        'nationality' => '',
        'placeOfBirth' => '',
        'memo' => '',
        'numberOfChildren' => '',
        'age' => '@ignored',
        'resources' => [],
        'center' => '@ignored',
    ];

    private NormalizerInterface $normalizer;

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

        $this->normalizer = self::getContainer()->get(NormalizerInterface::class);
    }

    /**
     * @dataProvider dataGeneratorNormalizationNullOrNotNullHaveSameKeys
     */
    public function testNormalizationNullOrNotNullHaveSameKeys(mixed $context)
    {
        $period = new Person();
        $notNullData = $this->buildPersonNormalizer()->normalize($period, 'docgen', $context);
        $nullData = $this->buildPersonNormalizer()->normalize(null, 'docgen', $context);

        $this->assertEqualsCanonicalizing(
            array_keys($notNullData),
            array_keys($nullData),
            'test that the data returned by null value and a Person have the same keys'
        );
    }

    public static function dataGeneratorNormalizationNullOrNotNullHaveSameKeys(): iterable
    {
        yield [['docgen:expects' => Person::class, 'groups' => ['docgen:read']]];

        yield [['docgen:expects' => Person::class, 'groups' => ['docgen:read'], 'docgen:person:with-household' => true]];

        yield [['docgen:expects' => Person::class, 'groups' => ['docgen:read'], 'docgen:person:with-relations' => true]];

        yield [['docgen:expects' => Person::class, 'groups' => ['docgen:read'], 'docgen:person:with-budget' => true]];
    }

    /**
     * @dataProvider generateData
     */
    public function testNormalize(?Person $person, mixed $expected, mixed $msg)
    {
        $normalized = $this->normalizer->normalize($person, 'docgen', [
            'docgen:expects' => Person::class,
            'groups' => 'docgen:read',
        ]);

        $this->assertIsArray($normalized);

        foreach ($normalized as $key => $value) {
            if ('@ignored' === $value) {
                continue;
            }

            $this->assertEquals($value, $normalized[$key]);
        }
        $this->assertEqualsCanonicalizing(array_keys($expected), array_keys($normalized), $msg);
    }

    public function testNormalizePersonWithHousehold()
    {
        $household = new Household();
        $person = new Person();
        $person
            ->setFirstName('Renaud')
            ->setLastName('Mégane');
        $householdMember = new HouseholdMember();
        $householdMember
            ->setPosition((new Position())->setAllowHolder(true)->setLabel(['fr' => 'position'])
                ->setShareHousehold(true))
            ->setHolder(true);
        $person->addHouseholdParticipation($householdMember);
        $household->addMember($householdMember);

        $person = new Person();
        $person
            ->setFirstName('Citroen')
            ->setLastName('Xsara');
        $householdMember = new HouseholdMember();
        $householdMember
            ->setPosition((new Position())->setAllowHolder(true)->setLabel(['fr' => 'position2'])
                ->setShareHousehold(true))
            ->setHolder(false);
        $person->addHouseholdParticipation($householdMember);
        $household->addMember($householdMember);

        $actual = $this->normalizer->normalize($person, 'docgen', [
            'groups' => 'docgen:read',
            'docgen:expects' => Person::class,
            'docgen:person:with-household' => true,
        ]);

        $this->assertCount(2, $household->getMembers());
        $this->assertIsArray($actual);
        $this->assertArrayHasKey('household', $actual);
        $this->assertCount(2, $actual['household']['currentMembers']);
        $this->assertCount(2, $actual['household']['members']);
    }

    public function testNormalizePersonWithRelationships()
    {
        $person = (new Person())->setFirstName('Renaud')->setLastName('megane');
        $father = (new Person())->setFirstName('Clément')->setLastName('megane');
        $mother = (new Person())->setFirstName('Mireille')->setLastName('Mathieu');
        $sister = (new Person())->setFirstName('Callie')->setLastName('megane');

        $relations = [
            (new Relationship())->setFromPerson($person)->setToPerson($father)
                ->setReverse(false)->setRelation((new Relation())->setTitle(['fr' => 'Père'])
                ->setReverseTitle(['fr' => 'Fils'])),
            (new Relationship())->setFromPerson($person)->setToPerson($mother)
                ->setReverse(false)->setRelation((new Relation())->setTitle(['fr' => 'Mère'])
                ->setReverseTitle(['fr' => 'Fils'])),
            (new Relationship())->setFromPerson($person)->setToPerson($sister)
                ->setReverse(true)->setRelation((new Relation())->setTitle(['fr' => 'Frère'])
                ->setReverseTitle(['fr' => 'Soeur'])),
        ];

        $repository = $this->prophesize(RelationshipRepository::class);
        $repository->findByPerson($person)->willReturn($relations);

        $normalizer = $this->buildPersonNormalizer(null, $repository->reveal(), null, null);

        $actual = $normalizer->normalize($person, 'docgen', [
            'groups' => 'docgen:read',
            'docgen:expects' => Person::class,
            'docgen:person:with-relations' => true,
        ]);

        $this->assertIsArray($actual);
        $this->assertArrayHasKey('relations', $actual);
        $this->assertCount(3, $actual['relations']);
    }

    /**
     * Test that the @see{PersonDocGenNormalizer::class} works without interaction with other
     * serializers.
     *
     * @dataProvider generateData
     */
    public function testNormalizeUsingNormalizer(?Person $person, mixed $expected, mixed $msg)
    {
        $normalizer = $this->buildNormalizer();

        $this->assertTrue($normalizer->supportsNormalization($person, 'docgen', [
            'docgen:expects' => Person::class,
            'groups' => ['docgen:read'],
        ]), $msg);

        $this->assertIsArray($normalizer->normalize($person, 'docgen', [
            'docgen:expects' => Person::class,
            'groups' => ['docgen:read'],
        ]), $msg);
    }

    public static function generateData()
    {
        $person = new Person();
        $person
            ->setFirstName('Renaud')
            ->setLastName('Mégane');

        $expected = [...self::BLANK, 'firstName' => 'Renaud', 'lastName' => 'Mégane', 'text' => 'Renaud Mégane'];

        yield [$person, $expected, 'partial normalization for a person'];

        yield [null, self::BLANK, 'normalization for a null person'];
    }

    private function buildNormalizer(
        ?PersonRender $personRender = null,
        ?RelationshipRepository $relationshipRepository = null,
        ?TranslatorInterface $translator = null,
        ?TranslatableStringHelper $translatableStringHelper = null,
        ?NormalizerInterface $normalizer = null,
        ?SummaryBudgetInterface $summaryBudget = null,
    ): PersonDocGenNormalizer {
        if (null === $summaryBudget) {
            $summaryBudget = $this->prophesize(SummaryBudgetInterface::class);
            $summaryBudget->getSummaryForHousehold(Argument::any())->willReturn(
                ['resources' => [], 'charges' => []]
            );
            $summaryBudget->getSummaryForPerson(Argument::any())->willReturn(
                ['resources' => [], 'charges' => []]
            );
        }

        $personDocGenNormalizer = new PersonDocGenNormalizer(
            $personRender ?? self::getContainer()->get(PersonRender::class),
            $relationshipRepository ?? self::getContainer()->get(RelationshipRepository::class),
            //            $translator ?? self::getContainer()->get(TranslatorInterface::class),
            $translatableStringHelper ?? self::getContainer()->get(TranslatableStringHelperInterface::class),
            $summaryBudget->reveal(),
        );

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

        $personDocGenNormalizer->setNormalizer($normalizer->reveal());

        return $personDocGenNormalizer;
    }

    private function buildPersonNormalizer(
        ?PersonRender $personRender = null,
        ?RelationshipRepository $relationshipRepository = null,
        ?TranslatorInterface $translator = null,
        ?TranslatableStringHelper $translatableStringHelper = null,
        ?SummaryBudgetInterface $summaryBudget = null,
    ): PersonDocGenNormalizer {
        if (null === $relationshipRepository) {
            $relationshipRepository = $this->prophesize(RelationshipRepository::class);
            $relationshipRepository->findByPerson(Argument::type(Person::class))->willReturn([]);
            $relationshipRepository = $relationshipRepository->reveal();
        }

        if (null === $summaryBudget) {
            $summaryBudget = $this->prophesize(SummaryBudgetInterface::class);
            $summaryBudget->getSummaryForHousehold(Argument::any())->willReturn(
                ['resources' => [], 'charges' => []]
            );
            $summaryBudget->getSummaryForPerson(Argument::any())->willReturn(
                ['resources' => [], 'charges' => []]
            );
        }

        $normalizer = new PersonDocGenNormalizer(
            $personRender ?? self::getContainer()->get(PersonRender::class),
            $relationshipRepository,
            //            $translator ?? self::getContainer()->get(TranslatorInterface::class),
            $translatableStringHelper ?? self::getContainer()->get(TranslatableStringHelperInterface::class),
            $summaryBudget->reveal()
        );
        $normalizerManager = $this->prophesize(NormalizerInterface::class);
        $normalizerManager->supportsNormalization(Argument::any(), 'docgen', Argument::any())->willReturn(true);
        $normalizerManager->normalize(Argument::type(Person::class), 'docgen', Argument::any())
            ->will(static fn ($args) => $normalizer->normalize($args[0], $args[1], $args[2]));
        $normalizerManager->normalize(Argument::any(), 'docgen', Argument::any())->will(
            static function ($args) {
                if (is_iterable($args[0])) {
                    $r = [];

                    foreach ($args[0] as $i) {
                        $r[] = ['fake' => true, 'hash' => spl_object_hash($i)];
                    }

                    return $r;
                }

                return ['fake' => true, 'hash' => null !== $args[0] ? spl_object_hash($args[0]) : null];
            }
        );
        $normalizer->setNormalizer($normalizerManager->reveal());

        return $normalizer;
    }
}
