<?php

declare(strict_types=1);

namespace App\Tests\Module\Transport\Api;

use App\Module\Commercial\Domain\Entity\ClientAddress;
use App\Module\Core\Infrastructure\DataFixtures\RbacDemoFixtures;
use App\Module\Sites\Domain\Entity\Site;
use App\Module\Transport\Domain\Entity\CarrierPrice;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;

/**
 * Sous-ressource Prix d'un transporteur (spec-back M4 § 4.5, ERP-161).
 * POST /api/carriers/{id}/prices, PATCH/DELETE /api/carrier_prices/{id}.
 *
 * Contrat verifie (RG-4.09→4.11) :
 *  - branche CLIENT incomplete -> 422 ;
 *  - branche FOURNISSEUR incomplete -> 422 ;
 *  - adresse de livraison etrangere au client -> 422 ;
 *  - adresse d'appro etrangere au fournisseur -> 422 ;
 *  - prix CLIENT / FOURNISSEUR complets -> 201 ;
 *  - PATCH / DELETE OK avec transport.carriers.manage, 403 sans (view seul).
 *
 * @internal
 */
final class CarrierPriceApiTest extends AbstractCarrierApiTestCase
{
    private const string PWD = RbacDemoFixtures::DEMO_PASSWORD;

    protected function setUp(): void
    {
        parent::setUp();

        // Seed idempotent des roles + matrice § 5.2 + comptes demo (meme chemin
        // qu'en recette), requis pour les tests de permission (bureau/commerciale).
        self::bootKernel();
        $application = new Application(self::$kernel);
        $application->setAutoExit(false);
        $exit = $application->run(
            new ArrayInput([
                'command'           => 'app:seed-rbac',
                '--with-demo-users' => true,
                '--password'        => self::PWD,
            ]),
            new NullOutput(),
        );
        self::assertSame(0, $exit, 'app:seed-rbac a echoue (permissions transport.carriers.* synchronisees ?).');

        self::ensureKernelShutdown();
    }

