<?php

declare(strict_types=1);

namespace App\Tests\Module\Commercial\Api;

use App\Module\Catalog\Domain\Entity\Category;
use App\Module\Catalog\Domain\Entity\CategoryType;
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\Domain\Entity\Site;
use DateTimeImmutable;

/**
 * Base des tests fonctionnels du repertoire fournisseurs (M2). Jumelle de la base
 * clients (M1), elle ajoute les factories specifiques fournisseur au-dessus de
 * {@see AbstractCommercialApiTestCase} (qui apporte deja createCategory sous le
 * type CLIENT, createUserWithPermission, authenticatedClient...).
 *
 * Donnees (RETEX M1 — pas de fixtures globales pour les tests) : chaque test seede
 * ses fournisseurs en base via les helpers ci-dessous, puis le tearDown les purge.
 * Les referentiels comptables (tva_mode / payment_delay / payment_type / bank) et
 * les categories FOURNISSEUR (Negociant, Cooperative...) sont seedes par les
 * fixtures applicatives (make test-db-setup) ; on les recupere par code.
 *
 * Categories : `supplierCategory('NEGOCIANT')` fetch-or-create une categorie de
 * type FOURNISSEUR (requis par RG-2.10) — fetch-or-create par code pour rester
 * idempotent et auto-suffisant (ne depend pas du seed, que d'autres tests de la
 * suite peuvent purger). Pour fabriquer une categorie d'un AUTRE type (test de
 * rejet RG-2.10), utiliser `createCategory()` du parent, qui cree sous CLIENT.
 *
 * Cleanup : le tearDown purge les fournisseurs AVANT le parent (qui supprime les
 * categories `test_cli_cat_*`) : la jointure supplier_category est ON DELETE
 * CASCADE cote supplier mais RESTRICT cote category — le DELETE DQL sur Supplier
 * declenche le cascade BDD sur supplier_category / _contact / _address, liberant
 * les categories pour la purge du parent.
 *
 * @internal
 */
abstract class AbstractSupplierApiTestCase extends AbstractCommercialApiTestCase
{
    protected const string LD    = 'application/ld+json';
    protected const string MERGE = 'application/merge-patch+json';

    /** IBAN/BIC valides (Assert\Iban / Assert\Bic) reutilises par les seeds. */
    protected const string VALID_IBAN = 'FR1420041010050500013M02606';
    protected const string VALID_BIC  = 'BNPAFRPPXXX';
    // BIC allemand valide isolement (pays DE en positions 5-6) : sert au controle
    // croise pays BIC/IBAN (DE vs IBAN FR -> mismatch, cf. Assert\Bic ibanPropertyPath).
    protected const string FOREIGN_BIC = 'DEUTDEFFXXX';

    protected function tearDown(): void
    {
        $this->getEm()->createQuery('DELETE FROM '.Supplier::class)->execute();
        parent::tearDown();
    }

    /**
     * Fetch-or-create une categorie de type FOURNISSEUR par code (defaut
     * Negociant). Type FOURNISSEUR exige par RG-2.10 : un POST fournisseur portant
     * cette categorie passe la validation. Idempotent (lookup par code, aligne sur
     * l'index unique partiel uq_category_code) et auto-suffisant : ne depend pas du
     * seed CategoryFixtures (que d'autres tests de la suite peuvent purger). Une
     * categorie creee ici porte le prefixe de nom de test -> purgee par le parent.
     */
    protected function supplierCategory(string $code = 'NEGOCIANT'): Category
    {
        $em       = $this->getEm();
        $existing = $em->getRepository(Category::class)->findOneBy(['code' => $code, 'deletedAt' => null]);
        if (null !== $existing) {
            return $existing;
        }

        $category = new Category();
        $category->setName(self::TEST_CATEGORY_PREFIX.'fr_'.strtolower($code));
        $category->setCode($code);
        $category->addCategoryType($this->supplierCategoryType());
        $em->persist($category);
        $em->flush();

        return $category;
    }

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

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

