<?php

declare(strict_types=1);

namespace App\Tests\Architecture;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use Symfony\Component\Finder\Finder;

/**
 * Garde-fou architecture : toute operation `GetCollection` exposee via API Platform
 * doit avoir la pagination activee (ou laisser la valeur par defaut, qui est
 * activee globalement dans `config/packages/api_platform.yaml`).
 *
 * Interdit : `new GetCollection(paginationEnabled: false)` sans exception documentee.
 *
 * Raison : une collection non paginee peut retourner des milliers de lignes et
 * saturer la memoire du serveur, le reseau et le navigateur. La pagination est la
 * seule protection fiable contre ce risque sur un CRM a donnees croissantes.
 *
 * Quand ajouter une entree dans `EXCLUDED` :
 * - La collection est structurellement bornee (referentiel statique, < 100 items,
 *   jamais alimente par des utilisateurs) ET la suppression de la pagination est
 *   documentee avec une justification metier explicite.
 * - Format obligatoire : `FQCN => 'justification + reference ticket/spec'`
 *
 * @internal
 */
final class CollectionsArePaginatedTest extends TestCase
{
    /**
     * Resources API Platform dont un `GetCollection` peut desactiver la pagination.
     *
     * Laisser vide au demarrage. Pour ajouter une exception :
     *   'App\Module\Foo\Infrastructure\ApiPlatform\Resource\BarResource'
     *       => 'Referentiel statique < 50 items (types de contrat). Cf. ERP-XX.',
     *
     * @var array<class-string, string>
     */
    private const EXCLUDED = [];

    public function testAllGetCollectionOperationsHavePaginationEnabled(): void
    {
        $finder = new Finder()
            ->files()
            ->in(__DIR__.'/../../src')
            ->name('*.php')
            ->contains('#[ApiResource')
        ;

        // Garde : si le scan ne trouve rien, le chemin est casse — le test
        // deviendrait un faux positif vert. On verifie qu'il a du grain a moudre.
        self::assertNotEmpty(
            iterator_to_array($finder),
            'Aucun fichier #[ApiResource] trouve sous src/ : chemin invalide ou codebase vide.',
        );

        foreach ($finder as $file) {
            $fqcn = $this->extractFqcn($file->getRealPath());
            if (null === $fqcn || !class_exists($fqcn)) {
                continue;
            }

            $reflection            = new ReflectionClass($fqcn);
            $apiResourceAttributes = $reflection->getAttributes(ApiResource::class);

            if ([] === $apiResourceAttributes) {
                continue;
            }

            foreach ($apiResourceAttributes as $attribute) {
                /** @var ApiResource $apiResource */
                $apiResource = $attribute->newInstance();
                $operations  = $apiResource->getOperations()?->getIterator() ?? [];

                foreach ($operations as $operation) {
                    if (!$operation instanceof GetCollection) {
                        continue;
                    }

                    if (false !== $operation->getPaginationEnabled()) {
                        continue;
                    }

                    // La pagination est explicitement desactivee : verifier
                    // que la resource est dans la whitelist EXCLUDED.
                    self::assertArrayHasKey(
                        $fqcn,
                        self::EXCLUDED,
                        sprintf(
                            "La resource %s desactive la pagination sur une operation GetCollection.\n"
                            ."Regle : toute collection API Platform doit etre paginee (cf. .claude/rules/backend.md).\n"
                            ."Si cette collection est structurellement bornee et que la desactivation est justifiee,\n"
                            .'ajouter une entree dans CollectionsArePaginatedTest::EXCLUDED avec une justification.',
                            $fqcn,
                        ),
                    );
                }
            }
        }
    }

    /**
     * Extrait le FQCN (namespace + classe) d'un fichier PHP par lecture du
     * source, sans charger le fichier.
     */
    private function extractFqcn(string $path): ?string
    {
        $source = file_get_contents($path);
        if (false === $source) {
            return null;
        }

        if (
            1 !== preg_match('/^namespace\s+([^;]+);/m', $source, $nsMatch)
            || 1 !== preg_match('/^(?:final\s+|abstract\s+|readonly\s+)*class\s+(\w+)/m', $source, $classMatch)
        ) {
            return null;
        }

        return trim($nsMatch[1]).'\\'.$classMatch[1];
    }
}
