<?php

declare(strict_types=1);

namespace App\Tests\Module\Core\Api;

use App\Module\Core\Domain\Entity\Permission;
use App\Module\Core\Domain\Entity\Role;
use App\Module\Core\Domain\Entity\User;

/**
 * Tests fonctionnels de l'exposition API Platform de l'entite Permission.
 *
 * Strategie de donnees : on cree directement quelques instances de Permission
 * via l'EntityManager au setUp (choix le plus simple et le plus rapide, pas
 * besoin de booter la commande app:sync-permissions). Les fixtures de test
 * sont prefixees par "test." pour ne pas collisionner avec d'eventuelles
 * permissions reelles et sont nettoyees en tearDown.
 *
 * @internal
 */
final class PermissionApiTest extends AbstractApiTestCase
{
    private const TEST_CODE_PREFIX = 'test.';

    protected function setUp(): void
    {
        parent::setUp();

        // On boote le kernel une fois pour pouvoir seeder les fixtures.
        // ATTENTION : ne pas stocker l'EntityManager dans une propriete,
        // chaque createClient() dans les tests rebootera le kernel et
        // invalidera tout EM capture ici (cf. $alwaysBootKernel = true).
        self::bootKernel();
        $em = $this->getEm();

        // Nettoyage defensif au cas ou un run precedent aurait laisse des restes.
        $this->cleanupTestPermissions();

        // Donnees de test : deux permissions "core" dont une orpheline,
        // plus une permission d'un autre module pour verifier le filtre.
        $p1 = new Permission('test.core.users.view', 'View users (test)', 'core');
        $p2 = new Permission('test.core.users.manage', 'Manage users (test)', 'core');
        $p3 = new Permission('test.commercial.clients.view', 'View clients (test)', 'commercial');
        $p2->markOrphan();

        $em->persist($p1);
        $em->persist($p2);
        $em->persist($p3);
        $em->flush();
        $em->clear();
    }

    protected function tearDown(): void
    {
        $this->cleanupTestPermissions();
        parent::tearDown();
    }

    public function testGetCollectionAsAdminReturns200(): void
    {
        $client   = $this->authenticatedClient('admin', 'admin');
        $response = $client->request('GET', '/api/permissions');

        self::assertResponseIsSuccessful();
        $data = $response->toArray();
        // API Platform 4 emet du JSON-LD 1.1 avec un @context qui utilise un
        // @vocab : les cles sortent donc non prefixees (`member`, `totalItems`)
        // au lieu des anciennes `hydra:member` / `hydra:totalItems`.
        self::assertArrayHasKey('member', $data);
        self::assertGreaterThanOrEqual(3, $data['totalItems']);
    }

    /**
     * Verrouille le chemin paginE PAR DEFAUT (ERP-72) : sans `?pagination=false`,
     * `/api/permissions` doit borner la page au defaut global (10) et exposer
     * `view`. Les autres tests de filtre passent `?pagination=false` et
     * n'exercent donc plus ce contrat — on le reteste ici de maniere isolee.
     *
     * On seed 12 permissions de test pour garantir un total > 10 quelle que soit
     * la quantite de permissions reelles presentes en base.
     */
    public function testDefaultCollectionIsPaginatedToGlobalDefault(): void
    {
        $em = $this->getEm();
        for ($i = 1; $i <= 12; ++$i) {
            $em->persist(new Permission(sprintf('test.core.pagination.perm_%d', $i), sprintf('Perm pagination %d (test)', $i), 'core'));
        }
        $em->flush();
        $em->clear();

        $client   = $this->authenticatedClient('admin', 'admin');
        $response = $client->request('GET', '/api/permissions');

        self::assertResponseIsSuccessful();
        $data = $response->toArray();

        // La page par defaut ne doit jamais depasser le maximum global (10).
        self::assertLessThanOrEqual(10, count($data['member']), 'La page par defaut doit etre bornee a 10 items.');
        // Avec >= 12 permissions de test (+ reelles), le total depasse une page.
        self::assertGreaterThan(10, $data['totalItems']);
        // `view` n'est present que lorsque la collection est reellement paginee.
        self::assertArrayHasKey('view', $data, 'La collection doit exposer view quand totalItems > itemsPerPage.');
    }

