<?php

declare(strict_types=1);

namespace App\Tests\Module\Catalog\Api;

use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Module\Catalog\Domain\Entity\Category;
use App\Module\Catalog\Domain\Entity\CategoryType;
use App\Module\Core\Domain\Entity\Role;
use App\Module\Core\Domain\Entity\User;
use App\Module\Sites\Domain\Entity\Site;
use App\Tests\Module\Core\Api\AbstractApiTestCase;
use DateTimeImmutable;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

/**
 * Classe de base pour les tests fonctionnels du module Catalog.
 *
 * Etend la base Core :
 *  - factories `createCategoryType()` et `createCategory()` pour seeder vite
 *    les referentiels et les entites metier dans les tests ;
 *  - helpers d'authentification specifiques au M0 : `createAdminClient()`,
 *    `createManageClient()`, `createViewClient()` et un helper persona
 *    `createPersonaClient($label)` simulant les 4 roles MALIO sans permission
 *    catalog (Bureau / Compta / Commerciale / Usine).
 *
 * Cleanup : les noms de Category sont prefixes `test_cat_` et les codes de
 * CategoryType sont prefixes `TEST_`. Le tearDown purge ces lignes, ainsi
 * que les users / roles `test_*` crees par `createUserWithPermission` et
 * `createPersonaClient`. Pas de DAMA en local, donc purge manuelle obligatoire.
 *
 * @internal
 */
abstract class AbstractCatalogApiTestCase extends AbstractApiTestCase
{
    protected const string TEST_CATEGORY_PREFIX      = 'test_cat_';
    protected const string TEST_CATEGORY_TYPE_PREFIX = 'TEST_';
    protected const string TEST_USER_PREFIX          = 'test_';
    protected const string TEST_ROLE_PREFIX          = 'test_';

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

    /**
     * Cree un CategoryType de test. Le code est prefixe `TEST_` pour le
     * cleanup, suffixe par un nonce aleatoire pour eviter les collisions
     * inter-tests.
     */
    protected function createCategoryType(?string $code = null, ?string $label = null): CategoryType
    {
        $em = $this->getEm();

        $suffix = substr(bin2hex(random_bytes(4)), 0, 8);
        $type   = new CategoryType();
        $type->setCode($code ?? self::TEST_CATEGORY_TYPE_PREFIX.strtoupper($suffix));
        $type->setLabel($label ?? 'Test Type '.$suffix);

        $em->persist($type);
        $em->flush();

        return $type;
    }

    /**
     * Cree une Category de test. Le nom est prefixe `test_cat_` pour le
     * cleanup. Si aucun type n'est fourni, un nouveau CategoryType est cree.
     * Le flag $deletedAt permet de seeder directement une categorie
     * soft-deleted (pour les tests RG-1.08 / RG-1.11).
     *
     * Multi-types (ManyToMany) : `$type` est le type principal (cree si null) ;
     * `$additionalTypes` permet d'attacher d'autres types pour les cas multi.
     *
     * @param list<CategoryType> $additionalTypes
     */
    protected function createCategory(
        ?string $name = null,
        ?CategoryType $type = null,
        ?DateTimeImmutable $deletedAt = null,
        array $additionalTypes = [],
    ): Category {
        $em = $this->getEm();

        $type ??= $this->createCategoryType();

        $suffix   = substr(bin2hex(random_bytes(4)), 0, 8);
        $category = new Category();
        $category->setName($name ?? self::TEST_CATEGORY_PREFIX.$suffix);
        // ERP-78 : code NOT NULL + unique parmi les actifs (uq_category_code).
        // Nonce aleatoire -> unicite garantie entre seeds successifs du test.
        $category->setCode('TEST_'.strtoupper($suffix));
        $category->addCategoryType($type);
        foreach ($additionalTypes as $additionalType) {
            $category->addCategoryType($additionalType);
        }
        if (null !== $deletedAt) {
            $category->setDeletedAt($deletedAt);
        }

        $em->persist($category);
        $em->flush();

        return $category;
    }

