<?php

declare(strict_types=1);

namespace App\Tests\Module\Commercial\Unit;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\Validator\Exception\ValidationException;
use App\Module\Commercial\Application\Service\SupplierFieldNormalizer;
use App\Module\Commercial\Application\Validator\SupplierInformationCompletenessValidator;
use App\Module\Commercial\Domain\Entity\Supplier;
use App\Module\Commercial\Infrastructure\ApiPlatform\State\Processor\SupplierProcessor;
use App\Shared\Domain\Contract\BusinessRoleAwareInterface;
use App\Shared\Domain\Security\BusinessRoles;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\UnitOfWork;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Tests unitaires du SupplierProcessor — perimetre ERP-89 : detection du role
 * Commerciale cote back (RG-2.03). Les autres responsabilites du processor
 * (gating accounting / archive / mode strict) sont heritees d'ERP-87 et testees
 * a leur niveau ; les RG inter-champs (RG-2.07/2.08/2.10) sont des contraintes
 * d'entite (cf. SupplierValidationTest), non portees ici.
 *
 * @internal
 */
final class SupplierProcessorTest extends TestCase
{
    public function testCommercialeIncompleteInformationIsUnprocessable(): void
    {
        // RG-2.03 : role Commerciale + onglet Information incomplet -> 422, meme
        // sur un POST (les champs Information n'y sont pas renseignables).
        $supplier = $this->minimalSupplier();
        $supplier->setDescription('Une description'); // les autres champs Information restent null

        $processor = $this->makeProcessor(
            payload: ['description' => 'Une description'],
            user: $this->commercialeUser(),
        );

        $this->expectException(ValidationException::class);
        $processor->process($supplier, $this->operation());
    }

    public function testCommercialeIncompleteInformationOnMainOnlyPatchIsUnprocessable(): void
    {
        // RG-2.03 : pour une Commerciale, la completude Information est exigee
        // meme quand le payload ne touche PAS l'onglet Information (ici
        // companyName seul) -> 422.
        $supplier = $this->minimalSupplier();
        $supplier->setCompanyName('Renamed Co');

        $processor = $this->makeProcessor(
            granted: ['commercial.suppliers.manage'],
            payload: ['companyName' => 'Renamed Co'],
            user: $this->commercialeUser(),
            managed: true,
            originalData: [
                'companyName' => 'TEST CO',
                'isArchived'  => false,
            ],
        );

        $this->expectException(ValidationException::class);
        $processor->process($supplier, $this->operation());
    }

    public function testCommercialeCompleteInformationPasses(): void
    {
        // RG-2.03 satisfaite : tous les champs Information renseignes -> 200.
        $supplier = $this->completeInformationSupplier();

        $processor = $this->makeProcessor(
            granted: ['commercial.suppliers.manage'],
            payload: ['description' => 'desc'],
            user: $this->commercialeUser(),
        );

        self::assertInstanceOf(Supplier::class, $processor->process($supplier, $this->operation()));
    }

    public function testNonCommercialeSkipsInformationCompleteness(): void
    {
        // Meme onglet Information incomplet, mais user non-Commerciale -> aucun
        // blocage (la completude est specifique a la Commerciale).
        $supplier = $this->minimalSupplier();
        $supplier->setDescription('Une description');

        $processor = $this->makeProcessor(
            payload: ['description' => 'Une description'],
            user: null,
        );

        self::assertInstanceOf(Supplier::class, $processor->process($supplier, $this->operation()));
    }

    public function testAdminIncompleteInformationPasses(): void
    {
        // Distinct du cas user=null : un utilisateur AUTHENTIFIE mais non-Commerciale
        // (ici un admin, BusinessRoleAwareInterface renvoyant false pour tout role
        // metier) n'est pas soumis a la completude Information -> 200 malgre un
        // onglet Information incomplet. Prouve que le gate porte bien sur le ROLE
        // metier Commerciale, et pas sur « il y a un utilisateur connecte ».
        $supplier = $this->minimalSupplier();
        $supplier->setDescription('Une description');

        $processor = $this->makeProcessor(
            payload: ['description' => 'Une description'],
            user: $this->adminUser(),
        );

        self::assertInstanceOf(Supplier::class, $processor->process($supplier, $this->operation()));
    }

