<?php

declare(strict_types=1);

namespace App\Tests\Module\Sites\Infrastructure\ApiPlatform\Extension;

use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
use App\Module\Core\Domain\Entity\User;
use App\Module\Sites\Application\Service\CurrentSiteProviderInterface;
use App\Module\Sites\Domain\Entity\Site;
use App\Module\Sites\Infrastructure\ApiPlatform\Extension\SiteScopedQueryExtension;
use App\Tests\Fixtures\SiteAware\FakeSiteAwareEntity;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\SecurityBundle\Security;

/**
 * Tests d'integration de SiteScopedQueryExtension.
 *
 * Approche : on cree la table `fake_site_aware_entity` a la volee via
 * SchemaTool dans le setUp, on y persiste 2 entites sur siteA + 1 sur
 * siteB, puis on construit un QueryBuilder via EntityManager et on
 * invoque l'extension a la main (pas besoin de monter un endpoint API
 * Platform complet — on teste la logique du filtre).
 *
 * @internal
 */
final class SiteScopedQueryExtensionTest extends KernelTestCase
{
    private EntityManagerInterface $em;
    private Site $siteA;
    private Site $siteB;

    protected function setUp(): void
    {
        self::bootKernel();
        $container = self::getContainer();

        /** @var EntityManagerInterface $em */
        $em       = $container->get(EntityManagerInterface::class);
        $this->em = $em;

        // Creation de la table fake_site_aware_entity uniquement.
        // La base de test partage deja les autres tables (site, user, etc.).
        $metadata = $this->em->getClassMetadata(FakeSiteAwareEntity::class);
        $schema   = new SchemaTool($this->em);
        // Drop si existe deja (re-run des tests), puis create.
        $schema->dropSchema([$metadata]);
        $schema->createSchema([$metadata]);

        // Fixtures locales : 2 entites sur siteA, 1 sur siteB.
        $this->siteA = $this->em->getRepository(Site::class)->findOneBy(['name' => 'Chatellerault']);
        $this->siteB = $this->em->getRepository(Site::class)->findOneBy(['name' => 'Saint-Jean']);
        self::assertNotNull($this->siteA);
        self::assertNotNull($this->siteB);

        $e1 = new FakeSiteAwareEntity('A-in-site-A');
        $e1->setSite($this->siteA);
        $e2 = new FakeSiteAwareEntity('B-in-site-A');
        $e2->setSite($this->siteA);
        $e3 = new FakeSiteAwareEntity('C-in-site-B');
        $e3->setSite($this->siteB);

        $this->em->persist($e1);
        $this->em->persist($e2);
        $this->em->persist($e3);
        $this->em->flush();
        $this->em->clear();
    }

    protected function tearDown(): void
    {
        // Drop de la table fake entre tests pour eviter toute pollution.
        if (isset($this->em)) {
            $metadata = $this->em->getClassMetadata(FakeSiteAwareEntity::class);
            $schema   = new SchemaTool($this->em);
            $schema->dropSchema([$metadata]);
            $this->em->close();
        }

        parent::tearDown();
    }

    public function testCollectionFilteredByCurrentSite(): void
    {
        $extension = $this->makeExtension($this->siteA);

        $results = $this->runQuery($extension, FakeSiteAwareEntity::class);

        self::assertCount(2, $results, '2 entites sur siteA doivent etre retournees.');
        foreach ($results as $entity) {
            self::assertSame($this->siteA->getId(), $entity->getSite()->getId());
        }
    }

    public function testCollectionSwitchesToSiteB(): void
    {
        $extension = $this->makeExtension($this->siteB);

        $results = $this->runQuery($extension, FakeSiteAwareEntity::class);

        self::assertCount(1, $results);
        self::assertSame($this->siteB->getId(), $results[0]->getSite()->getId());
    }

    public function testNoOpIfNoCurrentSite(): void
    {
        // Decision assumee (ticket 4 spec Risque 1) : no-op plutot que
        // collection vide. L'user sans currentSite voit TOUTES les entites.
        $extension = $this->makeExtension(currentSite: null);

        $results = $this->runQuery($extension, FakeSiteAwareEntity::class);

        self::assertCount(3, $results);
    }

    public function testNoOpIfBypassScopePermission(): void
    {
        // User avec sites.bypass_scope voit TOUTES les entites, meme
        // avec un currentSite positionne. Comportement admin / audit.
        $extension = $this->makeExtension($this->siteA, bypassScope: true);

        $results = $this->runQuery($extension, FakeSiteAwareEntity::class);

        self::assertCount(3, $results);
    }

