<?php

declare(strict_types=1);

namespace App\Tests\Module\Transport\Api;

/**
 * Tests du CONTRAT DE SERIALISATION du repertoire transporteurs (M4, spec-back
 * § 4.0 / § 4.0.bis). Jumeau de {@see \App\Tests\Module\Commercial\Api\SupplierSerializationContractTest}.
 * Reverifie sur le JSON REEL les pieges silencieux du M1 transposes au M4 :
 *  - #1/#2 : relations embarquees en OBJET (pas IRI nu) — qualimatCarrier, et au
 *            detail prices[].client / .supplier / .departureSite / .deliverySite.
 *  - #3 : booleens isArchived / isChartered presents dans le JSON (Groups +
 *         SerializedName sur le getter).
 *  - enveloppe AP4 (member/totalItems/view sans prefixe hydra:) + exclusion des
 *    archives par defaut, ?includeArchived=true les reintegre.
 *
 * REGLE D'OR : on asserte sur le CORPS JSON reel, jamais sur les annotations.
 *
 * @internal
 */
final class CarrierSerializationContractTest extends AbstractCarrierApiTestCase
{
    // === Enveloppe AP4 + exclusion des archives (§ 4.1) ===

    public function testCollectionEnvelopeShapeAndArchivedExcluded(): void
    {
        $http  = $this->createAdminClient();
        $token = 'EnvCheck'.substr(bin2hex(random_bytes(3)), 0, 6);

        $this->seedCarrier($token.' Active');
        $this->seedCarrier($token.' Archived', true);

        $default = $http->request('GET', '/api/carriers?search='.$token, ['headers' => ['Accept' => self::LD]])->toArray();

        self::assertArrayHasKey('member', $default);
        self::assertArrayHasKey('totalItems', $default);
        self::assertArrayNotHasKey('hydra:member', $default);
        self::assertArrayNotHasKey('hydra:totalItems', $default);
        self::assertSame(1, $default['totalItems'], 'Archive exclu du totalItems par defaut.');

        $all = $http->request('GET', '/api/carriers?search='.$token.'&includeArchived=true', ['headers' => ['Accept' => self::LD]])->toArray();
        self::assertSame(2, $all['totalItems']);

        $paged = $http->request('GET', '/api/carriers?search='.$token.'&includeArchived=true&itemsPerPage=1', ['headers' => ['Accept' => self::LD]])->toArray();
        self::assertArrayHasKey('view', $paged);
        self::assertArrayNotHasKey('hydra:view', $paged);
    }

    // === #3 — Booleens presents (isArchived) + embed qualimatCarrier en LISTE ===

    public function testListExposesIsArchivedAndEmbeddedQualimat(): void
    {
        $token   = 'List'.substr(bin2hex(random_bytes(3)), 0, 6);
        $carrier = $this->seedCompleteCarrier($token);

        $http = $this->createAdminClient();
        $list = $http->request('GET', '/api/carriers?search='.$token, ['headers' => ['Accept' => self::LD]])->toArray();

        $row = $this->memberById($list, (int) $carrier->getId());
        self::assertNotNull($row, 'Le transporteur seede doit apparaitre dans la liste filtree.');

        // Boolean trap (#3) : cle presente et typee bool.
        self::assertArrayHasKey('isArchived', $row);
        self::assertFalse($row['isArchived']);

        // qualimatCarrier embarque en OBJET (statut + date de validite — RG-4.04),
        // pas un IRI nu (#1/#2).
        self::assertArrayHasKey('qualimatCarrier', $row);
        self::assertIsArray($row['qualimatCarrier'], 'qualimatCarrier doit etre un objet embarque, pas un IRI nu.');
        self::assertArrayHasKey('status', $row['qualimatCarrier']);
        self::assertArrayHasKey('validityDate', $row['qualimatCarrier']);

        // updatedAt (default:read) expose pour la colonne « Derniere activite ».
        self::assertArrayHasKey('updatedAt', $row);
    }

    // === Detail : sous-collections embarquees + booleens ===

    public function testDetailEmbedsSubCollectionsAndBooleans(): void
    {
        $carrier = $this->seedCompleteCarrier('Detail Co');

        $http = $this->createAdminClient();
        $data = $http->request('GET', '/api/carriers/'.$carrier->getId(), ['headers' => ['Accept' => self::LD]])->toArray();

        self::assertArrayHasKey('isArchived', $data);
        self::assertArrayHasKey('isChartered', $data);
        self::assertFalse($data['isArchived']);

        self::assertNotEmpty($data['addresses']);
        self::assertSame('Poitiers', $data['addresses'][0]['city']);

        self::assertNotEmpty($data['contacts']);
        self::assertSame('Marie', $data['contacts'][0]['firstName']);

        self::assertNotEmpty($data['prices']);
        self::assertGreaterThanOrEqual(2, count($data['prices']));
    }