    public function testIncompleteClientBranchReturns422(): void
    {
        // RG-4.10 : direction CLIENT sans client / adresse / site de depart -> 422.
        $carrier = $this->seedCarrier('Prix Client Incomplet');
        $client  = $this->createAdminClient();

        $client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'direction'     => 'CLIENT',
                'containerType' => 'BENNE',
                'pricingUnit'   => 'TONNE',
                'price'         => '42.50',
                'priceState'    => 'VALIDE',
            ],
        ]);
        self::assertResponseStatusCodeSame(422);
    }

    public function testIncompleteSupplierBranchReturns422(): void
    {
        // RG-4.11 : direction FOURNISSEUR sans fournisseur / adresse / site -> 422.
        $carrier = $this->seedCarrier('Prix Fournisseur Incomplet');
        $client  = $this->createAdminClient();

        $client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'direction'     => 'FOURNISSEUR',
                'containerType' => 'FOND_MOUVANT',
                'pricingUnit'   => 'FORFAIT',
                'price'         => '320.00',
                'priceState'    => 'EN_COURS',
            ],
        ]);
        self::assertResponseStatusCodeSame(422);
    }

    public function testForeignClientAddressReturns422(): void
    {
        // RG-4.10 : l'adresse de livraison doit appartenir au client choisi.
        $carrier = $this->seedCarrier('Prix Adresse Etrangere Client');
        $addrA   = $this->seedClientWithAddress('Client A');
        $addrB   = $this->seedClientWithAddress('Client B');
        $this->getEm()->flush();
        $siteId = $this->aSiteId();

        $client   = $this->createAdminClient();
        $response = $client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'direction'             => 'CLIENT',
                'client'                => '/api/clients/'.$addrA->getClient()?->getId(),
                'clientDeliveryAddress' => '/api/client_addresses/'.$addrB->getId(), // adresse du client B
                'departureSite'         => '/api/sites/'.$siteId,
                'containerType'         => 'BENNE',
                'pricingUnit'           => 'TONNE',
                'price'                 => '42.50',
                'priceState'            => 'VALIDE',
            ],
        ]);
        self::assertResponseStatusCodeSame(422);
        // Faux-vert evite : la 422 doit prouver l'integrite referentielle adresse<->tiers
        // (violation sur clientDeliveryAddress), pas une autre cause (RG-4.10, ERP-101).
        self::assertViolationOnPath($response, 'clientDeliveryAddress');
    }

    public function testForeignSupplierAddressReturns422(): void
    {
        // RG-4.11 : l'adresse d'appro doit appartenir au fournisseur choisi.
        $carrier = $this->seedCarrier('Prix Adresse Etrangere Fournisseur');
        $addrA   = $this->seedSupplierWithAddress('Fournisseur A');
        $addrB   = $this->seedSupplierWithAddress('Fournisseur B');
        $this->getEm()->flush();
        $siteId = $this->aSiteId();

        $client   = $this->createAdminClient();
        $response = $client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'direction'             => 'FOURNISSEUR',
                'supplier'              => '/api/suppliers/'.$addrA->getSupplier()?->getId(),
                'supplierSupplyAddress' => '/api/supplier_addresses/'.$addrB->getId(), // adresse du fournisseur B
                'deliverySite'          => '/api/sites/'.$siteId,
                'containerType'         => 'FOND_MOUVANT',
                'pricingUnit'           => 'FORFAIT',
                'price'                 => '320.00',
                'priceState'            => 'EN_COURS',
            ],
        ]);
        self::assertResponseStatusCodeSame(422);
        self::assertViolationOnPath($response, 'supplierSupplyAddress');
    }

    public function testValidClientPriceIsCreated(): void
    {
        $carrier = $this->seedCarrier('Prix Client Valide');
        $addr    = $this->seedClientWithAddress('Client OK');
        $this->getEm()->flush();
        $siteId = $this->aSiteId();

        $client = $this->createAdminClient();
        $client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'direction'             => 'CLIENT',
                'client'                => '/api/clients/'.$addr->getClient()?->getId(),
                'clientDeliveryAddress' => '/api/client_addresses/'.$addr->getId(),
                'departureSite'         => '/api/sites/'.$siteId,
                'containerType'         => 'BENNE',
                'pricingUnit'           => 'TONNE',
                'price'                 => '42.50',
                'priceState'            => 'VALIDE',
            ],
        ]);
        self::assertResponseStatusCodeSame(201);
        self::assertJsonContains(['direction' => 'CLIENT', 'priceState' => 'VALIDE']);
    }

    public function testValidSupplierPriceIsCreated(): void
    {
        $carrier = $this->seedCarrier('Prix Fournisseur Valide');
        $addr    = $this->seedSupplierWithAddress('Fournisseur OK');
        $this->getEm()->flush();
        $siteId = $this->aSiteId();

        $client = $this->createAdminClient();
        $client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'direction'             => 'FOURNISSEUR',
                'supplier'              => '/api/suppliers/'.$addr->getSupplier()?->getId(),
                'supplierSupplyAddress' => '/api/supplier_addresses/'.$addr->getId(),
                'deliverySite'          => '/api/sites/'.$siteId,
                'containerType'         => 'FOND_MOUVANT',
                'pricingUnit'           => 'FORFAIT',
                'price'                 => '320.00',
                'priceState'            => 'EN_COURS',
            ],
        ]);
        self::assertResponseStatusCodeSame(201);
        self::assertJsonContains(['direction' => 'FOURNISSEUR', 'priceState' => 'EN_COURS']);
    }

    public function testNegativePriceReturns422(): void
    {
        // Le prix porte un Assert\PositiveOrZero : une valeur negative -> 422 sur `price`
        // (la branche CLIENT est par ailleurs complete pour isoler la cause).
        $carrier = $this->seedCarrier('Prix Negatif');
        $addr    = $this->seedClientWithAddress('Client Prix Negatif');
        $this->getEm()->flush();
        $siteId = $this->aSiteId();

        $client   = $this->createAdminClient();
        $response = $client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'direction'             => 'CLIENT',
                'client'                => '/api/clients/'.$addr->getClient()?->getId(),
                'clientDeliveryAddress' => '/api/client_addresses/'.$addr->getId(),
                'departureSite'         => '/api/sites/'.$siteId,
                'containerType'         => 'BENNE',
                'pricingUnit'           => 'TONNE',
                'price'                 => '-5.00',
                'priceState'            => 'VALIDE',
            ],
        ]);
        self::assertResponseStatusCodeSame(422);
        self::assertViolationOnPath($response, 'price');
    }

    public function testPostPriceOnUnknownCarrierReturns404(): void
    {
        // Parent introuvable (read:false) -> 404 explicite du processor (linkParent
        // s'execute avant validateBranch). Le payload porte les scalaires NotBlank
        // (containerType/pricingUnit/price/priceState) pour passer la validation
        // d'entite et atteindre le processor, ou le 404 prime.
        $client = $this->createAdminClient();

        $client->request('POST', '/api/carriers/999999/prices', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'direction'     => 'CLIENT',
                'containerType' => 'BENNE',
                'pricingUnit'   => 'TONNE',
                'price'         => '42.50',
                'priceState'    => 'VALIDE',
            ],
        ]);
        self::assertResponseStatusCodeSame(404);
    }

    public function testPatchAndDeleteSucceedWithManage(): void
    {
        $price  = $this->seedClientPrice('Patch Delete');
        $client = $this->authenticatedClient('bureau', self::PWD); // manage (matrice § 5.2)

        // PATCH (manage) -> 200
        $client->request('PATCH', '/api/carrier_prices/'.$price->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['priceState' => 'NON_VALIDE'],
        ]);
        self::assertResponseStatusCodeSame(200);
        self::assertJsonContains(['priceState' => 'NON_VALIDE']);

        // DELETE (manage) -> 204
        $client->request('DELETE', '/api/carrier_prices/'.$price->getId());
        self::assertResponseStatusCodeSame(204);
    }

    public function testWriteForbiddenWithoutManage(): void
    {
        $price   = $this->seedClientPrice('Forbidden');
        $carrier = $price->getCarrier();
        self::assertNotNull($carrier);
        $client = $this->authenticatedClient('commerciale', self::PWD); // view seul

        $client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => ['direction' => 'CLIENT'],
        ]);
        self::assertResponseStatusCodeSame(403);

        $client->request('PATCH', '/api/carrier_prices/'.$price->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['priceState' => 'VALIDE'],
        ]);
        self::assertResponseStatusCodeSame(403);

        $client->request('DELETE', '/api/carrier_prices/'.$price->getId());
        self::assertResponseStatusCodeSame(403);
    }

    /** Id d'un site fixture (adresse de depart / livraison des prix). */
    private function aSiteId(): int
    {
        $site = $this->getEm()->getRepository(Site::class)->findOneBy([]);
        self::assertNotNull($site, 'Un site fixture est requis (SitesFixtures).');
        $id = $site->getId();
        self::assertNotNull($id);

        return $id;
    }

    /**
     * Seede un transporteur + un prix CLIENT complet rattache (pour les tests
     * PATCH / DELETE). Passe par l'EM directement (le flux d'ecriture est teste
     * via l'API ailleurs).
     */
    private function seedClientPrice(string $name): CarrierPrice
    {
        $em      = $this->getEm();
        $carrier = $this->seedCarrier($name);

        /** @var ClientAddress $addr */
        $addr = $this->seedClientWithAddress($name);

        $price = new CarrierPrice();
        $price->setCarrier($carrier);
        $price->setDirection('CLIENT');
        $price->setClient($addr->getClient());
        $price->setClientDeliveryAddress($addr);
        $price->setDepartureSite($em->getRepository(Site::class)->findOneBy([]));
        $price->setContainerType('BENNE');
        $price->setPricingUnit('TONNE');
        $price->setPrice('42.50');
        $price->setPriceState('VALIDE');
        $carrier->addPrice($price);
        $em->persist($price);
        $em->flush();

        return $price;
    }
}
