<?php

declare(strict_types=1);

namespace App\Tests\Module\Commercial\Api;

use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Module\Commercial\Domain\Entity\Client as ClientEntity;
use App\Module\Commercial\Domain\Entity\ClientAddress;
use App\Module\Sites\Domain\Entity\Site;

/**
 * Tests fonctionnels de l'API /api/clients (M1) — branche ERP-55.
 *
 * Authentifies en ADMIN (bypass RBAC via isAdmin) : on valide ici les regles
 * METIER (normalisation, unicite, distributor/broker, archivage, liste). Le
 * gating par permission (accounting.manage / archive / RG-1.28 strict) est
 * couvert par les tests unitaires du ClientProcessor : il
 * exige des users non-admin portant des permissions `commercial.clients.*` qui
 * ne sont declarees qu'en ERP-59 (tests RBAC complets en ERP-60).
 *
 * @internal
 */
final class ClientApiTest extends AbstractCommercialApiTestCase
{
    private const string LD = 'application/ld+json';

    public function testPostNormalizesCompanyName(): void
    {
        $client = $this->createAdminClient();
        $cat    = $this->createCategory('SECTEUR');

        $response = $client->request('POST', '/api/clients', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'companyName' => 'acme sas',
                'categories'  => ['/api/categories/'.$cat->getId()],
            ],
        ]);

        self::assertResponseStatusCodeSame(201);
        $data = $response->toArray();
        // RG-1.18 : companyName normalise en MAJUSCULES. Les champs de contact
        // inline ont disparu (refonte contact) -> plus de normalisation ici.
        self::assertSame('ACME SAS', $data['companyName']);
        self::assertArrayNotHasKey('firstName', $data);
        self::assertArrayNotHasKey('email', $data);
        self::assertFalse($data['isArchived']);
    }

    public function testPostDuplicateCompanyNameReturns409(): void
    {
        $client = $this->createAdminClient();
        $cat    = $this->createCategory('SECTEUR');
        $iri    = '/api/categories/'.$cat->getId();

        $payload = [
            'companyName' => 'Doublon SARL',
            'categories'  => [$iri],
        ];

        $client->request('POST', '/api/clients', ['headers' => ['Content-Type' => self::LD], 'json' => $payload]);
        self::assertResponseStatusCodeSame(201);

        // Meme nom (insensible a la casse via l'index LOWER) -> 409 (RG-1.16).
        $client->request('POST', '/api/clients', ['headers' => ['Content-Type' => self::LD], 'json' => $payload]);
        self::assertResponseStatusCodeSame(409);
    }

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

        $client->request('POST', '/api/clients', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'companyName' => 'No Category',
                'categories'  => [],
            ],
        ]);

        // Assert\Count(min: 1)
        self::assertResponseStatusCodeSame(422);
    }

    public function testPostWithDistributorAndBrokerReturns422(): void
    {
        $client      = $this->createAdminClient();
        $cat         = $this->createCategory('SECTEUR');
        $distributor = $this->seedClient('Distrib Mutex', false, 'DISTRIBUTEUR');

        $client->request('POST', '/api/clients', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'companyName'  => 'Mutex Client',
                'categories'   => ['/api/categories/'.$cat->getId()],
                'distributor'  => '/api/clients/'.$distributor->getId(),
                'broker'       => '/api/clients/'.$distributor->getId(),
            ],
        ]);

        // RG-1.03 (exclusivite)
        self::assertResponseStatusCodeSame(422);
    }

    public function testPostDistributorReferencingNonDistributorReturns422(): void
    {
        $client    = $this->createAdminClient();
        $cat       = $this->createCategory('SECTEUR');
        $notDistro = $this->seedClient('Pas Un Distrib', false, 'SECTEUR');

        $client->request('POST', '/api/clients', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'companyName'  => 'Bad Distrib Ref',
                'categories'   => ['/api/categories/'.$cat->getId()],
                'distributor'  => '/api/clients/'.$notDistro->getId(),
            ],
        ]);

        // RG-1.03 (le distributor doit etre categorise DISTRIBUTEUR)
        self::assertResponseStatusCodeSame(422);
    }

    public function testPostValidDistributorReturns201(): void
    {
        $client      = $this->createAdminClient();
        $cat         = $this->createCategory('SECTEUR');
        $distributor = $this->seedClient('Vrai Distrib', false, 'DISTRIBUTEUR');

        $client->request('POST', '/api/clients', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'companyName'  => 'Client Avec Distrib',
                'categories'   => ['/api/categories/'.$cat->getId()],
                'distributor'  => '/api/clients/'.$distributor->getId(),
            ],
        ]);

        self::assertResponseStatusCodeSame(201);
    }

    public function testPostBrokerReferencingNonBrokerReturns422(): void
    {
        $client    = $this->createAdminClient();
        $cat       = $this->createCategory('SECTEUR');
        $notBroker = $this->seedClient('Pas Un Courtier', false, 'SECTEUR');

        $client->request('POST', '/api/clients', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'companyName'  => 'Bad Broker Ref',
                'categories'   => ['/api/categories/'.$cat->getId()],
                'broker'       => '/api/clients/'.$notBroker->getId(),
            ],
        ]);

        // RG-1.03 (le broker doit porter la categorie de code COURTIER)
        self::assertResponseStatusCodeSame(422);
    }

    public function testPostValidBrokerReturns201(): void
    {
        $client = $this->createAdminClient();
        $cat    = $this->createCategory('SECTEUR');
        $broker = $this->seedClient('Vrai Courtier', false, 'COURTIER');

        $client->request('POST', '/api/clients', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'companyName'  => 'Client Avec Courtier',
                'categories'   => ['/api/categories/'.$cat->getId()],
                'broker'       => '/api/clients/'.$broker->getId(),
            ],
        ]);

        self::assertResponseStatusCodeSame(201);
    }

    public function testListSortedByCompanyNameAscAndExcludesArchived(): void
    {
        $client = $this->createAdminClient();
        $this->seedClient('Zebra Co');
        $this->seedClient('Alpha Co');
        $this->seedClient('Archivé Co', true);

        $names = $client->request('GET', '/api/clients?pagination=false', [
            'headers' => ['Accept' => self::LD],
        ])->toArray()['member'];
        $companyNames = array_map(static fn (array $c): string => $c['companyName'], $names);

        // RG-1.24 : l'archive est exclue par defaut.
        self::assertNotContains('ARCHIVÉ CO', $companyNames);
        // RG-1.26 : tri companyName ASC (Alpha avant Zebra).
        $alpha = array_search('ALPHA CO', $companyNames, true);
        $zebra = array_search('ZEBRA CO', $companyNames, true);
        self::assertNotFalse($alpha);
        self::assertNotFalse($zebra);
        self::assertLessThan($zebra, $alpha);
    }

    public function testListIncludeArchivedReturnsArchived(): void
    {
        $client = $this->createAdminClient();
        $this->seedClient('Hidden Archived', true);

        $members = $client->request('GET', '/api/clients?includeArchived=true&pagination=false', [
            'headers' => ['Accept' => self::LD],
        ])->toArray()['member'];
        $names = array_map(static fn (array $c): string => $c['companyName'], $members);

        // RG-1.25
        self::assertContains('HIDDEN ARCHIVED', $names);
    }

    public function testCollectionIsPaginated(): void
    {
        $client = $this->createAdminClient();
        $this->seedClient('Paginated One');

        // Collection Hydra avec total (la cle `view` n'apparait qu'a partir de
        // 2 pages cote API Platform 4, donc non assertable sur page unique).
        $page1 = $client->request('GET', '/api/clients', ['headers' => ['Accept' => self::LD]])->toArray();
        self::assertArrayHasKey('totalItems', $page1);
        self::assertNotEmpty($page1['member']);

        // Preuve que la pagination serveur est bien engagee : la page 2 d'un jeu
        // tenant sur une page est vide (un provider non pagine ignorerait `page`
        // et renverrait quand meme les items).
        $page2 = $client->request('GET', '/api/clients?page=2', ['headers' => ['Accept' => self::LD]])->toArray();
        self::assertSame([], $page2['member']);
    }

    public function testPatchArchiveSetsArchivedAtThenRestore(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedClient('To Archive');
        $iri    = '/api/clients/'.$seed->getId();

        // Archive (RG-1.22) : admin a la permission archive via bypass isAdmin.
        $archived = $client->request('PATCH', $iri, [
            'headers' => ['Content-Type' => 'application/merge-patch+json'],
            'json'    => ['isArchived' => true],
        ])->toArray();
        self::assertResponseStatusCodeSame(200);
        self::assertTrue($archived['isArchived']);
        self::assertNotNull($archived['archivedAt']);

        // Restauration (RG-1.23) : archivedAt repasse a null.
        $restored = $client->request('PATCH', $iri, [
            'headers' => ['Content-Type' => 'application/merge-patch+json'],
            'json'    => ['isArchived' => false],
        ])->toArray();
        self::assertResponseStatusCodeSame(200);
        self::assertFalse($restored['isArchived']);
        self::assertNull($restored['archivedAt']);
    }

    public function testPatchArchiveWithOtherFieldReturns422(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedClient('Archive Plus Field');

        $client->request('PATCH', '/api/clients/'.$seed->getId(), [
            'headers' => ['Content-Type' => 'application/merge-patch+json'],
            'json'    => ['isArchived' => true, 'companyName' => 'Renamed'],
        ]);

        // RG-1.22 : une requete d'archivage ne modifie aucun autre champ.
        self::assertResponseStatusCodeSame(422);
    }

    public function testGetDetailEmbedsSubCollections(): void
    {
        $client = $this->createAdminClient();
        $seed   = $this->seedClient('Detail Embed');

        $data = $client->request('GET', '/api/clients/'.$seed->getId(), [
            'headers' => ['Accept' => self::LD],
        ])->toArray();

        // § 4.2 : le detail embarque contacts / adresses / ribs.
        self::assertArrayHasKey('contacts', $data);
        self::assertArrayHasKey('addresses', $data);
        self::assertArrayHasKey('ribs', $data);
    }

    /**
     * ERP-62 : la LISTE doit alimenter les colonnes « Catégories » (codes) et
     * « Site(s) » (badges name + color) du Repertoire. On verifie donc que la
     * collection embarque le `code` de chaque categorie et les sites agreges des
     * adresses (accessoire Client::getSites()).
     */
    public function testListEmbedsCategoryCodesAndAggregatedSites(): void
    {
        $client = $this->createAdminClient();

        // Client seede + une adresse rattachee a un site (fixtures Sites).
        $seed = $this->seedClient('Embed List Co', false, 'DISTRIBUTEUR');
        $em   = $this->getEm();
        $site = $em->getRepository(Site::class)->findOneBy([]);
        self::assertNotNull($site, 'Aucun site seede : impossible de tester la colonne Site(s).');

        $address = new ClientAddress();
        $address->setClient($seed);
        $address->setPostalCode('86100');
        $address->setCity('Châtellerault');
        $address->setStreet('1 rue du Test');
        $address->addSite($site);
        $em->persist($address);
        $em->flush();

        $member = $client->request('GET', '/api/clients?pagination=false', [
            'headers' => ['Accept' => self::LD],
        ])->toArray()['member'];

        $row = null;
        foreach ($member as $candidate) {
            if ('EMBED LIST CO' === $candidate['companyName']) {
                $row = $candidate;

                break;
            }
        }
        self::assertNotNull($row, 'Le client seede doit figurer dans la liste.');

        // Colonne « Catégories » : chaque categorie embarquee porte son code.
        self::assertNotEmpty($row['categories']);
        self::assertArrayHasKey('code', $row['categories'][0]);
        self::assertSame('DISTRIBUTEUR', $row['categories'][0]['code']);

        // Colonne « Site(s) » : sites agreges des adresses, avec name + color.
        self::assertArrayHasKey('sites', $row);
        self::assertNotEmpty($row['sites']);
        self::assertArrayHasKey('name', $row['sites'][0]);
        self::assertArrayHasKey('color', $row['sites'][0]);
        self::assertSame($site->getName(), $row['sites'][0]['name']);
    }

    /**
     * ERP-62 (drawer) : filtre Catégories multi (?categoryCode[]=A&categoryCode[]=B)
     * — union des clients possedant l'un OU l'autre code.
     */
    public function testListFilterByMultipleCategoryCodes(): void
    {
        $client = $this->createAdminClient();
        $this->seedClient('Filtre Distrib Co', false, 'DISTRIBUTEUR');
        $this->seedClient('Filtre Courtier Co', false, 'COURTIER');
        $this->seedClient('Filtre Secteur Co', false, 'SECTEUR');

        $names = $this->companyNames($client, '/api/clients?pagination=false&categoryCode[]=DISTRIBUTEUR&categoryCode[]=COURTIER');

        self::assertContains('FILTRE DISTRIB CO', $names);
        self::assertContains('FILTRE COURTIER CO', $names);
        self::assertNotContains('FILTRE SECTEUR CO', $names);
    }

    /**
     * ERP-62 (drawer) : filtre Sites (?siteId[]=X) — clients ayant >= 1 adresse
     * rattachee au site donne.
     */
    public function testListFilterBySite(): void
    {
        $client = $this->createAdminClient();
        $em     = $this->getEm();

        $sites = $em->getRepository(Site::class)->findBy([], null, 2);
        self::assertCount(2, $sites, 'Deux sites seedes requis pour ce test.');
        [$siteA, $siteB] = $sites;

        $onSiteA = $this->seedClient('Client Sur Site A');
        $this->attachAddressWithSite($onSiteA, $siteA);

        $onSiteB = $this->seedClient('Client Sur Site B');
        $this->attachAddressWithSite($onSiteB, $siteB);

        $names = $this->companyNames($client, '/api/clients?pagination=false&siteId[]='.$siteA->getId());

        self::assertContains('CLIENT SUR SITE A', $names);
        self::assertNotContains('CLIENT SUR SITE B', $names);
    }

    /**
     * ERP-62 (drawer) : statut « Archivés » (?archivedOnly=true) — uniquement les
     * archives, contrairement a includeArchived qui ajoute les archives aux actifs.
     */
    public function testListArchivedOnlyReturnsOnlyArchived(): void
    {
        $client = $this->createAdminClient();
        $this->seedClient('Actif Visible Co');
        $this->seedClient('Archive Visible Co', true);

        $names = $this->companyNames($client, '/api/clients?pagination=false&archivedOnly=true');

        self::assertContains('ARCHIVE VISIBLE CO', $names);
        self::assertNotContains('ACTIF VISIBLE CO', $names);
    }

    /**
     * Rattache une adresse minimale portant un site au client (les sites vivent
     * sur les adresses, RG-1.10).
     */
    private function attachAddressWithSite(ClientEntity $client, Site $site): void
    {
        $em      = $this->getEm();
        $address = new ClientAddress();
        $address->setClient($client);
        $address->setPostalCode('86100');
        $address->setCity('Châtellerault');
        $address->setStreet('1 rue du Test');
        $address->addSite($site);
        $em->persist($address);
        $em->flush();
    }

    /**
     * Helper : recupere les companyName d'une collection /api/clients.
     *
     * @return list<string>
     */
    private function companyNames(Client $client, string $url): array
    {
        $members = $client->request('GET', $url, [
            'headers' => ['Accept' => self::LD],
        ])->toArray()['member'];

        return array_map(static fn (array $c): string => $c['companyName'], $members);
    }
}
