<?php

declare(strict_types=1);

namespace App\Tests\Module\Technique\Api;

use App\Module\Technique\Domain\Entity\Provider;

/**
 * Tests fonctionnels des sous-ressources Contacts / Adresses / RIB du prestataire
 * (M3, spec § 4.5 — ERP-135). Couvrent : normalisation contact (RG-3.11), RG-3.04
 * (au moins le prenom OU le nom — aligne M1/M2), RG-3.05 (>= 1 site sur
 * l'adresse), RG-3.06 (code postal), RG-3.09 (categorie PRESTATAIRE sur adresse),
 * le cloisonnement d'ecriture des sites de l'adresse (§ 2.13 -> 422 sur `sites`),
 * RG-3.08 (DELETE dernier RIB sous LCR -> 409), DELETE contact libre au M3 (pas de
 * garde « dernier contact ») et le gating selon permission (Contacts/Adresses =
 * manage, RIB = accounting.manage). Jumeau de SupplierSubResourceApiTest.
 *
 * @internal
 */
final class ProviderSubResourceApiTest extends AbstractProviderApiTestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        // seedProvider exige >= 1 site (RG-3.03) : le module Sites doit etre actif.
        $this->skipIfSitesModuleDisabled();
    }

    // === Contacts (security: technique.providers.manage) ===

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

        $data = $client->request('POST', '/api/providers/'.$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-3.11 : 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']);
    }

    /**
     * RG-3.04 (aligne M1/M2) : un bloc Contact exige le prenom OU le nom. Une
     * Fonction seule (sans nom ni prenom) ne suffit plus -> 422 rattachee a firstName.
     */
    public function testPostContactWithOnlyJobTitleReturns422(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedProvider('Contact JobTitle Only');

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

        self::assertResponseStatusCodeSame(422);
        self::assertArrayHasKey('firstName', $this->violationsByPath($response->toArray(false)));
    }

    /**
     * RG-3.04 : un bloc Contact sans prenom NI nom (meme avec d'autres champs ou
     * apres normalisation des chaines vides) est rejete avant la base -> 422
     * rattachee a firstName (double garde CHECK chk_provider_contact_name).
     */
    public function testPostContactWithoutNameReturns422OnFirstNamePath(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedProvider('Contact No Name');

        $response = $client->request('POST', '/api/providers/'.$seed->getId().'/contacts', [
            'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
            // Email + telephone fournis mais ni prenom ni nom -> invalide (RG-3.04).
            'json'    => ['email' => 'contact@acme.fr', 'phonePrimary' => '0612345678'],
        ]);

        self::assertResponseStatusCodeSame(422);
        self::assertArrayHasKey('firstName', $this->violationsByPath($response->toArray(false)));
    }

    /**
     * RG-3.04 : le prenom SEUL (sans nom) suffit a valider le contact -> 201.
     */
    public function testPostContactWithOnlyFirstNameReturns201(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedProvider('Contact FirstName Only');

        $data = $client->request('POST', '/api/providers/'.$seed->getId().'/contacts', [
            'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
            'json'    => ['firstName' => 'Jean'],
        ])->toArray();

        self::assertResponseStatusCodeSame(201);
        self::assertSame('Jean', $data['firstName']);
    }

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

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

        self::assertResponseStatusCodeSame(404);
    }

    public function testPatchContactNormalizesFields(): void
    {
        $client  = $this->createAdminClient();
        $seed    = $this->seedProvider('Contact Patch');
        $contact = $this->addContact($seed, 'Marie', 'Martin');

        $data = $client->request('PATCH', '/api/provider_contacts/'.$contact->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['lastName' => 'durand'],
        ])->toArray();

        self::assertResponseStatusCodeSame(200);
        // Normalisation aussi sur PATCH : "durand" -> "Durand".
        self::assertSame('Durand', $data['lastName']);
    }

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

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

        self::assertResponseStatusCodeSame(204);
    }

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

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

    // === Adresses (security: technique.providers.manage) ===

    public function testPostAddressWithValidPayloadReturns201(): void
    {
        $client   = $this->createAdminClient();
        $seed     = $this->seedProvider('Address Host');
        $category = $this->providerCategory('NETTOYAGE');

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

        self::assertResponseStatusCodeSame(201);
        self::assertSame('Châtellerault', $data['city']);
    }

    public function testPostAddressWithoutSiteReturns422(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedProvider('Address No Site');

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

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

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

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

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

    public function testPostAddressWithNonPrestataireCategoryReturns422(): void
    {
        $client  = $this->createAdminClient();
        $seed    = $this->seedProvider('Address Bad Cat');
        $foreign = $this->foreignCategory(); // type CLIENT -> interdite (RG-3.09).

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

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

    public function testDeleteAddressReturns204(): void
    {
        $client   = $this->createAdminClient();
        $seed     = $this->seedProvider('Address Delete');
        $category = $this->providerCategory('NETTOYAGE');

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

        $client->request('DELETE', $created['@id']);
        self::assertResponseStatusCodeSame(204);
    }

    public function testAddressWriteWithoutManageReturns403(): void
    {
        $seed  = $this->seedProvider('Address Forbidden');
        $creds = $this->createUserWithPermission('core.users.view');
        $http  = $this->authenticatedClient($creds['username'], $creds['password']);

        $http->request('POST', '/api/providers/'.$seed->getId().'/addresses', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'postalCode' => '86100',
                'city'       => 'Châtellerault',
                'street'     => '1 rue du Test',
                'sites'      => ['/api/sites/'.$this->site(self::SITE_86)->getId()],
            ],
        ]);
        self::assertResponseStatusCodeSame(403);
    }

    /**
     * § 2.13 / RG-3.05 (cloisonnement d'ECRITURE sur l'adresse) : un user non-bypass
     * `sites.read_ref` (qui peut resoudre n'importe quel IRI de site, sinon 400 en
     * amont) ne peut attacher a l'adresse que ses propres user_site. Site hors
     * perimetre -> 422 sur `sites` (garde ProviderAddressProcessor).
     */
    public function testPostAddressWithOutOfScopeSiteReturns422OnSitesPath(): void
    {
        $seed     = $this->seedProvider('Address Scope', [self::SITE_86]);
        $category = $this->providerCategory('NETTOYAGE');

        $creds = $this->createScopedUser(
            ['technique.providers.view', 'technique.providers.manage', 'sites.read_ref'],
            sitePostalCodes: [self::SITE_86],
            currentSitePostalCode: self::SITE_86,
        );
        $client = $this->authenticatedClient($creds['username'], $creds['password']);

        $response = $client->request('POST', '/api/providers/'.$seed->getId().'/addresses', [
            'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
            'json'    => [
                'postalCode' => '17400',
                'city'       => 'Saint-Jean-d\'Angély',
                'street'     => '1 rue du Test',
                'sites'      => ['/api/sites/'.$this->site(self::SITE_17)->getId()], // hors user_site
                'categories' => ['/api/categories/'.$category->getId()],
            ],
        ]);

        self::assertResponseStatusCodeSame(422);
        self::assertArrayHasKey('sites', $this->violationsByPath($response->toArray(false)));
    }

    // === RIBs (security: technique.providers.accounting.manage) ===

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

        $data = $client->request('POST', '/api/providers/'.$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->seedProvider('Rib Bad Iban');

        $client->request('POST', '/api/providers/'.$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 sur `bic`.
     */
    public function testPostRibWithBicIbanCountryMismatchReturns422OnBic(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedProvider('Rib Pays Mismatch');

        $response = $client->request('POST', '/api/providers/'.$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);
        self::assertSame('Le BIC ne correspond pas au pays de l\'IBAN.', $byPath['bic']);
    }

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

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

        self::assertResponseStatusCodeSame(204);
    }

    public function testDeleteLastRibUnderLcrReturns409(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedProvider('Rib LCR Solo');
        $rib    = $this->addRib($seed);

        // Passe le prestataire en LCR (seed direct).
        $em      = $this->getEm();
        $managed = $em->getRepository(Provider::class)->find($seed->getId());
        $managed->setPaymentType($this->paymentType('LCR'));
        $em->flush();

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

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

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

        $http->request('POST', '/api/providers/'.$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/provider_ribs/'.$rib->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['label' => 'Y'],
        ]);
        self::assertResponseStatusCodeSame(403);

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