<?php

declare(strict_types=1);

namespace App\Tests\Module\Catalog\Api;

use App\Module\Catalog\Domain\Entity\Category;

/**
 * Tests des regles de validation POST/PATCH sur Category :
 *  - RG-1.02 : `name` obligatoire (NotBlank) ;
 *  - RG-1.03 : `name` trim cote serveur via CategoryProcessor ;
 *  - RG-1.04 : `name` longueur 2..120 (Length) ;
 *  - RG-1.05 : `categoryTypes` — au moins un type (Count min 1) ;
 *  - RG-1.06 : chaque IRI de `categoryTypes` doit pointer un type existant.
 *
 * @internal
 */
final class CategoryValidationTest extends AbstractCatalogApiTestCase
{
    // ============ RG-1.02 — name NotBlank ============

    public function testNameRequiredReturns422(): void
    {
        $type   = $this->createCategoryType();
        $client = $this->createAdminClient();
        $client->request('POST', '/api/categories', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => [
                'categoryTypes' => ['/api/category_types/'.$type->getId()],
                // name absent
            ],
        ]);

        self::assertResponseStatusCodeSame(422);
    }

    public function testNameEmptyStringReturns422(): void
    {
        $type   = $this->createCategoryType();
        $client = $this->createAdminClient();
        $client->request('POST', '/api/categories', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => [
                'name'          => '',
                'categoryTypes' => ['/api/category_types/'.$type->getId()],
            ],
        ]);

        self::assertResponseStatusCodeSame(422);
    }

    public function testNameWhitespaceOnlyReturns422(): void
    {
        // Le Processor trim avant la validation : "   " devient "" -> NotBlank
        // doit declencher 422 (RG-1.02 combinee a RG-1.03).
        $type   = $this->createCategoryType();
        $client = $this->createAdminClient();
        $client->request('POST', '/api/categories', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => [
                'name'          => '   ',
                'categoryTypes' => ['/api/category_types/'.$type->getId()],
            ],
        ]);

        self::assertResponseStatusCodeSame(422);
    }

    // ============ RG-1.03 — name trim cote serveur ============

    public function testNameIsTrimmedOnCreate(): void
    {
        $type        = $this->createCategoryType();
        $client      = $this->createAdminClient();
        $payloadName = '  '.self::TEST_CATEGORY_PREFIX.'trim  ';
        $expected    = trim($payloadName);

        $client->request('POST', '/api/categories', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => [
                'name'          => $payloadName,
                'categoryTypes' => ['/api/category_types/'.$type->getId()],
            ],
        ]);

        self::assertResponseStatusCodeSame(201);

        // Verification cote base : la valeur stockee est trimee.
        $em = $this->getEm();
        $em->clear();
        $stored = $em->getRepository(Category::class)->findOneBy(['name' => $expected]);
        self::assertNotNull($stored, 'La categorie trimee doit etre persistee sous "'.$expected.'"');
        self::assertSame($expected, $stored->getName());
    }

    // ============ RG-1.04 — longueur 2..120 ============

    public function testNameTooShortReturns422(): void
    {
        $type   = $this->createCategoryType();
        $client = $this->createAdminClient();
        $client->request('POST', '/api/categories', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => [
                'name'          => 'A',
                'categoryTypes' => ['/api/category_types/'.$type->getId()],
            ],
        ]);

        self::assertResponseStatusCodeSame(422);
    }

    public function testNameTooLongReturns422(): void
    {
        $type   = $this->createCategoryType();
        $client = $this->createAdminClient();
        $client->request('POST', '/api/categories', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => [
                'name'          => str_repeat('a', 121),
                'categoryTypes' => ['/api/category_types/'.$type->getId()],
            ],
        ]);

        self::assertResponseStatusCodeSame(422);
    }

    public function testNameAtMaxLengthIs201(): void
    {
        // Borne haute : 120 caracteres doit passer (l'index est sur LOWER, name
        // est unique en collision avec d'autres tests donc on prefixe la marque
        // test_cat_ pour le cleanup et completons jusqu'a 120 caracteres).
        $prefix = self::TEST_CATEGORY_PREFIX;
        $name   = $prefix.str_repeat('z', 120 - strlen($prefix));
        self::assertSame(120, strlen($name));

        $type   = $this->createCategoryType();
        $client = $this->createAdminClient();
        $client->request('POST', '/api/categories', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => [
                'name'          => $name,
                'categoryTypes' => ['/api/category_types/'.$type->getId()],
            ],
        ]);

        self::assertResponseStatusCodeSame(201);
    }

    // ============ RG-1.05 — au moins un type (Count min 1) ============

    public function testCategoryTypesRequiredReturns422(): void
    {
        $client = $this->createAdminClient();
        $client->request('POST', '/api/categories', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => [
                'name' => self::TEST_CATEGORY_PREFIX.'no_type',
                // categoryTypes absent -> collection vide -> Count(min:1) viole.
            ],
        ]);

        self::assertResponseStatusCodeSame(422);
    }

    public function testCategoryTypesEmptyReturns422(): void
    {
        // Tableau vide explicite : Assert\Count(min: 1) doit declencher 422 avec
        // une violation sur le propertyPath `categoryTypes` (consommable inline).
        $client   = $this->createAdminClient();
        $response = $client->request('POST', '/api/categories', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => [
                'name'          => self::TEST_CATEGORY_PREFIX.'empty_types',
                'categoryTypes' => [],
            ],
        ]);

        self::assertSame(422, $response->getStatusCode());
        $payload    = $response->toArray(false);
        $violations = $payload['violations'] ?? $payload['hydra:violations'] ?? [];
        $paths      = array_column($violations, 'propertyPath');
        self::assertContains(
            'categoryTypes',
            $paths,
            'La violation Count doit porter le propertyPath `categoryTypes`.',
        );
    }

    // ============ RG-1.06 — chaque type doit exister ============

    public function testCategoryTypeMustExistReturns4xx(): void
    {
        // IRI vers un id qui n'existe pas. API Platform peut renvoyer 400
        // (resolution IRI echouee) ou 422 (validation declenchee). La spec § 4.3
        // accepte les deux : on assert le contrat "ne passe pas".
        $client   = $this->createAdminClient();
        $response = $client->request('POST', '/api/categories', [
            'headers' => ['Content-Type' => 'application/ld+json'],
            'json'    => [
                'name'          => self::TEST_CATEGORY_PREFIX.'ghost_type',
                'categoryTypes' => ['/api/category_types/9999999'],
            ],
        ]);

        self::assertContains(
            $response->getStatusCode(),
            [400, 404, 422],
            'IRI categoryTypes inexistante doit etre rejetee (400/404/422 selon API Platform).',
        );
    }
}
