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

use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Gender;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonDenormalizer;
use libphonenumber\PhoneNumber;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

/**
 * @internal
 *
 * @covers \Chill\PersonBundle\Serializer\Normalizer\PersonJsonDenormalizer
 */
final class PersonJsonDenormalizerTest extends TestCase
{
    private function createIdentifierManager(): PersonIdentifierManagerInterface
    {
        return new class () implements PersonIdentifierManagerInterface {
            public function getWorkers(): array
            {
                return [];
            }

            public function buildWorkerByPersonIdentifierDefinition(int|PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker
            {
                if (is_int($personIdentifierDefinition)) {
                    $definition = new PersonIdentifierDefinition(['en' => 'Test'], 'dummy');
                    // Force the id for testing purposes
                    $r = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id');
                    $r->setAccessible(true);
                    $r->setValue($definition, $personIdentifierDefinition);
                } else {
                    $definition = $personIdentifierDefinition;
                }

                $engine = new class () implements PersonIdentifierEngineInterface {
                    public static function getName(): string
                    {
                        return 'dummy';
                    }

                    public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
                    {
                        // trivial canonicalization for tests
                        return isset($value['content']) ? (string) $value['content'] : null;
                    }

                    public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}

                    public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
                    {
                        return '';
                    }

                    public function isEmpty(PersonIdentifier $identifier): bool
                    {
                        $value = $identifier->getValue();
                        $content = isset($value['content']) ? trim((string) $value['content']) : '';

                        return '' === $content;
                    }

                    public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
                    {
                        return [];
                    }

                    public function getDefaultValue(PersonIdentifierDefinition $definition): array
                    {
                        return [];
                    }
                };

                return new PersonIdentifierWorker($engine, $definition);
            }
        };
    }

    public function testSupportsDenormalizationReturnsTrueForValidData(): void
    {
        $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());

        $data = [
            'type' => 'person',
            // important: new Person (creation) must not contain an id
        ];

