<?php

declare(strict_types=1);

namespace App\Tests\Module\Commercial\Api;

use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Module\Catalog\Domain\Entity\Category;
use App\Module\Catalog\Domain\Entity\CategoryType;
use App\Module\Commercial\Domain\Entity\Client as ClientEntity;
use App\Module\Core\Domain\Entity\Role;
use App\Module\Core\Domain\Entity\User;
use App\Tests\Module\Core\Api\AbstractApiTestCase;
use DateTimeImmutable;

/**
 * Base des tests fonctionnels du module Commercial (M1 — repertoire clients).
 *
 * Etend la base Core : ajoute des factories pour seeder vite des categories
 * codees (DISTRIBUTEUR / COURTIER / SECTEUR...) sous le type unique CLIENT et
 * des clients, plus un helper d'authentification admin.
 *
 * Refonte taxonomie ERP-78 : il n'y a plus qu'un type CLIENT ; le code metier
 * vit desormais sur la Category. `createCategory($code)` est un fetch-or-create
 * PAR CODE (idempotent) sous CLIENT — deux clients d'un meme test partagent ainsi
 * la categorie de meme code sans violer l'index unique partiel uq_category_code.
 *
 * Cleanup : tearDown purge clients, categories `test_cli_cat_*` et users/roles
 * `test_*`. Le type CLIENT est fetch-or-create (idempotent) et laisse en place.
 * Pas de DAMA en local -> purge manuelle obligatoire.
 *
 * @internal
 */
abstract class AbstractCommercialApiTestCase extends AbstractApiTestCase
{
    protected const string TEST_CATEGORY_PREFIX = 'test_cli_cat_';

    /**
     * Codes pilotant les RG (RG-1.03 distributor/broker, RG-1.29 adresse) : ils
     * doivent matcher exactement, donc createCategory() les fetch-or-create par
     * code. Les autres codes sont traites comme de simples libelles generiques et
     * produisent une categorie a code UNIQUE (cf. createCategory).
     */
    private const array RG_EXACT_CODES = ['DISTRIBUTEUR', 'COURTIER'];

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

    protected function createAdminClient(): Client
    {
        return $this->authenticatedClient('admin', 'admin');
    }

    /**
     * Recupere (ou cree) le type unique CLIENT (refonte ERP-78). Idempotent : la
     * contrainte d'unicite sur category_type.code interdit les doublons.
     */
    protected function clientCategoryType(): CategoryType
    {
        $em       = $this->getEm();
        $existing = $em->getRepository(CategoryType::class)->findOneBy(['code' => 'CLIENT']);
        if (null !== $existing) {
            return $existing;
        }

        $type = new CategoryType();
        $type->setCode('CLIENT');
        $type->setLabel('Client');
        $em->persist($type);
        $em->flush();

        return $type;
    }

    /**
     * Cree une Category de test sous le type unique CLIENT (ERP-78).
     *
     * - Code RG (DISTRIBUTEUR / COURTIER) : fetch-or-create par code EXACT — le
     *   code doit matcher la regle de gestion, et l'appel repete dans un test
     *   renvoie la meme categorie (pas de violation de uq_category_code).
     * - Autre code (SECTEUR, AUTRE, ...) : simple libelle generique -> categorie
     *   a code UNIQUE (suffixe aleatoire). Garantit que deux categories
     *   « generiques » d'un meme test sont DISTINCTES (ex: detection de
     *   changement de categorie dans les tests RBAC).
     */
    protected function createCategory(string $code = 'SECTEUR'): Category
    {
        $em = $this->getEm();

        if (in_array($code, self::RG_EXACT_CODES, true)) {
            $existing = $em->getRepository(Category::class)->findOneBy(['code' => $code, 'deletedAt' => null]);
            if (null !== $existing) {
                return $existing;
            }

            $effectiveCode = $code;
            $name          = self::TEST_CATEGORY_PREFIX.strtolower($code);
        } else {
            $suffix        = substr(bin2hex(random_bytes(4)), 0, 8);
            $effectiveCode = strtoupper($code).'_'.strtoupper($suffix);
            $name          = self::TEST_CATEGORY_PREFIX.strtolower($code).'_'.$suffix;
        }

        $category = new Category();
        $category->setName($name);
        $category->setCode($effectiveCode);
        $category->addCategoryType($this->clientCategoryType());
        $em->persist($category);
        $em->flush();

        return $category;
    }

    /**
     * Seede directement un Client en base (sans passer par l'API), pour les
     * tests de liste / archivage. Le client porte une categorie du code donne
     * (defaut SECTEUR — categorie generique non interdite sur adresse).
     */
    protected function seedClient(string $companyName, bool $isArchived = false, string $categoryCode = 'SECTEUR'): ClientEntity
    {
        $em     = $this->getEm();
        $client = new ClientEntity();
        // Stocke en MAJUSCULES pour refleter l'etat normalise (RG-1.18) qu'aurait
        // produit le ClientProcessor via l'API.
        $client->setCompanyName(mb_strtoupper($companyName, 'UTF-8'));
        $client->addCategory($this->createCategory($categoryCode));
        $client->setIsArchived($isArchived);
        if ($isArchived) {
            $client->setArchivedAt(new DateTimeImmutable());
        }
        $em->persist($client);
        $em->flush();

        return $client;
    }

    /**
     * Indexe les violations d'un corps de reponse 422 par propertyPath. Permet
     * d'asserter qu'un 422 porte bien sur le champ attendu (et n'est pas un 422
     * orthogonal) : un test qui se contente du code 422 passerait meme si la RG
     * visee etait cassee pour une autre raison. Mutualise ici (et non dans la
     * sous-classe Supplier) pour etre accessible a tous les tests Commercial.
     *
     * @param array<string, mixed> $body corps decode de la reponse (toArray(false))
     *
     * @return array<string, string> propertyPath => message
     */
    protected function violationsByPath(array $body): array
    {
        $byPath = [];
        foreach ($body['violations'] ?? [] as $v) {
            $byPath[$v['propertyPath']] = $v['message'];
        }

        return $byPath;
    }

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

        // Clients d'abord (la jointure client_category est purgee par
        // ON DELETE CASCADE ; les auto-references distributor/broker sont
        // ON DELETE SET NULL).
        $em->createQuery('DELETE FROM '.ClientEntity::class)->execute();

        // Categories de test ensuite (FK client_category deja purgee).
        $em->createQuery(
            'DELETE FROM '.Category::class.' c WHERE c.name LIKE :prefix',
        )->setParameter('prefix', self::TEST_CATEGORY_PREFIX.'%')->execute();

        // Users / roles jetables.
        $em->createQuery(
            'DELETE FROM '.User::class.' u WHERE u.username LIKE :prefix',
        )->setParameter('prefix', 'test_%')->execute();

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