        return $type;
    }

    /**
     * Seede directement un Supplier minimal (sans passer par l'API), pour les
     * tests de liste / archivage / serialisation. Nom stocke en MAJUSCULES pour
     * refleter l'etat normalise (RG-2.12) qu'aurait produit le SupplierProcessor.
     * Porte une categorie FOURNISSEUR (defaut Negociant).
     */
    protected function seedSupplier(string $companyName, bool $isArchived = false, string $categoryCode = 'NEGOCIANT'): Supplier
    {
        $em       = $this->getEm();
        $supplier = new Supplier();
        $supplier->setCompanyName(mb_strtoupper($companyName, 'UTF-8'));
        $supplier->addCategory($this->supplierCategory($categoryCode));
        $supplier->setIsArchived($isArchived);
        if ($isArchived) {
            $supplier->setArchivedAt(new DateTimeImmutable());
        }
        $em->persist($supplier);
        $em->flush();

        return $supplier;
    }

    /**
     * Seede un fournisseur COMPLET (sans passer par l'API — validations
     * applicatives non rejouees mais CHECK BDD respectes) : onglet Information
     * rempli, bloc comptable non nul (SIREN + refs), >= 1 RIB, >= 1 adresse
     * multi-sites (>= 2 sites, triageProvider=true) avec >= 1 categorie
     * FOURNISSEUR, >= 1 contact, >= 1 categorie sur le fournisseur. Sert de socle
     * au contrat de serialisation et a la DoD (§ 4.0.bis).
     *
     * @param string $paymentTypeCode code du type de reglement a poser (defaut LCR,
     *                                coherent avec le RIB seede ; RG-2.08)
     */
    protected function seedCompleteSupplier(string $companyName, string $paymentTypeCode = 'LCR'): Supplier
    {
        $em = $this->getEm();

        // Nom unique parmi les actifs (index partiel uq_supplier_company_name_active).
        $suffix = substr(bin2hex(random_bytes(3)), 0, 6);

        $supplier = new Supplier();
        $supplier->setCompanyName(mb_strtoupper($companyName.' '.$suffix, 'UTF-8'));
        $supplier->addCategory($this->supplierCategory('NEGOCIANT'));

        // Onglet Information complet : donnees de reference pour les tests de
        // lecture / serialisation / comptabilite (l'Information est facultative).
        $supplier->setDescription('Fournisseur de test complet.');
        $supplier->setCompetitors('Concurrent A, Concurrent B');
        $supplier->setFoundedAt(new DateTimeImmutable('2008-04-01'));
        $supplier->setEmployeesCount(42);
        $supplier->setRevenueAmount('1500000.00');
        $supplier->setDirectorName('Jean Dupont');
        $supplier->setProfitAmount('120000.00');
        $supplier->setVolumeForecast(8000);

        // Bloc comptable non nul (gating par omission cote Commerciale).
        $supplier->setSiren('123456789');
        $supplier->setAccountNumber('F0001');
        $supplier->setNTva('FR00123456789');
        $supplier->setTvaMode($this->tvaMode('FRANCE_VENTES'));
        $supplier->setPaymentDelay($this->paymentDelay('J30'));
        $supplier->setPaymentType($this->paymentType($paymentTypeCode));
        if ('VIREMENT' === $paymentTypeCode) {
            $supplier->setBank($this->bank('SG'));
        }
        $em->persist($supplier);

        // >= 2 sites fixtures pour une adresse multi-sites (RG-2.06).
        $sites = $em->getRepository(Site::class)->findBy([], null, 2);
        self::assertGreaterThanOrEqual(2, count($sites), 'Au moins 2 sites fixtures requis (SitesFixtures).');

        $contact = new SupplierContact();
        $contact->setSupplier($supplier);
        $contact->setFirstName('Marie');
        $contact->setLastName('Martin');
        $contact->setJobTitle('Responsable achats');
        $contact->setPhonePrimary('0612345678');
        $contact->setEmail('marie.martin@seed.test');
        $supplier->addContact($contact);
        $em->persist($contact);

        $address = new SupplierAddress();
        $address->setSupplier($supplier);
        $address->setAddressType('DEPART');
        $address->setPostalCode('86000');
        $address->setCity('Poitiers');
        $address->setStreet('12 rue des Acacias');
        $address->setBennes(3);
        // triageProvider=true : prouve qu'un booleen `true` est bien serialise
        // (piege n°3 du M1 — la cle etait droppee).
        $address->setTriageProvider(true);
        foreach ($sites as $site) {
            $address->addSite($site);
        }
        $address->addCategory($this->supplierCategory('NEGOCIANT'));
        $address->addContact($contact);
        $supplier->addAddress($address);
        $em->persist($address);

        $rib = new SupplierRib();
        $rib->setSupplier($supplier);
        $rib->setLabel('Compte principal');
        $rib->setBic(self::VALID_BIC);
        $rib->setIban(self::VALID_IBAN);
        $supplier->addRib($rib);
        $em->persist($rib);

        $em->flush();

        return $supplier;
    }

    /**
     * Ajoute un contact a un fournisseur deja persiste (seed direct).
     */
    protected function addContact(
        Supplier $supplier,
        ?string $firstName = 'Marie',
        ?string $lastName = 'Martin',
        ?string $phonePrimary = null,
        ?string $email = null,
        int $position = 0,
    ): SupplierContact {
        $contact = new SupplierContact();
        $contact->setSupplier($supplier);
        $contact->setFirstName($firstName);
        $contact->setLastName($lastName);
        $contact->setPhonePrimary($phonePrimary);
        $contact->setEmail($email);
        $contact->setPosition($position);
        $supplier->addContact($contact);
        $this->getEm()->persist($contact);
        $this->getEm()->flush();

        return $contact;
    }

    /**
     * Ajoute un RIB a un fournisseur deja persiste (seed direct).
     */
    protected function addRib(Supplier $supplier, string $label = 'Compte principal'): SupplierRib
    {
        $rib = new SupplierRib();
        $rib->setSupplier($supplier);
        $rib->setLabel($label);
        $rib->setBic(self::VALID_BIC);
        $rib->setIban(self::VALID_IBAN);
        $supplier->addRib($rib);
        $this->getEm()->persist($rib);
        $this->getEm()->flush();

        return $rib;
    }

    /**
     * Payload minimal valide de l'onglet principal (companyName + 1 categorie
     * FOURNISSEUR). Si $categoryId est null, la categorie Negociant seedee est
     * utilisee.
     *
     * @return array<string, mixed>
     */
    protected function validMainPayload(string $companyName, ?int $categoryId = null): array
    {
        $categoryId ??= $this->supplierCategory('NEGOCIANT')->getId();

        return [
            'companyName' => $companyName,
            'categories'  => ['/api/categories/'.$categoryId],
        ];
    }

    protected function paymentType(string $code): PaymentType
    {
        return $this->referential(PaymentType::class, $code);
    }

    protected function paymentDelay(string $code): PaymentDelay
    {
        return $this->referential(PaymentDelay::class, $code);
    }

    protected function tvaMode(string $code): TvaMode
    {
        return $this->referential(TvaMode::class, $code);
    }

    protected function bank(string $code): Bank
    {
        return $this->referential(Bank::class, $code);
    }

    /**
     * Recupere un referentiel comptable seede (CommercialReferentialFixtures) par
     * code. Echoue explicitement si absent (fixtures non chargees).
     *
     * @template T of object
     *
     * @param class-string<T> $entityClass
     *
     * @return T
     */
    private function referential(string $entityClass, string $code): object
    {
        $entity = $this->getEm()->getRepository($entityClass)->findOneBy(['code' => $code]);

        self::assertNotNull(
            $entity,
            sprintf('Referentiel %s "%s" introuvable : fixtures comptables chargees (make test-db-setup) ?', $entityClass, $code),
        );

        return $entity;
    }
}