    public function testCollectionFilterByModule(): void
    {
        $client   = $this->authenticatedClient('admin', 'admin');
        $response = $client->request('GET', '/api/permissions', [
            'query' => ['module' => 'core', 'pagination' => 'false'],
        ]);

        self::assertResponseIsSuccessful();
        $data = $response->toArray();
        foreach ($data['member'] as $item) {
            self::assertSame('core', $item['module']);
        }
        // Doit contenir au moins nos deux permissions core de test.
        $codes = array_column($data['member'], 'code');
        self::assertContains('test.core.users.view', $codes);
        self::assertContains('test.core.users.manage', $codes);
        self::assertNotContains('test.commercial.clients.view', $codes);
    }

    public function testCollectionFilterByOrphanTrue(): void
    {
        $client   = $this->authenticatedClient('admin', 'admin');
        $response = $client->request('GET', '/api/permissions', [
            'query' => ['orphan' => 'true', 'pagination' => 'false'],
        ]);

        self::assertResponseIsSuccessful();
        $data = $response->toArray();
        foreach ($data['member'] as $item) {
            self::assertTrue($item['orphan']);
        }
        $codes = array_column($data['member'], 'code');
        // La permission marquee orpheline dans setUp() doit remonter...
        self::assertContains('test.core.users.manage', $codes);
        // ...et celles non orphelines doivent etre exclues.
        self::assertNotContains('test.core.users.view', $codes);
        self::assertNotContains('test.commercial.clients.view', $codes);
    }

    public function testCollectionFilterByOrphanFalse(): void
    {
        $client   = $this->authenticatedClient('admin', 'admin');
        $response = $client->request('GET', '/api/permissions', [
            'query' => ['orphan' => 'false', 'pagination' => 'false'],
        ]);

        self::assertResponseIsSuccessful();
        $data = $response->toArray();
        foreach ($data['member'] as $item) {
            self::assertFalse($item['orphan']);
        }
        $codes = array_column($data['member'], 'code');
        self::assertContains('test.core.users.view', $codes);
        self::assertNotContains('test.core.users.manage', $codes);
    }

    public function testGetItemAsAdminReturnsAllReadFields(): void
    {
        /** @var null|Permission $permission */
        $permission = $this->getEm()->getRepository(Permission::class)
            ->findOneBy(['code' => 'test.core.users.view'])
        ;
        self::assertNotNull($permission);

        $client   = $this->authenticatedClient('admin', 'admin');
        $response = $client->request('GET', '/api/permissions/'.$permission->getId());

        self::assertResponseIsSuccessful();
        $data = $response->toArray();
        self::assertSame($permission->getId(), $data['id']);
        self::assertSame('test.core.users.view', $data['code']);
        self::assertSame('View users (test)', $data['label']);
        self::assertSame('core', $data['module']);
        self::assertFalse($data['orphan']);
    }

    public function testPostIsMethodNotAllowed(): void
    {
        $client = $this->authenticatedClient('admin', 'admin');
        $client->request('POST', '/api/permissions', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => ['code' => 'test.foo.bar.baz', 'label' => 'Foo', 'module' => 'foo'],
        ]);