    /**
     * @param list<string>         $granted      Permissions accordees a l'utilisateur courant
     * @param array<string, mixed> $payload      Corps JSON simule de la requete
     * @param bool                 $managed      true = entite geree par l'ORM (PATCH), false = creation (POST)
     * @param array<string, mixed> $originalData Etat persiste simule (getOriginalEntityData)
     */
    private function makeProcessor(
        array $granted = [],
        array $payload = [],
        ?UserInterface $user = null,
        bool $managed = false,
        array $originalData = [],
    ): SupplierProcessor {
        $persist = new class implements ProcessorInterface {
            public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
            {
                return $data;
            }
        };

        $security = $this->createStub(Security::class);
        $security->method('isGranted')->willReturnCallback(
            static fn (mixed $attribute): bool => is_string($attribute) && in_array($attribute, $granted, true),
        );
        $security->method('getUser')->willReturn($user);

        $requestStack = new RequestStack();
        $requestStack->push(new Request([], [], [], [], [], [], json_encode($payload, JSON_THROW_ON_ERROR)));

        $uow = $this->createMock(UnitOfWork::class);
        $uow->method('getOriginalEntityData')->willReturn($originalData);

        $em = $this->createMock(EntityManagerInterface::class);
        $em->method('contains')->willReturn($managed);
        $em->method('getUnitOfWork')->willReturn($uow);

        return new SupplierProcessor(
            $persist,
            new SupplierFieldNormalizer(),
            new SupplierInformationCompletenessValidator(),
            $security,
            $requestStack,
            $em,
        );
    }

    private function minimalSupplier(): Supplier
    {
        $supplier = new Supplier();
        $supplier->setCompanyName('Test Co');

        return $supplier;
    }

    private function completeInformationSupplier(): Supplier
    {
        $supplier = $this->minimalSupplier();
        $supplier->setDescription('desc');
        $supplier->setCompetitors('concurrents');
        $supplier->setFoundedAt(new DateTimeImmutable('2010-01-01'));
        $supplier->setEmployeesCount(10);
        $supplier->setRevenueAmount('1000.00');
        $supplier->setDirectorName('Marie Durand');
        $supplier->setProfitAmount('100.00');
        $supplier->setVolumeForecast(500);

        return $supplier;
    }

    private function operation(): Operation
    {
        return $this->createStub(Operation::class);
    }

    /**
     * Utilisateur authentifie non-Commerciale (profil admin) : porte
     * BusinessRoleAwareInterface mais ne reconnait aucun role metier. Sert a
     * distinguer « pas de role Commerciale » de « pas d'utilisateur » (null).
     */
    private function adminUser(): UserInterface
    {
        return new class implements UserInterface, BusinessRoleAwareInterface {
            public function hasBusinessRole(string $roleCode): bool
            {
                return false;
            }

            public function getRoles(): array
            {
                return ['ROLE_ADMIN'];
            }

            public function eraseCredentials(): void {}

            public function getUserIdentifier(): string
            {
                return 'admin-test';
            }
        };
    }

    private function commercialeUser(): UserInterface
    {
        return new class implements UserInterface, BusinessRoleAwareInterface {
            public function hasBusinessRole(string $roleCode): bool
            {
                return BusinessRoles::COMMERCIALE === $roleCode;
            }

            public function getRoles(): array
            {
                return ['ROLE_USER'];
            }

            public function eraseCredentials(): void {}

            public function getUserIdentifier(): string
            {
                return 'commerciale-test';
            }
        };
    }
}
