<?php

declare(strict_types=1);

namespace App\Tests\Module\Commercial\Api;

use App\Module\Commercial\Domain\Entity\Supplier;
use App\Module\Sites\Domain\Entity\Site;

/**
 * Tests fonctionnels des sous-ressources Contacts / Adresses / RIB du fournisseur
 * (M2, spec § 4.5). Couvrent : normalisation contact (RG-2.12), RG-2.04 (prenom
 * OU nom), RG-2.05 (code postal), RG-2.06 (>= 1 site), RG-2.09 (enum addressType),
 * RG-2.10 (categorie FOURNISSEUR sur adresse), RG-2.08 (DELETE dernier RIB sous
 * LCR -> 409), DELETE contact libre au M2 (pas de garde « dernier contact ») et le
 * gating comptable des RIB (manage seul -> 403). Jumeau de ClientSubResourceApiTest.
 *
 * @internal
 */
final class SupplierSubResourceApiTest extends AbstractSupplierApiTestCase
{
    // === Contacts ===

    public function testPostContactNormalizesFields(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Contact Host');

        $data = $client->request('POST', '/api/suppliers/'.$seed->getId().'/contacts', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'firstName'    => 'JEAN',
                'lastName'     => 'dupont',
                'phonePrimary' => '06.12.34.56.78',
                'email'        => 'Jean.DUPONT@ACME.FR',
            ],
        ])->toArray();

        self::assertResponseStatusCodeSame(201);
        // RG-2.12 : prenom/nom Title Case, telephone chiffres seuls, email lowercase.
        self::assertSame('Jean', $data['firstName']);
        self::assertSame('Dupont', $data['lastName']);
        self::assertSame('0612345678', $data['phonePrimary']);
        self::assertSame('jean.dupont@acme.fr', $data['email']);
    }

    public function testPostContactWithoutNameReturns422OnFirstNamePath(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Contact No Name');

        $response = $client->request('POST', '/api/suppliers/'.$seed->getId().'/contacts', [
            'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
            'json'    => ['jobTitle' => 'Directeur'],
        ]);

        // RG-2.04 (prenom OU nom obligatoire) -> 422 rattachee a firstName.
        self::assertResponseStatusCodeSame(422);
        $byPath = $this->violationsByPath($response->toArray(false));
        self::assertArrayHasKey('firstName', $byPath);
    }

    public function testPostContactOnMissingSupplierReturns404(): void
    {
        $client = $this->createAdminClient();

        $client->request('POST', '/api/suppliers/999999/contacts', [
            'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
            'json'    => ['firstName' => 'Orphan'],
        ]);

        self::assertResponseStatusCodeSame(404);
    }

    public function testDeleteLastContactReturns204(): void
    {
        // M2 : pas de garde « dernier contact » (RG-2.13 front-driven) — la
        // suppression du dernier contact est libre (204), contrairement au M1.
        $client  = $this->createAdminClient();
        $seed    = $this->seedSupplier('Contact Solo');
        $contact = $this->addContact($seed, 'Unique', 'Contact');

        $client->request('DELETE', '/api/supplier_contacts/'.$contact->getId());

        self::assertResponseStatusCodeSame(204);
    }

    public function testContactWriteWithoutManageReturns403(): void
    {
        // Un user sans aucune permission suppliers -> 403 sur la sous-ressource.
        $seed  = $this->seedSupplier('Contact Forbidden');
        $creds = $this->createUserWithPermission('core.users.view');
        $http  = $this->authenticatedClient($creds['username'], $creds['password']);

        $http->request('POST', '/api/suppliers/'.$seed->getId().'/contacts', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => ['firstName' => 'Nope'],
        ]);
        self::assertResponseStatusCodeSame(403);
    }

    // === Adresses ===

    public function testPostAddressWithValidPayloadReturns201(): void
    {
        $this->skipIfSitesModuleDisabled();
        $client   = $this->createAdminClient();
        $seed     = $this->seedSupplier('Address Host');
        $category = $this->supplierCategory('NEGOCIANT');

        $data = $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'addressType' => 'DEPART',
                'postalCode'  => '86100',
                'city'        => 'Châtellerault',
                'street'      => '1 rue du Test',
                'sites'       => [$this->firstSiteIri()],
                'categories'  => ['/api/categories/'.$category->getId()],
            ],
        ])->toArray();

        self::assertResponseStatusCodeSame(201);
        self::assertSame('DEPART', $data['addressType']);
    }

    public function testPostAddressWithoutSiteReturns422(): void
    {
        // Sans cette garde, un module Sites desactive renverrait 404 (route
        // /addresses indisponible) et le test passerait pour la MAUVAISE raison
        // au lieu de prouver RG-2.06 (Assert\Count min 1 sur sites).
        $this->skipIfSitesModuleDisabled();
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Address No Site');

        $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'addressType' => 'DEPART',
                'postalCode'  => '86100',
                'city'        => 'Châtellerault',
                'street'      => '1 rue du Test',
                'sites'       => [],
            ],
        ]);

        // RG-2.06 (Assert\Count min 1 sur sites).
        self::assertResponseStatusCodeSame(422);
    }

    public function testPostAddressWithInvalidPostalCodeReturns422(): void
    {
        $this->skipIfSitesModuleDisabled();
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Address Bad CP');

        $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'addressType' => 'DEPART',
                'postalCode'  => '123',
                'city'        => 'Châtellerault',
                'street'      => '1 rue du Test',
                'sites'       => [$this->firstSiteIri()],
            ],
        ]);

        // RG-2.05 (Assert\Regex ^[0-9]{4,5}$).
        self::assertResponseStatusCodeSame(422);
    }

    public function testPostAddressWithIncoherentCityAndPostalCodeReturns201(): void
    {
        $this->skipIfSitesModuleDisabled();
        $client   = $this->createAdminClient();
        $seed     = $this->seedSupplier('Address Incoherent');
        $category = $this->supplierCategory('NEGOCIANT');

        // RG-2.05 : pas de controle strict de coherence CP/ville cote serveur.
        $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'addressType' => 'DEPART',
                'postalCode'  => '86100',
                'city'        => 'Marseille',
                'street'      => '1 rue du Test',
                'sites'       => [$this->firstSiteIri()],
                'categories'  => ['/api/categories/'.$category->getId()],
            ],
        ]);

        self::assertResponseStatusCodeSame(201);
    }

    public function testPostAddressWithInvalidTypeReturns422(): void
    {
        $this->skipIfSitesModuleDisabled();
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Address Bad Type');

        $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'addressType' => 'INVALID',
                'postalCode'  => '86100',
                'city'        => 'Châtellerault',
                'street'      => '1 rue du Test',
                'sites'       => [$this->firstSiteIri()],
            ],
        ]);

        // RG-2.09 (Assert\Choice PROSPECT|DEPART|RENDU).
        self::assertResponseStatusCodeSame(422);
    }

    /**
     * RG-2.09 : les 3 valeurs valides de addressType sont acceptees.
     */
    public function testPostAddressWithEachValidTypeReturns201(): void
    {
        $this->skipIfSitesModuleDisabled();
        $client   = $this->createAdminClient();
        $seed     = $this->seedSupplier('Address Types');
        $siteIri  = $this->firstSiteIri();
        $category = $this->supplierCategory('NEGOCIANT');

        foreach (['PROSPECT', 'DEPART', 'RENDU'] as $type) {
            $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
                'headers' => ['Content-Type' => self::LD],
                'json'    => [
                    'addressType' => $type,
                    'postalCode'  => '86100',
                    'city'        => 'Châtellerault',
                    'street'      => '1 rue du Test',
                    'sites'       => [$siteIri],
                    'categories'  => ['/api/categories/'.$category->getId()],
                ],
            ]);
            self::assertResponseStatusCodeSame(201, sprintf('addressType=%s doit etre accepte.', $type));
        }
    }

    public function testPostAddressWithNonFournisseurCategoryReturns422(): void
    {
        $this->skipIfSitesModuleDisabled();
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Address Bad Cat');
        // categorie de type CLIENT -> interdite sur une adresse fournisseur.
        $clientTypedCategory = $this->createCategory('SECTEUR');

        $response = $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
            'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
            'json'    => [
                'addressType' => 'DEPART',
                'postalCode'  => '86100',
                'city'        => 'Châtellerault',
                'street'      => '1 rue du Test',
                'sites'       => [$this->firstSiteIri()],
                'categories'  => ['/api/categories/'.$clientTypedCategory->getId()],
            ],
        ]);

        // RG-2.10 -> 422 rattachee a categories.
        self::assertResponseStatusCodeSame(422);
        self::assertArrayHasKey('categories', $this->violationsByPath($response->toArray(false)));
    }

    // === RIBs ===

    public function testPostRibByAdminReturns201(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Rib Host');

        $data = $client->request('POST', '/api/suppliers/'.$seed->getId().'/ribs', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'label' => 'Compte principal',
                'bic'   => self::VALID_BIC,
                'iban'  => self::VALID_IBAN,
            ],
        ])->toArray();

        self::assertResponseStatusCodeSame(201);
        self::assertSame('Compte principal', $data['label']);
    }

    public function testPostRibWithInvalidIbanReturns422(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Rib Bad Iban');

        $client->request('POST', '/api/suppliers/'.$seed->getId().'/ribs', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => ['label' => 'Compte invalide', 'bic' => self::VALID_BIC, 'iban' => 'INVALID-IBAN'],
        ]);

        self::assertResponseStatusCodeSame(422);
    }

    /**
     * Controle croise pays BIC/IBAN (Assert\Bic ibanPropertyPath) : un BIC (DE) et
     * un IBAN (FR) valides isolement mais de pays differents -> 422. La violation
     * porte propertyPath=bic et le message FR `ibanMessage` (mapping inline front).
     */
    public function testPostRibWithBicIbanCountryMismatchReturns422WithFrenchMessageOnBic(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Rib Pays Mismatch');

        $response = $client->request('POST', '/api/suppliers/'.$seed->getId().'/ribs', [
            'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
            'json'    => ['label' => 'Compte incoherent', 'bic' => self::FOREIGN_BIC, 'iban' => self::VALID_IBAN],
        ]);

        self::assertResponseStatusCodeSame(422);
        $byPath = $this->violationsByPath($response->toArray(false));
        self::assertArrayHasKey('bic', $byPath, 'Le mismatch pays BIC/IBAN doit porter propertyPath=bic (mapping front).');
        self::assertSame('Le BIC ne correspond pas au pays de l\'IBAN.', $byPath['bic']);
    }

    public function testDeleteRibNonLcrReturns204(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Rib Non LCR');
        $rib    = $this->addRib($seed);

        $client->request('DELETE', '/api/supplier_ribs/'.$rib->getId());

        self::assertResponseStatusCodeSame(204);
    }

    public function testDeleteLastRibUnderLcrReturns409(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedSupplier('Rib LCR Solo');
        $rib    = $this->addRib($seed);
        // Passe le fournisseur en LCR (seed direct).
        $em      = $this->getEm();
        $managed = $em->getRepository(Supplier::class)->find($seed->getId());
        $managed->setPaymentType($this->paymentType('LCR'));
        $em->flush();

        $client->request('DELETE', '/api/supplier_ribs/'.$rib->getId());

        // RG-2.08 : LCR exige >= 1 RIB -> suppression du dernier refusee.
        self::assertResponseStatusCodeSame(409);
    }

    public function testRibWriteWithoutAccountingManageReturns403(): void
    {
        // Un user portant seulement suppliers.manage (sans accounting.manage) ne
        // peut ni creer, ni modifier, ni supprimer un RIB (gating renforce § 4.5).
        $seed  = $this->seedSupplier('Rib Forbidden');
        $rib   = $this->addRib($seed);
        $creds = $this->createUserWithPermission('commercial.suppliers.manage');
        $http  = $this->authenticatedClient($creds['username'], $creds['password']);

        $http->request('POST', '/api/suppliers/'.$seed->getId().'/ribs', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => ['label' => 'X', 'bic' => self::VALID_BIC, 'iban' => self::VALID_IBAN],
        ]);
        self::assertResponseStatusCodeSame(403);

        $http->request('PATCH', '/api/supplier_ribs/'.$rib->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['label' => 'Y'],
        ]);
        self::assertResponseStatusCodeSame(403);

        $http->request('DELETE', '/api/supplier_ribs/'.$rib->getId());
        self::assertResponseStatusCodeSame(403);
    }

    // === Helpers ===

    // violationsByPath() : helper mutualise dans AbstractSupplierApiTestCase.

    private function firstSiteIri(): string
    {
        $site = $this->getEm()->getRepository(Site::class)->findOneBy([]);
        self::assertNotNull($site, 'Aucun site seede : impossible de tester les adresses.');

        return '/api/sites/'.$site->getId();
    }
}