    public function testNoOpIfResourceClassNotSiteAware(): void
    {
        // Une resource qui n'implemente pas SiteAwareInterface ne doit
        // jamais etre filtree (l'extension se contente d'un `return` tot).
        $extension = $this->makeExtension($this->siteA);

        // On query les users (non SiteAware). Verification robuste : on
        // inspecte la partie WHERE du QueryBuilder avant et apres l'appel
        // a l'extension. Le before/after doit etre identique (idealement
        // null dans les deux cas vu qu'on n'a pas ajoute de WHERE).
        $qb      = $this->em->createQueryBuilder()->select('u')->from(User::class, 'u');
        $nameGen = new QueryNameGenerator();

        $whereBefore = $qb->getDQLPart('where');
        $extension->applyToCollection($qb, $nameGen, User::class);
        $whereAfter = $qb->getDQLPart('where');

        self::assertEquals(
            $whereBefore,
            $whereAfter,
            'La clause WHERE du QueryBuilder doit etre intacte pour une resource non SiteAware.',
        );
    }

    public function testItemNotFoundIfWrongSite(): void
    {
        // GET /api/entity/{id} pour un item du siteB alors que l'user est
        // sur siteA -> le filtre ajoute `WHERE site = siteA`, la requete
        // retourne null -> API Platform renverra 404.
        $em      = $this->em;
        $entityB = $em->getRepository(FakeSiteAwareEntity::class)
            ->findOneBy(['name' => 'C-in-site-B'])
        ;
        self::assertNotNull($entityB);
        $idB = $entityB->getId();
        $em->clear();

        $extension = $this->makeExtension($this->siteA);

        $qb = $this->em->createQueryBuilder()
            ->select('e')
            ->from(FakeSiteAwareEntity::class, 'e')
            ->andWhere('e.id = :id')
            ->setParameter('id', $idB)
        ;
        $nameGen = new QueryNameGenerator();

        $extension->applyToItem($qb, $nameGen, FakeSiteAwareEntity::class, ['id' => $idB]);

        self::assertNull($qb->getQuery()->getOneOrNullResult());
    }

    public function testItemFoundIfCorrectSite(): void
    {
        $entityA = $this->em->getRepository(FakeSiteAwareEntity::class)
            ->findOneBy(['name' => 'A-in-site-A'])
        ;
        self::assertNotNull($entityA);
        $idA = $entityA->getId();
        $this->em->clear();

        $extension = $this->makeExtension($this->siteA);

        $qb = $this->em->createQueryBuilder()
            ->select('e')
            ->from(FakeSiteAwareEntity::class, 'e')
            ->andWhere('e.id = :id')
            ->setParameter('id', $idA)
        ;
        $nameGen = new QueryNameGenerator();

        $extension->applyToItem($qb, $nameGen, FakeSiteAwareEntity::class, ['id' => $idA]);

        $result = $qb->getQuery()->getOneOrNullResult();
        self::assertNotNull($result);
        self::assertSame('A-in-site-A', $result->getName());
    }

    /**
     * Construit une extension avec un provider et un security mockes selon
     * le scenario testé. Passe par reflection pour forcer le flag
     * sitesActive du provider sans toucher au filesystem.
     */
    private function makeExtension(?Site $currentSite, bool $bypassScope = false): SiteScopedQueryExtension
    {
        // createStub : pas d'attentes sur le nombre d'appels, juste fixer
        // les valeurs de retour des methodes sollicitees. Evite les notices
        // PHPUnit "No expectations configured".
        $security = $this->createStub(Security::class);
        $security->method('isGranted')->willReturnCallback(
            fn (string $perm): bool => 'sites.bypass_scope' === $perm && $bypassScope,
        );

        $provider = $this->createStub(CurrentSiteProviderInterface::class);
        $provider->method('get')->willReturn($currentSite);

        return new SiteScopedQueryExtension($provider, $security);
    }

    private function runQuery(SiteScopedQueryExtension $extension, string $resourceClass): array
    {
        $qb      = $this->em->createQueryBuilder()->select('e')->from($resourceClass, 'e');
        $nameGen = new QueryNameGenerator();

        $extension->applyToCollection($qb, $nameGen, $resourceClass);

        return $qb->getQuery()->getResult();
    }
}
