<?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\PersonIdentifier\Validator;

use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\PersonIdentifier\Validator\UniqueIdentifierConstraint;
use Chill\PersonBundle\PersonIdentifier\Validator\UniqueIdentifierConstraintValidator;
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;

/**
 * @internal
 */
#[CoversClass(UniqueIdentifierConstraintValidator::class)]
final class UniqueIdentifierConstraintValidatorTest extends ConstraintValidatorTestCase
{
    use ProphecyTrait;

    /**
     * @var ObjectProphecy|PersonIdentifierRepository
     */
    private ObjectProphecy $repository;

    /**
     * @var ObjectProphecy|PersonRenderInterface
     */
    private ObjectProphecy $personRender;

    protected function setUp(): void
    {
        $this->repository = $this->prophesize(PersonIdentifierRepository::class);
        $this->personRender = $this->prophesize(PersonRenderInterface::class);
        parent::setUp();
    }

    protected function createValidator(): UniqueIdentifierConstraintValidator
    {
        return new UniqueIdentifierConstraintValidator($this->repository->reveal(), $this->personRender->reveal());
    }

    public function testThrowsOnInvalidConstraintType(): void
    {
        $this->expectException(UnexpectedTypeException::class);

        // Provide a valid value so execution reaches the constraint type check
        $definition = new PersonIdentifierDefinition(['en' => 'Test'], 'string');
        $identifier = new PersonIdentifier($definition);
        $identifier->setValue(['value' => 'ABC']);

        $this->validator->validate($identifier, new NotBlank());
    }

    public function testThrowsOnInvalidValueType(): void
    {
        $this->expectException(UnexpectedValueException::class);
        $this->validator->validate(new \stdClass(), new UniqueIdentifierConstraint());
    }

    public function testNoViolationWhenNoDuplicate(): void
    {
        $definition = new PersonIdentifierDefinition(['en' => 'Test'], 'string');
        $identifier = new PersonIdentifier($definition);
        $identifier->setValue(['value' => 'UNIQ']);

        // Configure repository mock to return empty array
        $this->repository->findByDefinitionAndCanonical($definition, ['value' => 'UNIQ'])->willReturn([]);

        $this->validator->validate($identifier, new UniqueIdentifierConstraint());
        $this->assertNoViolation();
    }

    public function testViolationWhenDuplicateFound(): void
    {
        $definition = new PersonIdentifierDefinition(['en' => 'SSN'], 'string');
        $reflectionClass = new \ReflectionClass($definition);
        $reflectionId = $reflectionClass->getProperty('id');
        $reflectionId->setValue($definition, 1);

        $personA = new Person();
        $personA->setFirstName('Alice')->setLastName('Anderson');
        $personB = new Person();
        $personB->setFirstName('Bob')->setLastName('Brown');

        $dup1 = new PersonIdentifier($definition);
        $dup1->setPerson($personA);
        $dup1->setValue(['value' => '123']);
        $dup2 = new PersonIdentifier($definition);
        $dup2->setPerson($personB);
        $dup2->setValue(['value' => '123']);

        // Repository returns duplicates
        $this->repository->findByDefinitionAndCanonical($definition, ['value' => '123'])->willReturn([$dup1, $dup2]);

        // Person renderer returns names
        $this->personRender->renderString($personA, Argument::type('array'))->willReturn('Alice Anderson');
        $this->personRender->renderString($personB, Argument::type('array'))->willReturn('Bob Brown');

        $identifier = new PersonIdentifier($definition);
        $identifier->setPerson(new Person());
        $identifier->setValue(['value' => '123']);

        $constraint = new UniqueIdentifierConstraint();

        $this->validator->validate($identifier, $constraint);

        $this->buildViolation($constraint->message)
            ->setParameter('{{ persons }}', 'Alice Anderson, Bob Brown')
            ->setParameter('definition_id', '1')
            ->assertRaised();
    }

    public function testViolationWhenDuplicateFoundButForSamePerson(): void
    {
        $definition = new PersonIdentifierDefinition(['en' => 'SSN'], 'string');
        $reflectionClass = new \ReflectionClass($definition);
        $reflectionId = $reflectionClass->getProperty('id');
        $reflectionId->setValue($definition, 1);

        $personA = new Person();
        $personA->setFirstName('Alice')->setLastName('Anderson');

        $dup1 = new PersonIdentifier($definition);
        $dup1->setPerson($personA);
        $dup1->setValue(['value' => '123']);

        // Repository returns duplicates
        $this->repository->findByDefinitionAndCanonical($definition, ['value' => '123'])->willReturn([$dup1]);

        $identifier = new PersonIdentifier($definition);
        $identifier->setPerson($personA);
        $identifier->setValue(['value' => '123']);

        $constraint = new UniqueIdentifierConstraint();

        $this->validator->validate($identifier, $constraint);

        $this->assertNoViolation();
    }
}
