<?php

declare(strict_types=1);

namespace App\Tests\Module\Core\Api;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Module\Core\Domain\Entity\Permission;
use App\Module\Core\Domain\Entity\Role;
use App\Module\Core\Domain\Entity\User;
use App\Module\Sites\Domain\Entity\Site;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

/**
 * Classe de base pour les tests fonctionnels API Platform du module Core.
 *
 * Mutualise :
 *  - `$alwaysBootKernel = true` : bascule le nouveau comportement API Platform 5
 *    et evite la deprecation emise a la creation du client de test.
 *  - `authenticatedClient()` : cree un client authentifie via `/login_check`
 *    (cookie BEARER HTTP-only pose par lexik_jwt_authentication).
 *  - `getEm()` : recupere l'EntityManager depuis le container courant.
 *    A rappeler apres chaque createClient() car le kernel est reboote.
 *  - `createUserWithPermission()` : cree un user non-admin jetable portant
 *    une permission specifique via un role custom. Utile pour prouver qu'un
 *    non-admin avec la permission obtient 200, et sans la permission 403.
 *
 * @internal
 */
abstract class AbstractApiTestCase extends ApiTestCase
{
    // Bascule explicite sur le nouveau comportement API Platform 5 pour
    // eviter la deprecation emise a la creation du client de test.
    protected static ?bool $alwaysBootKernel = true;

    /**
     * Recupere l'EntityManager depuis le container courant. A utiliser a
     * chaque appel : apres un createClient(), le kernel est reboote et tout
     * EM precedemment capture est invalide.
     */
    protected function getEm(): EntityManagerInterface
    {
        if (!self::$kernel) {
            self::bootKernel();
        }

        return self::getContainer()->get('doctrine')->getManager();
    }

    /**
     * Cree un client authentifie via /login_check. La configuration du projet
     * pose le JWT dans un cookie HTTP-only `BEARER` (cf. lexik_jwt_authentication.yaml)
     * et retire le token du body de reponse ; le client BrowserKit persiste
     * automatiquement le cookie pour les requetes suivantes.
     */
    protected function authenticatedClient(string $username, string $password): Client
    {
        $client   = self::createClient();
        $response = $client->request('POST', '/login_check', [
            'headers' => ['Content-Type' => 'application/json'],
            'json'    => ['username' => $username, 'password' => $password],
        ]);

        self::assertContains(
            $response->getStatusCode(),
            [200, 204],
            'Login failed for '.$username.': '.$response->getStatusCode(),
        );

        return $client;
    }

    /**
     * Cree un utilisateur non-admin portant une permission specifique via un
     * role custom jetable. A utiliser dans les tests fonctionnels qui doivent
     * prouver qu'un non-admin avec la permission requise obtient 200, et
     * sans la permission obtient 403.
     *
     * Le user et le role sont persistes avec un suffixe aleatoire pour eviter
     * les collisions inter-tests. Le password est "testpass".
     *
     * Prerequis : la permission identifiee par $permissionCode doit exister en
     * base (seeder via `app:sync-permissions`). Si elle est introuvable, le test
     * echoue immediatement avec un message explicite.
     *
     * @param string $permissionCode Le code de la permission (ex: "core.users.view")
     *
     * @return array{username: string, password: string} Les identifiants pour authenticatedClient()
     */
    protected function createUserWithPermission(string $permissionCode): array
    {
        return $this->createUserWithPermissions([$permissionCode]);
    }

    /**
     * Variante multi-permissions de {@see createUserWithPermission()} : cree un
     * utilisateur non-admin portant PLUSIEURS permissions via un unique role
     * jetable. Utile pour prouver qu'une combinaison precise de permissions
     * (sans le bypass admin) suffit a debloquer un comportement — ex. la colonne
     * SIREN de l'export, gatee par accounting.view EN PLUS de suppliers.view.
     *
     * Memes garanties que le singulier : suffixe aleatoire, password "testpass",
     * rattachement a tous les sites, echec explicite si une permission est
     * introuvable en base.
     *
     * @param list<string> $permissionCodes codes des permissions a accorder
     *
     * @return array{username: string, password: string} identifiants pour authenticatedClient()
     */
    protected function createUserWithPermissions(array $permissionCodes): array
    {
        if (!self::$kernel) {
            self::bootKernel();
        }

        $em = $this->getEm();

        $suffix   = substr(bin2hex(random_bytes(4)), 0, 8);
        $username = 'testuser_'.$suffix;
        $password = 'testpass';

        /** @var UserPasswordHasherInterface $hasher */
        $hasher = self::getContainer()->get(UserPasswordHasherInterface::class);

        $role = new Role('test_'.$suffix, 'Test Role '.$suffix, false);

        foreach ($permissionCodes as $permissionCode) {
            /** @var null|Permission $permission */
            $permission = $em->getRepository(Permission::class)->findOneBy(['code' => $permissionCode]);

            self::assertNotNull(
                $permission,
                sprintf(
                    'Permission "%s" introuvable en base. Assurez-vous que `app:sync-permissions` a ete execute.',
                    $permissionCode,
                ),
            );

            $role->addPermission($permission);
        }

        $em->persist($role);

        $user = new User();
        $user->setUsername($username);
        $user->setIsAdmin(false);
        $user->setPassword($hasher->hashPassword($user, $password));
        $user->addRbacRole($role);

        // Le helper attache le user jetable a tous les sites existants pour
        // neutraliser le filtrage par UserSiteScopedExtension : la plupart
        // des tests assume une visibilite globale sur les users cibles. Les
        // tests qui valident le comportement "sans sites" doivent creer leur
        // user a la main (pas via ce helper).
        $siteRepository = $em->getRepository(Site::class);
        if (null !== $siteRepository) {
            foreach ($siteRepository->findAll() as $site) {
                $user->addSite($site);
            }
        }

        $em->persist($user);

        $em->flush();
        $em->clear();

        return ['username' => $username, 'password' => $password];
    }

    /**
     * Skip le test courant si le module Sites est desactive dans
     * `config/modules.php` de l'environnement de test.
     *
     * Mecanisme : on cherche la permission `sites.view` en base. Si le
     * module Sites est desactive, `app:sync-permissions` aura marque cette
     * permission comme orpheline et l'aura supprimee de la table — donc
     * `findOneBy(['code' => 'sites.view'])` renvoie null.
     *
     * Quand utiliser ce helper : tests qui s'appuient sur
     * `createUserWithPermission('sites.*')`. Les tests qui utilisent
     * uniquement l'admin (qui bypass via isAdmin) n'en ont pas besoin :
     * la classe Site reste mappee Doctrine et exposee via API Platform
     * meme module desactive (mapping inconditionnel, decision assumee
     * ticket 1).
     */
    protected function skipIfSitesModuleDisabled(): void
    {
        if (!self::$kernel) {
            self::bootKernel();
        }
        $perm = $this->getEm()
            ->getRepository(Permission::class)
            ->findOneBy(['code' => 'sites.view'])
        ;
        if (null === $perm) {
            self::markTestSkipped('Module Sites desactive : permission sites.view introuvable en base.');
        }
    }
}
