<?php

declare(strict_types=1);

namespace App\Tests\Module\Core\Infrastructure\ApiPlatform\State\Processor;

use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\State\ProcessorInterface;
use App\Module\Core\Domain\Entity\Role;
use App\Module\Core\Infrastructure\ApiPlatform\State\Processor\RoleProcessor;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\UnitOfWork;
use LogicException;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use stdClass;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

/**
 * Tests unitaires du RoleProcessor : couvre les gardes metier
 * (immuabilite du code, refus de suppression des roles systeme) et la
 * delegation aux processors Doctrine decores.
 *
 * @internal
 */
#[AllowMockObjectsWithoutExpectations]
final class RoleProcessorTest extends TestCase
{
    private MockObject&ProcessorInterface $persistProcessor;
    private MockObject&ProcessorInterface $removeProcessor;
    private EntityManagerInterface&MockObject $entityManager;
    private MockObject&UnitOfWork $unitOfWork;
    private RoleProcessor $processor;

    protected function setUp(): void
    {
        $this->persistProcessor = $this->createMock(ProcessorInterface::class);
        $this->removeProcessor  = $this->createMock(ProcessorInterface::class);
        $this->entityManager    = $this->createMock(EntityManagerInterface::class);
        $this->unitOfWork       = $this->createMock(UnitOfWork::class);

        $this->entityManager->method('getUnitOfWork')->willReturn($this->unitOfWork);

        $this->processor = new RoleProcessor(
            $this->persistProcessor,
            $this->removeProcessor,
            $this->entityManager,
        );
    }

    public function testDeleteCustomRoleDelegatesToRemoveProcessor(): void
    {
        $role = new Role('editor', 'Editor', false);

        $this->removeProcessor
            ->expects(self::once())
            ->method('process')
            ->with($role)
            ->willReturn(null)
        ;

        $this->persistProcessor->expects(self::never())->method('process');

        $result = $this->processor->process($role, new Delete());

        self::assertNull($result);
    }

    public function testDeleteSystemRoleThrowsAccessDeniedHttpException(): void
    {
        $role = new Role('admin', 'Admin', true);

        $this->removeProcessor->expects(self::never())->method('process');
        $this->persistProcessor->expects(self::never())->method('process');

        $this->expectException(AccessDeniedHttpException::class);
        $this->expectExceptionMessage('Le role systeme "admin" ne peut pas etre supprime.');

        $this->processor->process($role, new Delete());
    }

    public function testPostCreatesCustomRoleDelegatesToPersistProcessor(): void
    {
        $role = new Role('editor', 'Editor', false);

        // Entite nouvelle : l'UnitOfWork n'a pas d'etat d'origine.
        $this->unitOfWork
            ->expects(self::once())
            ->method('getOriginalEntityData')
            ->with($role)
            ->willReturn([])
        ;

        $this->persistProcessor
            ->expects(self::once())
            ->method('process')
            ->with($role)
            ->willReturn($role)
        ;

        $this->removeProcessor->expects(self::never())->method('process');

        $result = $this->processor->process($role, new Post());

        self::assertSame($role, $result);
    }

    public function testPatchWithChangedCodeThrowsBadRequestHttpException(): void
    {
        // L'entite arrive avec le nouveau code deja applique par le denormalizer.
        $role = new Role('editor_renamed', 'Editor', false);
        $this->setRoleId($role, 42);

        $this->unitOfWork
            ->expects(self::once())
            ->method('getOriginalEntityData')
            ->with($role)
            ->willReturn([
                'id'       => 42,
                'code'     => 'editor',
                'label'    => 'Editor',
                'isSystem' => false,
            ])
        ;

        $this->persistProcessor->expects(self::never())->method('process');
        $this->removeProcessor->expects(self::never())->method('process');

        $this->expectException(BadRequestHttpException::class);
        $this->expectExceptionMessage("Le code d'un role est immuable apres creation.");

        $this->processor->process($role, new Patch());
    }

    public function testPatchWithUnchangedCodeDelegatesToPersistProcessor(): void
    {
        $role = new Role('editor', 'Editor modifie', false, 'desc');
        $this->setRoleId($role, 42);

        $this->unitOfWork
            ->expects(self::once())
            ->method('getOriginalEntityData')
            ->with($role)
            ->willReturn([
                'id'       => 42,
                'code'     => 'editor',
                'label'    => 'Editor',
                'isSystem' => false,
            ])
        ;

        $this->persistProcessor
            ->expects(self::once())
            ->method('process')
            ->with($role)
            ->willReturn($role)
        ;

        $this->removeProcessor->expects(self::never())->method('process');

        $result = $this->processor->process($role, new Patch());

        self::assertSame($role, $result);
    }

    public function testPatchSystemRoleLabelDelegatesToPersistProcessor(): void
    {
        // Regle uniforme : un role systeme peut voir son label modifie tant
        // que son code reste inchange. Seul le DELETE est bloque.
        $role = new Role('admin', 'Administrateur', true);
        $this->setRoleId($role, 1);

        $this->unitOfWork
            ->expects(self::once())
            ->method('getOriginalEntityData')
            ->with($role)
            ->willReturn([
                'id'       => 1,
                'code'     => 'admin',
                'label'    => 'Admin',
                'isSystem' => true,
            ])
        ;

        $this->persistProcessor
            ->expects(self::once())
            ->method('process')
            ->with($role)
            ->willReturn($role)
        ;

        $this->removeProcessor->expects(self::never())->method('process');

        $result = $this->processor->process($role, new Patch());

        self::assertSame($role, $result);
    }

    public function testProcessNonRoleDataThrowsLogicException(): void
    {
        // Garde-fou contre une misconfiguration : ce processor est wire
        // exclusivement sur les operations Role.
        $this->persistProcessor->expects(self::never())->method('process');
        $this->removeProcessor->expects(self::never())->method('process');

        $this->expectException(LogicException::class);
        $this->expectExceptionMessage('RoleProcessor attend une instance de');

        $this->processor->process(new stdClass(), new Patch());
    }

    /**
     * Positionne l'id d'un Role via reflection pour simuler une entite deja
     * persistee (les mocks d'UnitOfWork n'alimentent pas l'id tout seul).
     */
    private function setRoleId(Role $role, int $id): void
    {
        $refl = new ReflectionClass($role);
        $prop = $refl->getProperty('id');
        $prop->setAccessible(true);
        $prop->setValue($role, $id);
    }
}