    /**
     * Client authentifie en tant qu'admin fixture (bypass via isAdmin).
     */
    protected function createAdminClient(): Client
    {
        return $this->authenticatedClient('admin', 'admin');
    }

    /**
     * Client non-admin portant la permission `catalog.categories.manage`.
     * Utilise pour prouver qu'un non-admin avec la permission obtient 200 /
     * 201 / 204 sur POST / PATCH / DELETE.
     *
     * @return array{client: Client, credentials: array{username: string, password: string}}
     */
    protected function createManageClient(): array
    {
        $credentials = $this->createUserWithPermission('catalog.categories.manage');
        $client      = $this->authenticatedClient($credentials['username'], $credentials['password']);

        return ['client' => $client, 'credentials' => $credentials];
    }

    /**
     * Client non-admin portant la permission `catalog.categories.view`.
     */
    protected function createViewClient(): Client
    {
        $credentials = $this->createUserWithPermission('catalog.categories.view');

        return $this->authenticatedClient($credentials['username'], $credentials['password']);
    }

    /**
     * Client authentifie en tant qu'un des 4 personas metier MALIO sans
     * permission catalog. Les 4 roles (Bureau / Compta / Commerciale / Usine)
     * sont seules creees a la volee dans le test, sans aucune permission
     * catalog.categories.* attachee. Le user obtient donc systematiquement
     * 403 sur tous les endpoints `/api/categories*` et `/api/category_types*`.
     *
     * Note : ces roles ne sont pas seedes dans AppFixtures (cf. HP-8 de la
     * spec M0). Les tests les materialisent juste pour prouver que porter
     * un role metier sans la permission catalog donne bien 403.
     */
    protected function createPersonaClient(string $personaLabel): Client
    {
        if (!self::$kernel) {
            self::bootKernel();
        }

        $em = $this->getEm();

        $suffix   = substr(bin2hex(random_bytes(4)), 0, 8);
        $username = self::TEST_USER_PREFIX.strtolower($personaLabel).'_'.$suffix;
        $password = 'testpass';

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

        // Role nomme d'apres le persona MALIO, ZERO permission catalog.
        $role = new Role(
            self::TEST_ROLE_PREFIX.strtolower($personaLabel).'_'.$suffix,
            $personaLabel.' (test)',
            false,
        );
        $em->persist($role);

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

        // Rattachement aux sites pour rester aligne sur createUserWithPermission.
        foreach ($em->getRepository(Site::class)->findAll() as $site) {
            $user->addSite($site);
        }

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

        return $this->authenticatedClient($username, $password);
    }

    /**
     * Purge des donnees Catalog crees par les tests.
     *
     * Strategie : purge complete des tables `category` et `category_type`
     * (aucune fixture ne les remplit au M0 — la migration cree les tables
     * vides, cf. spec-back § 1 + HP-1). On evite ainsi les pieges de
     * cleanup par prefixe quand un test valide le mauvais payload (ex:
     * name="" persiste sans matcher le LIKE) et laisse des orphelins
     * bloquant le DELETE category_type par FK violation.
     *
     * Ordre :
     *  1. Categories d'abord (FK ON DELETE RESTRICT vers category_type) ;
     *  2. CategoryTypes ensuite ;
     *  3. Users / Roles `test_*` enfin (FK created_by/updated_by sur
     *     category est ON DELETE SET NULL, mais on a deja purge category).
     */
    private function cleanupCatalogTestData(): void
    {
        $em = $this->getEm();

        $em->createQuery('DELETE FROM '.Category::class)->execute();
        $em->createQuery('DELETE FROM '.CategoryType::class)->execute();

        $em->createQuery(
            'DELETE FROM '.User::class.' u WHERE u.username LIKE :prefix'
        )->setParameter('prefix', self::TEST_USER_PREFIX.'%')->execute();

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