<?php

declare(strict_types=1);

namespace App\Tests\Module\Technique\Api;

use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Module\Core\Infrastructure\DataFixtures\RbacDemoFixtures;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;

/**
 * Matrice RBAC complete du repertoire prestataires par role metier (spec-back M3
 * § 2.9 + § 2.13, ERP-138). Valide 200/403 par verbe et par onglet pour
 * bureau / compta / commerciale / usine, le gating des champs comptables en
 * lecture (omission de cle) et le cloisonnement par site de l'Usine.
 *
 * Les comptes demo et la matrice sont seedes via la commande reelle
 * `app:seed-rbac --with-demo-users` (le MEME chemin qu'en recette), idempotente —
 * pas de mock de role. Jumeau de SupplierRBACMatrixTest (M2), avec la difference
 * structurante du M3 : l'Usine n'est plus « 403 partout » mais possede
 * `technique.providers.view` en lecture seule, CLOISONNEE a son site courant
 * (pas de `sites.bypass_scope`).
 *
 * Matrice § 2.9 (ERP-138) — rappel :
 *  - bureau      : providers.view + manage (ni accounting, ni archive) + bypass_scope
 *  - compta      : providers.view + accounting.view + accounting.manage (PAS manage) + bypass_scope
 *  - commerciale : providers.view + manage (PAS accounting) + bypass_scope
 *  - usine       : providers.view seul, SANS bypass_scope (cloisonne a son site)
 *  - archive     : admin seul (aucun role metier)
 *
 * @internal
 */
final class ProviderRBACMatrixTest extends AbstractProviderApiTestCase
{
    private const string PWD = RbacDemoFixtures::DEMO_PASSWORD;

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