        self::assertTrue($denormalizer->supportsDenormalization($data, Person::class));
    }

    public function testSupportsDenormalizationReturnsFalseForInvalidData(): void
    {
        $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());

        // not an array
        self::assertFalse($denormalizer->supportsDenormalization('not-an-array', Person::class));

        // missing type
        self::assertFalse($denormalizer->supportsDenormalization([], Person::class));

        // wrong type value
        self::assertFalse($denormalizer->supportsDenormalization(['type' => 'not-person'], Person::class));

        // id present means it's not a create payload for this denormalizer
        self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person', 'id' => 123], Person::class));

        // wrong target class
        self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person'], \stdClass::class));
    }

    public function testDenormalizeMapsPayloadToPersonProperties(): void
    {
        $json = <<<'JSON'
            {
              "type": "person",
              "firstName": "Jérome",
              "lastName": "diallo",
              "altNames": [
                {
                  "key": "jeune_fille",
                  "value": "FJ"
                }
              ],
              "birthdate": null,
              "deathdate": null,
              "phonenumber": "",
              "mobilenumber": "",
              "email": "",
              "gender": {
                "id": 5,
                "type": "chill_main_gender"
              },
              "center": {
                "id": 1,
                "type": "center"
              },
              "civility": null,
              "identifiers": [
                {
                  "type": "person_identifier",
                  "value": {
                    "content": "789456"
                  },
                  "definition_id": 5
                }
              ]
            }
            JSON;
        $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);

        $inner = new class () implements DenormalizerInterface {
            public ?Gender $gender = null;
            public ?Center $center = null;

            public function denormalize($data, $type, $format = null, array $context = [])
            {
                if (PhoneNumber::class === $type) {
                    return '' === $data ? null : new PhoneNumber();
                }
                if (\DateTime::class === $type || \DateTimeImmutable::class === $type) {
                    return null === $data ? null : new \DateTimeImmutable((string) $data);
                }
                if (Gender::class === $type) {
                    return $this->gender ??= new Gender();
                }
                if (Center::class === $type) {
                    return $this->center ??= new Center();
                }
                if (Civility::class === $type) {
                    return null; // input is null in our payload
                }

                return null;
            }

            public function supportsDenormalization($data, $type, $format = null)
            {
                return true;
            }
        };

        $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
        $denormalizer->setDenormalizer($inner);

        $person = $denormalizer->denormalize($data, Person::class);

        self::assertInstanceOf(Person::class, $person);
        self::assertSame('Jérome', $person->getFirstName());
        self::assertSame('diallo', $person->getLastName());

        // phone numbers: empty strings map to null via the inner denormalizer stub
        self::assertNull($person->getPhonenumber());
        self::assertNull($person->getMobilenumber());

        // email passes through as is
        self::assertSame('', $person->getEmail());

        // nested objects are provided by our inner denormalizer and must be set back on the Person
        self::assertSame($inner->gender, $person->getGender());
        self::assertSame($inner->center, $person->getCenter());

        // dates are null in the provided payload
        self::assertNull($person->getBirthdate());
        self::assertNull($person->getDeathdate());

        // civility is null as provided
        self::assertNull($person->getCivility());

        // altNames: make sure the alt name with key jeune_fille has label FJ
        $found = false;
        foreach ($person->getAltNames() as $altName) {
            if ('jeune_fille' === $altName->getKey()) {
                $found = true;
                self::assertSame('FJ', $altName->getLabel());
            }
        }
        self::assertTrue($found, 'Expected altNames to contain key "jeune_fille"');

        $found = false;
        foreach ($person->getIdentifiers() as $identifier) {
            if (5 === $identifier->getDefinition()->getId()) {
                $found = true;
                self::assertSame(['content' => '789456'], $identifier->getValue());
            }
        }
        self::assertTrue($found, 'Expected identifiers with definition id 5');
    }

    public function testDenormalizeRemovesEmptyIdentifier(): void
    {
        $data = [
            'type' => 'person',
            'firstName' => 'Alice',
            'lastName' => 'Smith',
            'identifiers' => [
                [
                    'type' => 'person_identifier',
                    'value' => ['content' => ''],
                    'definition_id' => 7,
                ],
            ],
        ];

        $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());

        $person = $denormalizer->denormalize($data, Person::class);

        // The identifier with empty content must be considered empty and removed
        self::assertSame(0, $person->getIdentifiers()->count(), 'Expected no identifiers to remain on the person');
    }

    public function testDenormalizeRemovesPreviouslyExistingIdentifierWhenIncomingValueIsEmpty(): void
    {
        // Prepare an existing Person with a pre-existing identifier (definition id = 9)
        $definition = new PersonIdentifierDefinition(['en' => 'Test'], 'dummy');
        $ref = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id');
        $ref->setValue($definition, 9);

        $existingIdentifier = new PersonIdentifier($definition);
        $existingIdentifier->setValue(['content' => 'ABC']);

        $person = new Person();
        $person->addIdentifier($existingIdentifier);

        // Also set the identifier's own id = 9 so that the denormalizer logic matches it
        // (the current denormalizer matches by PersonIdentifier->getId() === definition_id)
        $refId = new \ReflectionProperty(PersonIdentifier::class, 'id');
        $refId->setValue($existingIdentifier, 9);

        // Incoming payload sets the same definition id with an empty value
        $data = [
            'type' => 'person',
            'identifiers' => [
                [
                    'type' => 'person_identifier',
                    'value' => ['content' => ''],
                    'definition_id' => 9,
                ],
            ],
        ];

        $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());

        // Use AbstractNormalizer::OBJECT_TO_POPULATE to update the existing person
        $result = $denormalizer->denormalize($data, Person::class, null, [
            AbstractNormalizer::OBJECT_TO_POPULATE => $person,
        ]);

        self::assertSame($person, $result, 'Denormalizer should update and return the provided Person instance');
        self::assertSame(0, $person->getIdentifiers()->count(), 'The previously existing identifier should be removed when incoming value is empty');
    }
}
