<?php

declare(strict_types=1);

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

use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Post;
use ApiPlatform\State\ProcessorInterface;
use App\Module\Sites\Application\Service\CurrentSiteProviderInterface;
use App\Module\Sites\Domain\Entity\Site;
use App\Module\Sites\Infrastructure\ApiPlatform\State\Processor\SiteAwareInjectionProcessor;
use App\Shared\Domain\Contract\SiteAwareInterface;
use App\Shared\Domain\Contract\SiteInterface;
use PHPUnit\Framework\TestCase;
use stdClass;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

/**
 * Tests unitaires du SiteAwareInjectionProcessor.
 *
 * Mocks isoles : le processor decore, donc on verifie (a) les mutations
 * appliquees sur $data avant delegation, (b) que inner->process est
 * toujours invoque sauf en cas de throw, (c) le throw 400 explicite si
 * $data SiteAware sans site + provider null.
 *
 * @internal
 */
final class SiteAwareInjectionProcessorTest extends TestCase
{
    public function testInjectsCurrentSiteOnSiteAwareEntityWithoutSite(): void
    {
        $currentSite = new Site('Chatellerault', 'Rue', null, '86100', 'Chatellerault', '#056CF2');
        $data        = $this->makeSiteAwareStub(null);

        $inner = $this->createMock(ProcessorInterface::class);
        $inner->expects(self::once())
            ->method('process')
            ->willReturnArgument(0)
        ;

        $processor = $this->makeProcessor($inner, $currentSite);
        $processor->process($data, $this->makeOperation());

        self::assertSame($currentSite, $data->getSite());
    }

    public function testDoesNotOverrideExistingSite(): void
    {
        $existingSite = new Site('Existing', 'Rue', null, '12345', 'Ville', '#000000');
        $currentSite  = new Site('Chatellerault', 'Rue', null, '86100', 'Chatellerault', '#056CF2');
        $data         = $this->makeSiteAwareStub($existingSite);

        $inner = $this->createMock(ProcessorInterface::class);
        $inner->expects(self::once())->method('process');

        $processor = $this->makeProcessor($inner, $currentSite);
        $processor->process($data, $this->makeOperation());

        self::assertSame(
            $existingSite,
            $data->getSite(),
            'Un site deja positionne doit etre preserve, pas ecrase par le currentSite.',
        );
    }

    public function testSkipsNonSiteAwareData(): void
    {
        $nonSiteAware = new stdClass();

        $inner = $this->createMock(ProcessorInterface::class);
        $inner->expects(self::once())
            ->method('process')
            ->with($nonSiteAware)
        ;

        $processor = $this->makeProcessor(
            $inner,
            new Site('Any', 'Rue', null, '12345', 'Ville', '#000000'),
        );
        $processor->process($nonSiteAware, $this->makeOperation());
    }

    public function testThrowsBadRequestIfSiteAwareAndNoCurrentSite(): void
    {
        $data = $this->makeSiteAwareStub(null);

        $inner = $this->createMock(ProcessorInterface::class);
        $inner->expects(self::never())
            ->method('process')
        ;

        $processor = $this->makeProcessor($inner, currentSite: null);

        $this->expectException(BadRequestHttpException::class);
        $this->expectExceptionMessage('aucun site selectionne');

        $processor->process($data, $this->makeOperation());
    }

    public function testDelegatesToInnerProcessorAlwaysWhenNoThrow(): void
    {
        $data  = new stdClass();
        $inner = $this->createMock(ProcessorInterface::class);
        $inner->expects(self::once())
            ->method('process')
            ->willReturn('delegated-result')
        ;

        $processor = $this->makeProcessor(
            $inner,
            new Site('Any', 'Rue', null, '12345', 'Ville', '#000000'),
        );
        $result = $processor->process($data, $this->makeOperation());

        self::assertSame('delegated-result', $result);
    }

    private function makeProcessor(
        ProcessorInterface $inner,
        ?Site $currentSite,
    ): SiteAwareInjectionProcessor {
        // createStub : on n'a besoin que de fixer la valeur de retour, pas
        // d'attentes sur le nombre d'appels. Evite la notice PHPUnit
        // "No expectations were configured for the mock object".
        $provider = $this->createStub(CurrentSiteProviderInterface::class);
        $provider->method('get')->willReturn($currentSite);

        // Stub Security : bypass_scope = true par defaut pour preserver le
        // comportement des tests historiques (pas de validation cross-site).
        // Les tests dedies a la validation cross-site instancient leur propre
        // Security via un helper dedie.
        $security = $this->createStub(Security::class);
        $security->method('isGranted')->willReturn(true);

        return new SiteAwareInjectionProcessor($inner, $provider, $security);
    }

    private function makeSiteAwareStub(?Site $initialSite): SiteAwareInterface
    {
        return new class($initialSite) implements SiteAwareInterface {
            public function __construct(private ?SiteInterface $site) {}

            public function getSite(): ?SiteInterface
            {
                return $this->site;
            }

            public function setSite(SiteInterface $site): void
            {
                $this->site = $site;
            }
        };
    }

    private function makeOperation(): Operation
    {
        return new Post();
    }
}