        // Seed idempotent via la commande applicative (roles + matrice § 2.9 +
        // comptes demo). Exerce aussi le chemin de code prod.
        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 : les permissions technique.providers.* sont-elles synchronisees (app:sync-permissions) ?',
        );

        self::ensureKernelShutdown();
    }

    public function testBureauHasViewAndManageButNoAccountingNoArchive(): void
    {
        $seed   = $this->seedProvider('Bureau Cible');
        $client = $this->authAs('bureau');

        // view
        $client->request('GET', '/api/providers', ['headers' => ['Accept' => self::LD]]);
        self::assertResponseStatusCodeSame(200);

        // manage : creation OK (bypass_scope -> peut attacher le site 86)
        $client->request('POST', '/api/providers', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => $this->validMainPayload('Bureau Cree'),
        ]);
        self::assertResponseStatusCodeSame(201);

        // manage : edition onglet principal OK
        $client->request('PATCH', '/api/providers/'.$seed->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['companyName' => 'Bureau Renomme'],
        ]);
        self::assertResponseStatusCodeSame(200);

        // PAS accounting : edition onglet Comptabilite refusee
        $client->request('PATCH', '/api/providers/'.$seed->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['siren' => '123456789'],
        ]);
        self::assertResponseStatusCodeSame(403);

        // PAS archive : archivage refuse
        $client->request('PATCH', '/api/providers/'.$seed->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['isArchived' => true],
        ]);
        self::assertResponseStatusCodeSame(403);
    }

    public function testBureauDetailHasNoAccountingFields(): void
    {
        // Bureau a view mais PAS accounting.view : les champs comptables sont
        // ABSENTS du JSON (gating par omission, pas null).
        $provider = $this->seedProvider('Bureau Gating Co', [self::SITE_86], siren: '123456789');
        $client   = $this->authAs('bureau');

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

        self::assertArrayNotHasKey('siren', $data);
        self::assertArrayNotHasKey('accountNumber', $data);
        self::assertArrayNotHasKey('nTva', $data);
        self::assertArrayNotHasKey('tvaMode', $data);
        self::assertArrayNotHasKey('paymentType', $data);
        self::assertArrayNotHasKey('ribs', $data);
    }

    public function testComptaCanEditAccountingOnly(): void
    {
        $seed   = $this->seedProvider('Compta Cible');
        $client = $this->authAs('compta');

        // view
        $client->request('GET', '/api/providers', ['headers' => ['Accept' => self::LD]]);
        self::assertResponseStatusCodeSame(200);

        // PAS manage : creation refusee
        $client->request('POST', '/api/providers', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => $this->validMainPayload('Compta Post'),
        ]);
        self::assertResponseStatusCodeSame(403);

        // accounting.manage : edition onglet Comptabilite OK
        $client->request('PATCH', '/api/providers/'.$seed->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['siren' => '123456789'],
        ]);
        self::assertResponseStatusCodeSame(200);

        // PAS manage : edition onglet principal refusee (mode strict RG-3.15)
        $client->request('PATCH', '/api/providers/'.$seed->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['companyName' => 'Compta Renomme'],
        ]);
        self::assertResponseStatusCodeSame(403);

        // PAS archive : archivage refuse
        $client->request('PATCH', '/api/providers/'.$seed->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['isArchived' => true],
        ]);
        self::assertResponseStatusCodeSame(403);
    }

    public function testComptaDetailHasAccountingFields(): void
    {
        // Compta a accounting.view : siren + ribs presents dans le JSON.
        $provider = $this->seedProvider('Compta View Co', [self::SITE_86], siren: '987654321');
        $this->addRib($provider);
        $client = $this->authAs('compta');

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

        self::assertArrayHasKey('siren', $data);
        self::assertSame('987654321', $data['siren']);
        self::assertArrayHasKey('ribs', $data);
        self::assertNotEmpty($data['ribs']);
    }

    public function testCommercialeHasViewAndManageButNoAccountingNoArchive(): void
    {
        $seed   = $this->seedProvider('Commerciale Cible');
        $client = $this->authAs('commerciale');

        // view
        $client->request('GET', '/api/providers', ['headers' => ['Accept' => self::LD]]);
        self::assertResponseStatusCodeSame(200);

        // manage : creation OK
        $client->request('POST', '/api/providers', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => $this->validMainPayload('Commerciale Cree'),
        ]);
        self::assertResponseStatusCodeSame(201);

        // PAS accounting : edition onglet Comptabilite refusee
        $client->request('PATCH', '/api/providers/'.$seed->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['siren' => '123456789'],
        ]);
        self::assertResponseStatusCodeSame(403);

        // PAS archive : archivage refuse
        $client->request('PATCH', '/api/providers/'.$seed->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['isArchived' => true],
        ]);
        self::assertResponseStatusCodeSame(403);
    }

    public function testCommercialeDetailHasNoAccountingFields(): void
    {
        $provider = $this->seedProvider('Commerciale Gating Co', [self::SITE_86], siren: '123456789');
        $client   = $this->authAs('commerciale');

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

        self::assertArrayNotHasKey('siren', $data);
        self::assertArrayNotHasKey('accountNumber', $data);
        self::assertArrayNotHasKey('nTva', $data);
        self::assertArrayNotHasKey('tvaMode', $data);
        self::assertArrayNotHasKey('paymentType', $data);
        self::assertArrayNotHasKey('ribs', $data);
    }

    public function testUsineHasReadOnlyAccessScopedToItsSite(): void
    {
        // Usine a view (lecture seule), SANS manage / accounting / archive, et
        // SANS bypass_scope -> cloisonnee a son site courant (Chatellerault,
        // site 86, pose par ensureDemoUsers).
        $inScope = $this->seedProvider('Usine InScope', [self::SITE_86]);
        $client  = $this->authAs('usine');

        // view : liste OK (pas un 403 comme au M2)
        $client->request('GET', '/api/providers', ['headers' => ['Accept' => self::LD]]);
        self::assertResponseStatusCodeSame(200);

        // view : detail d'un prestataire de SON site OK
        $client->request('GET', '/api/providers/'.$inScope->getId(), ['headers' => ['Accept' => self::LD]]);
        self::assertResponseStatusCodeSame(200);

        // PAS manage : creation refusee
        $client->request('POST', '/api/providers', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => $this->validMainPayload('Usine Post'),
        ]);
        self::assertResponseStatusCodeSame(403);

        // PAS manage : edition onglet principal refusee
        $client->request('PATCH', '/api/providers/'.$inScope->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['companyName' => 'Renomme Par Usine'],
        ]);
        self::assertResponseStatusCodeSame(403);

        // PAS accounting : edition onglet Comptabilite refusee
        $client->request('PATCH', '/api/providers/'.$inScope->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['siren' => '123456789'],
        ]);
        self::assertResponseStatusCodeSame(403);

        // PAS archive : archivage refuse
        $client->request('PATCH', '/api/providers/'.$inScope->getId(), [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['isArchived' => true],
        ]);
        self::assertResponseStatusCodeSame(403);
    }

    public function testUsineCannotSeeProviderOutOfItsSite(): void
    {
        // Cloisonnement § 2.13 : un prestataire hors du site courant de l'Usine
        // (site 17, l'Usine est sur le site 86) -> 404 (ne pas reveler la ligne).
        $outOfScope = $this->seedProvider('Usine OutOfScope', [self::SITE_17]);
        $client     = $this->authAs('usine');

        $client->request('GET', '/api/providers/'.$outOfScope->getId(), ['headers' => ['Accept' => self::LD]]);
        self::assertResponseStatusCodeSame(404);
    }

    private function authAs(string $role): Client
    {
        return $this->authenticatedClient($role, self::PWD);
    }
}