    // === #1/#2 — prices[] : client / supplier / sites embarques en OBJET ===

    public function testPriceCrossModuleRelationsAreEmbeddedObjects(): void
    {
        $carrier = $this->seedCompleteCarrier('Price Embed Co');

        $http = $this->createAdminClient();
        $data = $http->request('GET', '/api/carriers/'.$carrier->getId(), ['headers' => ['Accept' => self::LD]])->toArray();

        $byDirection = [];
        foreach ($data['prices'] as $price) {
            $byDirection[$price['direction']] = $price;
        }

        self::assertArrayHasKey('CLIENT', $byDirection);
        self::assertArrayHasKey('FOURNISSEUR', $byDirection);

        // Branche CLIENT : client + adresse + site de depart en OBJET (pas IRI).
        $clientPrice = $byDirection['CLIENT'];
        self::assertIsArray($clientPrice['client'], 'prices[].client doit etre un objet embarque (client:read), pas un IRI nu.');
        self::assertArrayHasKey('companyName', $clientPrice['client']);
        self::assertIsArray($clientPrice['clientDeliveryAddress']);
        self::assertArrayHasKey('city', $clientPrice['clientDeliveryAddress'], 'L\'adresse client doit embarquer ses champs (client_address:read).');
        self::assertIsArray($clientPrice['departureSite']);
        self::assertArrayHasKey('name', $clientPrice['departureSite']);

        // Branche FOURNISSEUR : supplier + adresse + site de livraison en OBJET.
        $supplierPrice = $byDirection['FOURNISSEUR'];
        self::assertIsArray($supplierPrice['supplier'], 'prices[].supplier doit etre un objet embarque (supplier:read), pas un IRI nu.');
        self::assertArrayHasKey('companyName', $supplierPrice['supplier']);
        self::assertIsArray($supplierPrice['supplierSupplyAddress']);
        self::assertArrayHasKey('city', $supplierPrice['supplierSupplyAddress'], 'L\'adresse fournisseur doit embarquer ses champs (supplier_address:read).');
        self::assertIsArray($supplierPrice['deliverySite']);
    }

    // === RBAC : 403 sans la permission view ===

    public function testForbiddenWithoutViewPermission(): void
    {
        $carrier = $this->seedCarrier('Rbac Co');

        // user-nothing : aucune permission transport.carriers.*.
        $creds = $this->createUserWithPermission('core.users.view');
        $http  = $this->authenticatedClient($creds['username'], $creds['password']);

        $http->request('GET', '/api/carriers', ['headers' => ['Accept' => self::LD]]);
        self::assertSame(403, $http->getResponse()->getStatusCode());

        $http->request('GET', '/api/carriers/'.$carrier->getId(), ['headers' => ['Accept' => self::LD]]);
        self::assertSame(403, $http->getResponse()->getStatusCode());
    }

    /**
     * DoD (§ 4.0.bis) : capture des reponses JSON REELLES (liste + detail) pour
     * les coller dans la spec avant de lancer les tickets front. Le test asserte
     * la forme ; si CARRIER_DOD_DUMP est positionnee, ecrit les corps sous /tmp.
     */
    public function testDodReferenceJsonShape(): void
    {
        $token   = 'DoD'.substr(bin2hex(random_bytes(3)), 0, 6);
        $carrier = $this->seedCompleteCarrier($token);
        $id      = (int) $carrier->getId();

        $admin  = $this->createAdminClient();
        $list   = $admin->request('GET', '/api/carriers?search='.$token, ['headers' => ['Accept' => self::LD]])->toArray();
        $detail = $admin->request('GET', '/api/carriers/'.$id, ['headers' => ['Accept' => self::LD]])->toArray();

        self::assertArrayHasKey('member', $list);
        self::assertArrayHasKey('qualimatCarrier', $detail);
        self::assertArrayHasKey('addresses', $detail);
        self::assertArrayHasKey('contacts', $detail);
        self::assertArrayHasKey('prices', $detail);

        if (false !== getenv('CARRIER_DOD_DUMP')) {
            $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
            file_put_contents('/tmp/carrier-dod-list.json', json_encode($list, $flags));
            file_put_contents('/tmp/carrier-dod-detail.json', json_encode($detail, $flags));
        }
    }

    /**
     * Retrouve un membre de la collection par son id (liste filtree).
     *
     * @param array<string, mixed> $collection
     *
     * @return array<string, mixed>|null
     */
    private function memberById(array $collection, int $id): ?array
    {
        foreach ($collection['member'] ?? [] as $member) {
            if (($member['id'] ?? null) === $id) {
                return $member;
            }
        }

        return null;
    }
}