        self::assertResponseStatusCodeSame(405);
    }

    public function testUnauthenticatedReturns401(): void
    {
        $client = self::createClient();
        $client->request('GET', '/api/permissions');

        self::assertResponseStatusCodeSame(401);
    }

    public function testStandardUserWithoutPermissionIsForbiddenOnCollection(): void
    {
        // Le catalogue de permissions est protege par `core.permissions.view` :
        // un user authentifie sans cette permission (ni flag admin) doit
        // recevoir un 403. Alice n'a que le role systeme "user" (zero
        // permission attachee) — elle est le cobaye ideal pour ce test.
        $client = $this->authenticatedClient('alice', 'alice');
        $client->request('GET', '/api/permissions');

        self::assertResponseStatusCodeSame(403);
    }

    public function testStandardUserWithoutPermissionIsForbiddenOnItem(): void
    {
        $permission = $this->getEm()->getRepository(Permission::class)
            ->findOneBy(['code' => 'test.core.users.view'])
        ;
        self::assertNotNull($permission);

        $client = $this->authenticatedClient('alice', 'alice');
        $client->request('GET', '/api/permissions/'.$permission->getId());

        self::assertResponseStatusCodeSame(403);
    }

    public function testNonAdminWithPermissionViewCanListPermissions(): void
    {
        // Chemin positif : un user non-admin qui porte la permission
        // `core.permissions.view` (via un role dedie) doit recevoir 200 sur
        // le catalogue. Couvre l'habilitation sans bypass isAdmin.
        $creds  = $this->createUserWithPermission('core.permissions.view');
        $client = $this->authenticatedClient($creds['username'], $creds['password']);
        $client->request('GET', '/api/permissions');

        self::assertResponseIsSuccessful();
    }

    public function testNonAdminWithUsersManageCanListPermissions(): void
    {
        // Bypass pragmatique : les gestionnaires d'users ont besoin du
        // catalogue pour les drawers RBAC. Couvre la branche OR de la
        // security expression `core.users.manage`.
        $creds  = $this->createUserWithPermission('core.users.manage');
        $client = $this->authenticatedClient($creds['username'], $creds['password']);
        $client->request('GET', '/api/permissions');

        self::assertResponseIsSuccessful();
    }

    public function testNonAdminWithRolesManageCanListPermissions(): void
    {
        // Meme logique que ci-dessus pour les gestionnaires de roles
        // (branche OR `core.roles.manage` de la security expression).
        $creds  = $this->createUserWithPermission('core.roles.manage');
        $client = $this->authenticatedClient($creds['username'], $creds['password']);
        $client->request('GET', '/api/permissions');

        self::assertResponseIsSuccessful();
    }

    public function testNonAdminWithPermissionViewCanGetItem(): void
    {
        // Miroir item de testNonAdminWithPermissionViewCanListPermissions :
        // la security expression OR est repliquee sur `new Get(...)` et doit
        // donc aussi s'appliquer ici.
        $permission = $this->getEm()->getRepository(Permission::class)
            ->findOneBy(['code' => 'test.core.users.view'])
        ;
        self::assertNotNull($permission);

        $creds  = $this->createUserWithPermission('core.permissions.view');
        $client = $this->authenticatedClient($creds['username'], $creds['password']);
        $client->request('GET', '/api/permissions/'.$permission->getId());

        self::assertResponseIsSuccessful();
    }

    public function testNonAdminWithUsersManageCanGetItem(): void
    {
        $permission = $this->getEm()->getRepository(Permission::class)
            ->findOneBy(['code' => 'test.core.users.view'])
        ;
        self::assertNotNull($permission);

        $creds  = $this->createUserWithPermission('core.users.manage');
        $client = $this->authenticatedClient($creds['username'], $creds['password']);
        $client->request('GET', '/api/permissions/'.$permission->getId());

        self::assertResponseIsSuccessful();
    }

    public function testNonAdminWithRolesManageCanGetItem(): void
    {
        $permission = $this->getEm()->getRepository(Permission::class)
            ->findOneBy(['code' => 'test.core.users.view'])
        ;
        self::assertNotNull($permission);

        $creds  = $this->createUserWithPermission('core.roles.manage');
        $client = $this->authenticatedClient($creds['username'], $creds['password']);
        $client->request('GET', '/api/permissions/'.$permission->getId());

        self::assertResponseIsSuccessful();
    }

    private function cleanupTestPermissions(): void
    {
        $em = $this->getEm();

        // Purge des users et roles jetables crees par createUserWithPermission().
        $em->createQuery(
            'DELETE FROM '.User::class.' u WHERE u.username LIKE :prefix'
        )->setParameter('prefix', 'testuser_%')->execute();

        $em->createQuery(
            'DELETE FROM '.Role::class.' r WHERE r.code LIKE :prefix'
        )->setParameter('prefix', 'test_%')->execute();

        $em->createQuery(
            'DELETE FROM '.Permission::class.' p WHERE p.code LIKE :prefix'
        )->setParameter('prefix', self::TEST_CODE_PREFIX.'%')->execute();
    }
}
