<?php

declare(strict_types=1);

namespace App\Module\Commercial\Infrastructure\DataFixtures;

use App\Module\Catalog\Infrastructure\DataFixtures\CategoryFixtures;
use App\Module\Commercial\Application\Service\SupplierFieldNormalizer;
use App\Module\Commercial\Domain\Entity\Bank;
use App\Module\Commercial\Domain\Entity\PaymentDelay;
use App\Module\Commercial\Domain\Entity\PaymentType;
use App\Module\Commercial\Domain\Entity\Supplier;
use App\Module\Commercial\Domain\Entity\SupplierAddress;
use App\Module\Commercial\Domain\Entity\SupplierContact;
use App\Module\Commercial\Domain\Entity\SupplierRib;
use App\Module\Commercial\Domain\Entity\TvaMode;
use App\Module\Sites\Infrastructure\DataFixtures\SitesFixtures;
use App\Shared\Domain\Contract\CategoryInterface;
use App\Shared\Domain\Contract\SiteInterface;
use App\Shared\Domain\Contract\SiteProviderInterface;
use DateTimeImmutable;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use RuntimeException;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Fixtures dev/test du module Commercial : ~13 fournisseurs de demonstration
 * couvrant l'ensemble des cas metier RG-2.xx du repertoire fournisseurs (M2),
 * jumelles des fixtures Client (ERP-68). Theme metier : negoce / recyclage de
 * metaux (d'ou les champs `bennes` et `triageProvider` sur les adresses).
 *
 * Cas pivots couverts (criteres d'acceptation ERP-112) :
 *  - reglement VIREMENT avec banque renseignee (RG-2.07) ;
 *  - reglement LCR avec 1 puis 2 RIB (RG-2.08) ; CHEQUE et NON_SOUMISE sans RIB ;
 *  - adresses multi-types PROSPECT / DEPART / RENDU (RG-2.09) et multi-sites
 *    (86 / 17 / 82, RG-2.06) ; bennes + prestataire de triage ;
 *  - 1 a 3 contacts dont un avec telephone secondaire et un nomme par le seul
 *    nom (RG-2.04) ;
 *  - 2 fournisseurs archives (isArchived + archivedAt) pour l'exclusion de la
 *    liste (RG-2.17) ;
 *  - mono et multi-categories de type FOURNISSEUR (RG-2.10) ;
 *  - onglet Information complet (dont volumeForecast, specifique fournisseur).
 *
 * Resolution inter-modules conforme a la regle n°1 (pas d'import direct) :
 *  - categories resolues via le contrat Shared CategoryInterface
 *    (resolve_target_entities -> Category) ;
 *  - sites resolus via le contrat Shared SiteProviderInterface.
 *
 * Normalisation : les valeurs sont fournies BRUTES (casse libre, telephones
 * formates) et normalisees par SupplierFieldNormalizer avant persist, exactement
 * comme le ferait le SupplierProcessor via l'API (companyName UPPERCASE,
 * first/last Capitalize, telephones chiffres seuls, emails lowercase).
 *
 * Coherence gating comptable (RG-2.16) : les scalaires comptables (siren,
 * tvaMode, paymentType, bank...) et les RIB ne sont visibles qu'avec
 * accounting.view. Les donnees sont posees pour que les roles SANS cette
 * permission (ex. Commerciale) ne voient pas de compta — support des tests
 * ERP-92 et du golden path front.
 *
 * Idempotence : lookup par companyName normalise (coherent avec l'index unique
 * partiel uq_supplier_company_name_active). Un fournisseur deja present n'est pas
 * reconstruit (ses sous-collections ne sont pas redupliquees). Rejouable sans
 * doublon meme si le purger Doctrine est desactive.
 *
 * Audit / Blamable : persist hors contexte HTTP -> created_by / updated_by
 * restent null (« Systeme » cote front), c'est attendu. Les donnees respectent
 * les CHECK BDD (chk_supplier_contact_name : firstName OU lastName ;
 * chk_supplier_address_type : PROSPECT | DEPART | RENDU) ET la coherence des
 * validators d'entite (RG-2.07/2.08 : VIREMENT => banque, LCR => >= 1 RIB).
 *
 * Depend de CategoryFixtures (categories FOURNISSEUR), SitesFixtures (sites) et
 * CommercialReferentialFixtures (referentiels comptables — REUTILISES de M1,
 * aucune nouvelle table).
 *
 * Portee : DONNEES DE DEMONSTRATION (dev uniquement). En environnement `test`,
 * la fixture ne charge rien : les tests seedent et nettoient leurs propres
 * fournisseurs et comptent sur une table `supplier` vierge — y injecter 13
 * fournisseurs de demo casserait les comptages de liste et les cleanups. Meme
 * garde-fou que ClientFixtures / CategoryFixtures.
 */
class SupplierFixtures extends Fixture implements DependentFixtureInterface
{
    /**
     * Type de categorie exige pour un fournisseur et ses adresses (RG-2.10).
     * Miroir de Supplier::REQUIRED_CATEGORY_TYPE_CODE (non importable — regle n°1).
     */
    private const string SUPPLIER_CATEGORY_TYPE_CODE = 'FOURNISSEUR';

    /** Cache des categories resolues par nom (evite des requetes repetees). */
    private array $categoryCache = [];

    /** Cache des sites resolus par nom. */
    private array $siteCache = [];

    /** ObjectManager courant, capture en debut de load (resolution categories). */
    private ObjectManager $manager;

    public function __construct(
        private readonly SupplierFieldNormalizer $normalizer,
        private readonly SiteProviderInterface $siteProvider,
        #[Autowire('%kernel.environment%')]
        private readonly string $environment,
    ) {}

    /**
     * @return array<int, class-string>
     */
    public function getDependencies(): array
    {
        return [
            CategoryFixtures::class,
            SitesFixtures::class,
            CommercialReferentialFixtures::class,
        ];
    }

    public function load(ObjectManager $manager): void
    {
        // Donnees de demo : dev uniquement. En test, on laisse la table vierge.
        if ('test' === $this->environment) {
            return;
        }

        $this->manager = $manager;

        // === Fournisseur basique — VIREMENT + banque (RG-2.07), compta complete ===
        [$negoce, $isNew] = $this->ensureSupplier($manager, 'Négoce Métaux Atlantique', ['Négociant']);
        if ($isNew) {
            $negoce->setSiren('841611054');
            $negoce->setAccountNumber('F0001');
            $negoce->setTvaMode($this->tvaMode($manager, 'FRANCE_VENTES'));
            $negoce->setNTva('FR12841611054');
            $negoce->setPaymentDelay($this->paymentDelay($manager, 'J30'));
            $negoce->setPaymentType($this->paymentType($manager, 'VIREMENT'));
            $negoce->setBank($this->bank($manager, 'SG'));
            $this->addContact($negoce, 'Jean', 'Dubois', 'Responsable achats', '05 49 00 00 01', null, 'jean.dubois@negoce-metaux.fr');
            $this->addAddress($negoce, 'DEPART', ['Chatellerault'], '86100', 'Châtellerault', '12 rue de la Ferraille', bennes: 4, triageProvider: true, categoryNames: ['Négociant']);
        }

        // === LCR avec 1 RIB (RG-2.08) + 2 contacts ===
        [$coop, $isNew] = $this->ensureSupplier($manager, 'Coopérative Agricole du Sud-Ouest', ['Coopérative']);
        if ($isNew) {
            $coop->setSiren('775680459');
            $coop->setTvaMode($this->tvaMode($manager, 'FRANCE_VENTES'));
            $coop->setPaymentDelay($this->paymentDelay($manager, 'J15'));
            $coop->setPaymentType($this->paymentType($manager, 'LCR'));
            $this->addContact($coop, 'Sophie', 'Marchand', 'Directrice', '05 56 10 20 30', '06 11 22 33 44', 'sophie.marchand@coop-so.fr', 0);
            $this->addContact($coop, 'Marc', 'Girard', 'Acheteur', '05 56 10 20 31', null, 'marc.girard@coop-so.fr', 1);
            $this->addAddress($coop, 'RENDU', ['Pommevic'], '82400', 'Pommevic', '8 route des Cooperateurs', bennes: 2);
            $this->addRib($coop, 'Compte principal', 'BNPAFRPPXXX', 'FR1420041010050500013M02606', 0);
        }

        // === Prospect seul (adresse PROSPECT), compta minimale ===
        [$producteur, $isNew] = $this->ensureSupplier($manager, 'Producteur Bio Charente', ['Producteur']);
        if ($isNew) {
            $this->addContact($producteur, 'Claire', 'Moreau', 'Gérante', '05 49 21 22 23', null, 'claire.moreau@bio-charente.fr');
            $this->addAddress($producteur, 'PROSPECT', ['Saint-Jean'], '17400', 'Fontenet', '1 chemin des Producteurs');
        }

        // === Multi-categories M2M + LCR avec 2 RIB + 3 contacts ===
        [$grossiste, $isNew] = $this->ensureSupplier($manager, 'Grossiste Multi-Métaux', ['Grossiste', 'Négociant']);
        if ($isNew) {
            $grossiste->setSiren('552081317');
            $grossiste->setAccountNumber('F0004');
            $grossiste->setTvaMode($this->tvaMode($manager, 'FRANCE_VENTES'));
            $grossiste->setNTva('FR45552081317');
            $grossiste->setPaymentDelay($this->paymentDelay($manager, 'J30'));
            $grossiste->setPaymentType($this->paymentType($manager, 'LCR'));
            $this->addContact($grossiste, 'Thomas', 'Petit', 'Directeur des achats', '05 56 31 32 33', '06 01 02 03 04', 'thomas.petit@grossiste-mm.fr', 0);
            $this->addContact($grossiste, 'Julie', 'Roux', 'Assistante commerciale', '05 56 31 32 34', null, 'julie.roux@grossiste-mm.fr', 1);
            $this->addContact($grossiste, 'Hélène', 'Faure', 'Logistique', '05 56 31 32 35', null, 'helene.faure@grossiste-mm.fr', 2);
            $this->addAddress($grossiste, 'DEPART', ['Chatellerault'], '86100', 'Châtellerault', '20 zone des Activités', streetComplement: 'Bâtiment C', bennes: 6, triageProvider: true, categoryNames: ['Grossiste', 'Négociant']);
            $this->addRib($grossiste, 'Compte principal', 'BNPAFRPPXXX', 'FR7630006000011234567890189', 0);
            $this->addRib($grossiste, 'Compte secondaire', 'SOGEFRPPXXX', 'FR7630001007941234567890185', 1);
        }

        // === VIREMENT + banque, TVA intracom (importateur), multi-sites sur l'adresse ===
        [$import, $isNew] = $this->ensureSupplier($manager, 'Import Recyclage International', ['Importateur']);
        if ($isNew) {
            $import->setSiren('409512012');
            $import->setTvaMode($this->tvaMode($manager, 'INTRACOM_VENTES'));
            $import->setNTva('FR90409512012');
            $import->setPaymentDelay($this->paymentDelay($manager, 'J30'));
            $import->setPaymentType($this->paymentType($manager, 'VIREMENT'));
            $import->setBank($this->bank($manager, 'CIC'));
            $this->addContact($import, 'Paul', 'Garnier', 'Import manager', '05 56 44 55 66', null, 'paul.garnier@import-recyclage.fr', 0);
            $this->addContact($import, null, 'Bernard', 'Douanes', '05 56 44 55 67', null, 'douanes@import-recyclage.fr', 1);
            $this->addAddress($import, 'RENDU', ['Pommevic', 'Saint-Jean'], '82400', 'Pommevic', '3 quai des Importateurs', bennes: 8);
        }

        // === Multi-adresses PROSPECT / DEPART / RENDU (RG-2.09) + VIREMENT/banque ===
        [$ferrailleur, $isNew] = $this->ensureSupplier($manager, 'Ferrailleur Grand Ouest', ['Négociant']);
        if ($isNew) {
            $ferrailleur->setSiren('732829320');
            $ferrailleur->setTvaMode($this->tvaMode($manager, 'FRANCE_VENTES'));
            $ferrailleur->setPaymentDelay($this->paymentDelay($manager, 'A_RECEPTION'));
            $ferrailleur->setPaymentType($this->paymentType($manager, 'VIREMENT'));
            $ferrailleur->setBank($this->bank($manager, 'CA'));
            $this->addContact($ferrailleur, 'Olivier', 'Renard', 'Responsable site', '05 49 61 62 63', null, 'olivier.renard@ferrailleur-go.fr', 0);
            $this->addContact($ferrailleur, 'Nadia', 'Benali', 'Pesée', '05 49 61 62 64', null, 'nadia.benali@ferrailleur-go.fr', 1);
            // Prospect (site en cours de demarchage).
            $this->addAddress($ferrailleur, 'PROSPECT', ['Chatellerault'], '86100', 'Châtellerault', '5 avenue de la Prospection', position: 0);
            // Depart (collecte) multi-sites avec bennes + triage.
            $this->addAddress($ferrailleur, 'DEPART', ['Saint-Jean', 'Pommevic'], '17400', 'Fontenet', '4 rue de la Collecte', bennes: 5, triageProvider: true, categoryNames: ['Négociant'], position: 1);
            // Rendu (livraison).
            $this->addAddress($ferrailleur, 'RENDU', ['Pommevic'], '82400', 'Pommevic', '7 boulevard du Rendu', bennes: 3, position: 2);
        }

        // === Onglet Information complet (dont volumeForecast) + VIREMENT/banque ===
        [$holding, $isNew] = $this->ensureSupplier($manager, 'Holding Recyclage Premium', ['Importateur']);
        if ($isNew) {
            $holding->setDescription('Holding de recyclage diversifiée, présente sur le Grand Sud-Ouest.');
            $holding->setCompetitors('Groupe Atlantique Recyclage, Sud Métaux');
            $holding->setFoundedAt(new DateTimeImmutable('2008-09-01'));
            $holding->setEmployeesCount(180);
            $holding->setRevenueAmount('24500000.00');
            $holding->setDirectorName('Antoine Lefèvre');
            $holding->setProfitAmount('1850000.00');
            $holding->setVolumeForecast(120000);
            $holding->setSiren('318471925');
            $holding->setAccountNumber('F0007');
            $holding->setTvaMode($this->tvaMode($manager, 'FRANCE_VENTES'));
            $holding->setNTva('FR33318471925');
            $holding->setPaymentDelay($this->paymentDelay($manager, 'J30'));
            $holding->setPaymentType($this->paymentType($manager, 'VIREMENT'));
            $holding->setBank($this->bank($manager, 'SG'));
            $this->addContact($holding, 'Antoine', 'Lefèvre', 'PDG', '05 56 51 52 53', null, 'antoine.lefevre@holding-recyclage.fr');
            $this->addAddress($holding, 'DEPART', ['Chatellerault'], '86100', 'Châtellerault', '1 allée des Investisseurs', bennes: 5, triageProvider: true, categoryNames: ['Importateur']);
        }

        // === Coop minimale — contact par le seul nom (RG-2.04), sans compta ===
        [$coopMin, $isNew] = $this->ensureSupplier($manager, 'Coop Métaux Réunis', ['Coopérative']);
        if ($isNew) {
            $this->addContact($coopMin, null, 'Caron', 'Président', '05 49 81 82 83', null, 'president@coop-metaux-reunis.fr');
            $this->addAddress($coopMin, 'DEPART', ['Saint-Jean'], '17400', 'Fontenet', '6 chemin du Village');
        }

        // === Reglement CHEQUE (sans banque ni RIB requis) ===
        [$petit, $isNew] = $this->ensureSupplier($manager, 'Petit Négoce Local', ['Négociant']);
        if ($isNew) {
            $petit->setTvaMode($this->tvaMode($manager, 'FRANCE_VENTES'));
            $petit->setPaymentDelay($this->paymentDelay($manager, 'A_RECEPTION'));
            $petit->setPaymentType($this->paymentType($manager, 'CHEQUE'));
            $this->addContact($petit, 'Luc', 'Martin', 'Gérant', '05 56 71 72 73', null, 'luc.martin@petit-negoce.fr');
            $this->addAddress($petit, 'RENDU', ['Chatellerault'], '86100', 'Châtellerault', '15 rue du Commerce');
        }

        // === Reglement NON_SOUMISE + adresse multi-sites avec triage ===
        [$recup, $isNew] = $this->ensureSupplier($manager, 'Récupération Métaux Express', ['Grossiste']);
        if ($isNew) {
            $recup->setSiren('490212019');
            $recup->setTvaMode($this->tvaMode($manager, 'FRANCE_VENTES'));
            $recup->setPaymentDelay($this->paymentDelay($manager, 'J15'));
            $recup->setPaymentType($this->paymentType($manager, 'NON_SOUMISE'));
            $this->addContact($recup, 'Marie', 'Lemoine', 'Responsable', '05 49 77 88 99', null, 'marie.lemoine@recup-express.fr', 0);
            $this->addContact($recup, 'Pierre', 'Durand', 'Chauffeur', '05 49 77 88 98', null, 'pierre.durand@recup-express.fr', 1);
            $this->addAddress($recup, 'DEPART', ['Saint-Jean', 'Chatellerault'], '17400', 'Fontenet', '10 zone industrielle', bennes: 7, triageProvider: true, categoryNames: ['Grossiste']);
        }

        // === Centre de tri — focus bennes/triage + multi-categories ===
        [$centre, $isNew] = $this->ensureSupplier($manager, 'Centre de Tri Sud', ['Producteur', 'Coopérative']);
        if ($isNew) {
            $centre->setPaymentDelay($this->paymentDelay($manager, 'A_RECEPTION'));
            $this->addContact($centre, 'Camille', 'Faure', 'Chef de centre', '05 56 91 92 93', null, 'camille.faure@centre-tri-sud.fr');
            $this->addAddress($centre, 'DEPART', ['Pommevic'], '82400', 'Pommevic', '2 route du Tri', bennes: 12, triageProvider: true, categoryNames: ['Producteur']);
        }

        // === Fournisseur archive #1 (RG-2.17) ===
        [$ancien, $isNew] = $this->ensureSupplier($manager, 'Ancien Fournisseur Fermé', ['Producteur'], isArchived: true);
        if ($isNew) {
            $this->addContact($ancien, null, 'Lambert', 'Ancien contact', '05 49 99 99 99', null, 'contact@ancien-fournisseur.fr');
            $this->addAddress($ancien, 'DEPART', ['Chatellerault'], '86100', 'Châtellerault', '99 rue Fermée');
        }

        // === Fournisseur archive #2 (RG-2.17) ===
        [$disparu, $isNew] = $this->ensureSupplier($manager, 'Négoce Disparu', ['Grossiste'], isArchived: true);
        if ($isNew) {
            $this->addContact($disparu, 'Gérard', 'Blanc', 'Ex-gérant', '05 56 00 00 00', null, 'gerard.blanc@negoce-disparu.fr');
            $this->addAddress($disparu, 'RENDU', ['Saint-Jean'], '17400', 'Fontenet', '0 impasse Oubliée');
        }

        $manager->flush();
    }

    /**
     * Cree un fournisseur (base normalisee + categories de type FOURNISSEUR)
     * s'il n'existe pas encore, sinon retourne l'existant. Retourne
     * [Supplier, isNew] : isNew=false bloque la reconstruction des
     * sous-collections (idempotence sans doublon).
     *
     * @param list<string> $categoryNames categories de type FOURNISSEUR (RG-2.10)
     *
     * @return array{0: Supplier, 1: bool}
     */
    private function ensureSupplier(
        ObjectManager $manager,
        string $companyName,
        array $categoryNames,
        bool $isArchived = false,
    ): array {
        $normalizedName = (string) $this->normalizer->normalizeCompanyName($companyName);

        $existing = $manager->getRepository(Supplier::class)->findOneBy(['companyName' => $normalizedName]);
        if ($existing instanceof Supplier) {
            return [$existing, false];
        }

        $supplier = new Supplier();
        $supplier->setCompanyName($normalizedName);

        foreach ($categoryNames as $categoryName) {
            $supplier->addCategory($this->category($manager, $categoryName));
        }

        if ($isArchived) {
            $supplier->setIsArchived(true);
            $supplier->setArchivedAt(new DateTimeImmutable());
        }

        $manager->persist($supplier);

        return [$supplier, true];
    }

    /**
     * Ajoute un contact normalise au fournisseur (cascade persist via
     * Supplier.contacts). Au moins firstName OU lastName est toujours fourni
     * (RG-2.04, chk_supplier_contact_name).
     */
    private function addContact(
        Supplier $supplier,
        ?string $firstName,
        ?string $lastName,
        ?string $jobTitle,
        ?string $phonePrimary,
        ?string $phoneSecondary,
        ?string $email,
        int $position = 0,
    ): void {
        $contact = new SupplierContact();
        $contact->setFirstName($this->normalizer->normalizePersonName($firstName));
        $contact->setLastName($this->normalizer->normalizePersonName($lastName));
        $contact->setJobTitle($jobTitle);
        $contact->setPhonePrimary($this->normalizer->normalizePhone($phonePrimary));
        $contact->setPhoneSecondary($this->normalizer->normalizePhone($phoneSecondary));
        $contact->setEmail($this->normalizer->normalizeEmail($email));
        $contact->setPosition($position);

        $supplier->addContact($contact);
    }

    /**
     * Ajoute une adresse au fournisseur (cascade persist via Supplier.addresses).
     * Le type d'adresse est exclusif (PROSPECT | DEPART | RENDU — RG-2.09,
     * chk_supplier_address_type) ; au moins un site est rattache (RG-2.06) ; les
     * categories d'adresse sont de type FOURNISSEUR (RG-2.10).
     *
     * @param list<string> $siteNames     au moins un site (RG-2.06)
     * @param list<string> $categoryNames categories de type FOURNISSEUR (RG-2.10)
     */
    private function addAddress(
        Supplier $supplier,
        string $addressType,
        array $siteNames,
        string $postalCode,
        string $city,
        string $street,
        ?string $streetComplement = null,
        ?int $bennes = null,
        bool $triageProvider = false,
        array $categoryNames = [],
        int $position = 0,
    ): void {
        $address = new SupplierAddress();
        $address->setAddressType($addressType);
        $address->setPostalCode($postalCode);
        $address->setCity($city);
        $address->setStreet($street);
        $address->setStreetComplement($streetComplement);
        $address->setBennes($bennes);
        $address->setTriageProvider($triageProvider);
        $address->setPosition($position);

        foreach ($siteNames as $siteName) {
            $address->addSite($this->site($siteName));
        }

        foreach ($categoryNames as $categoryName) {
            $address->addCategory($this->category($this->manager, $categoryName));
        }

        $supplier->addAddress($address);
    }

    /**
     * Ajoute un RIB au fournisseur (cascade persist via Supplier.ribs). IBAN/BIC
     * valides (Assert\Iban/Bic non rejouee sur persist direct mais donnees
     * coherentes pour le golden path / les tests).
     */
    private function addRib(Supplier $supplier, string $label, string $bic, string $iban, int $position = 0): void
    {
        $rib = new SupplierRib();
        $rib->setLabel($label);
        $rib->setBic($bic);
        $rib->setIban($iban);
        $rib->setPosition($position);

        $supplier->addRib($rib);
    }

    /**
     * Resout une categorie par son nom via le contrat Shared CategoryInterface
     * (resolve_target_entities -> Category), sans importer le module Catalog
     * (regle n°1). Mise en cache par nom.
     */
    private function category(ObjectManager $manager, string $name): CategoryInterface
    {
        if (isset($this->categoryCache[$name])) {
            return $this->categoryCache[$name];
        }

        // RG-2.10 : on garde la categorie des qu'elle PORTE le type FOURNISSEUR
        // (multi-type depuis le passage en ManyToMany). Le nom etant desormais
        // unique GLOBALEMENT parmi les actifs, le lookup par `name` renvoie au
        // plus une categorie, mais on conserve la verification du type pour
        // ecarter un homonyme qui ne porterait pas FOURNISSEUR. Le filtre type
        // est porte cote PHP (findBy ne sait pas filtrer la collection categoryTypes).
        $candidates = $manager->getRepository(CategoryInterface::class)->findBy([
            'name'      => $name,
            'deletedAt' => null,
        ]);

        foreach ($candidates as $candidate) {
            if ($candidate instanceof CategoryInterface
                && in_array(self::SUPPLIER_CATEGORY_TYPE_CODE, $candidate->getCategoryTypeCodes(), true)) {
                return $this->categoryCache[$name] = $candidate;
            }
        }

        throw new RuntimeException(sprintf(
            'Categorie FOURNISSEUR "%s" introuvable : CategoryFixtures doit tourner avant SupplierFixtures.',
            $name,
        ));
    }

    /**
     * Resout un site par son nom via le contrat Shared SiteProviderInterface,
     * sans importer le module Sites (regle n°1). Mise en cache par nom.
     */
    private function site(string $name): SiteInterface
    {
        if (isset($this->siteCache[$name])) {
            return $this->siteCache[$name];
        }

        $site = $this->siteProvider->findByName($name);

        if (!$site instanceof SiteInterface) {
            throw new RuntimeException(sprintf(
                'Site "%s" introuvable : SitesFixtures doit tourner avant SupplierFixtures.',
                $name,
            ));
        }

        return $this->siteCache[$name] = $site;
    }

    private function tvaMode(ObjectManager $manager, string $code): TvaMode
    {
        $mode = $manager->getRepository(TvaMode::class)->findOneBy(['code' => $code]);

        if (!$mode instanceof TvaMode) {
            throw new RuntimeException(sprintf(
                'TvaMode "%s" introuvable : CommercialReferentialFixtures doit tourner avant SupplierFixtures.',
                $code,
            ));
        }

        return $mode;
    }

    private function paymentDelay(ObjectManager $manager, string $code): PaymentDelay
    {
        $delay = $manager->getRepository(PaymentDelay::class)->findOneBy(['code' => $code]);

        if (!$delay instanceof PaymentDelay) {
            throw new RuntimeException(sprintf(
                'PaymentDelay "%s" introuvable : CommercialReferentialFixtures doit tourner avant SupplierFixtures.',
                $code,
            ));
        }

        return $delay;
    }

    private function paymentType(ObjectManager $manager, string $code): PaymentType
    {
        $type = $manager->getRepository(PaymentType::class)->findOneBy(['code' => $code]);

        if (!$type instanceof PaymentType) {
            throw new RuntimeException(sprintf(
                'PaymentType "%s" introuvable : CommercialReferentialFixtures doit tourner avant SupplierFixtures.',
                $code,
            ));
        }

        return $type;
    }

    private function bank(ObjectManager $manager, string $code): Bank
    {
        $bank = $manager->getRepository(Bank::class)->findOneBy(['code' => $code]);

        if (!$bank instanceof Bank) {
            throw new RuntimeException(sprintf(
                'Bank "%s" introuvable : CommercialReferentialFixtures doit tourner avant SupplierFixtures.',
                $code,
            ));
        }

        return $bank;
    }
}
