<?php

declare(strict_types=1);

namespace App\Tests\Module\Commercial\Api;

use App\Module\Commercial\Domain\Entity\Client as ClientEntity;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;

/**
 * Tests Audit + Timestampable/Blamable du repertoire clients (ERP-60).
 *
 * Couvre :
 *  - RG-1.27 : createdAt / createdBy figes au POST, updatedBy reflete bien
 *    l'auteur de la modification (POST admin puis PATCH par un autre user) ;
 *  - Audit (§ 6.1) : le RIB est `#[Auditable]` SANS `#[AuditIgnore]` sur iban /
 *    bic — ces champs sensibles DOIVENT donc apparaitre dans le diff audite
 *    (decision Matthieu, revue MR 29/05/2026).
 *
 * @internal
 */
final class ClientAuditTest extends AbstractCommercialApiTestCase
{
    private const string LD         = 'application/ld+json';
    private const string MERGE      = 'application/merge-patch+json';
    private const string RIB_TYPE   = 'commercial.ClientRib';
    private const string VALID_IBAN = 'FR1420041010050500013M02606';
    private const string VALID_BIC  = 'BNPAFRPPXXX';

    private ?Connection $auditConnection = null;

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

        /** @var Connection $conn */
        $conn                  = self::getContainer()->get('doctrine.dbal.audit_connection');
        $this->auditConnection = $conn;
    }

    protected function tearDown(): void
    {
        if (null !== $this->auditConnection) {
            $this->auditConnection->close();
        }
        parent::tearDown();
    }

    /**
     * RG-1.27 : createdAt / createdBy sont poses au POST puis figes ; updatedBy
     * suit l'auteur de la derniere modification. On cree en admin puis on
     * modifie avec un user `commercial.clients.manage` distinct : createdBy reste
     * l'admin, updatedBy devient le manager, createdAt ne bouge pas.
     */
    public function testCreatedFrozenAndUpdatedByReflectsModifier(): void
    {
        // 1. User modificateur (non-admin) cree AVANT le reboot de kernel induit
        //    par les clients authentifies suivants ; il est persiste en base.
        $manageCreds = $this->createUserWithPermission('commercial.clients.manage');

        // 2. Creation en admin (createdBy = admin).
        $admin = $this->createAdminClient();
        $cat   = $this->createCategory('SECTEUR');

        $created = $admin->request('POST', '/api/clients', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'companyName'  => 'Blamable Co',
                'categories'   => ['/api/categories/'.$cat->getId()],
            ],
        ])->toArray();
        self::assertResponseStatusCodeSame(201);
        $id          = (int) $created['id'];
        $createdAtTs = new DateTimeImmutable((string) $created['createdAt'])->getTimestamp();

        // 3. Modification par le manager (updatedBy = manager).
        $manage = $this->authenticatedClient($manageCreds['username'], $manageCreds['password']);
        $manage->request('PATCH', '/api/clients/'.$id, [
            'headers' => ['Content-Type' => self::MERGE],
            'json'    => ['companyName' => 'Blamable Renamed'],
        ]);
        self::assertResponseStatusCodeSame(200);

        // 4. Verification cote base (etat re-charge depuis la BDD).
        $em = $this->getEm();
        $em->clear();
        $reloaded = $em->getRepository(ClientEntity::class)->find($id);
        self::assertNotNull($reloaded);

        self::assertSame('admin', $reloaded->getCreatedBy()?->getUserIdentifier(), 'createdBy doit rester l\'admin createur.');
        self::assertSame(
            $manageCreds['username'],
            $reloaded->getUpdatedBy()?->getUserIdentifier(),
            'updatedBy doit refleter le dernier modificateur.',
        );
        self::assertSame($createdAtTs, $reloaded->getCreatedAt()?->getTimestamp(), 'createdAt doit etre fige au POST.');
        self::assertNotNull($reloaded->getUpdatedAt());
        self::assertGreaterThanOrEqual($createdAtTs, $reloaded->getUpdatedAt()->getTimestamp());
    }

    /**
     * Audit § 6.1 : la creation d'un RIB produit une ligne audit_log
     * `commercial.ClientRib` / `create` dont le snapshot contient iban et bic
     * (champs volontairement NON ignores).
     */
    public function testRibCreateAuditIncludesIbanAndBic(): void
    {
        $admin = $this->createAdminClient();
        $seed  = $this->seedClient('Rib Audit Host');

        $rib = $admin->request('POST', '/api/clients/'.$seed->getId().'/ribs', [
            'headers' => ['Content-Type' => self::LD],
            'json'    => [
                'label' => 'Compte audite',
                'bic'   => self::VALID_BIC,
                'iban'  => self::VALID_IBAN,
            ],
        ])->toArray();
        self::assertResponseStatusCodeSame(201);

        $rows = $this->auditConnection->fetchAllAssociative(
            'SELECT changes FROM audit_log '
            .'WHERE entity_type = :type AND entity_id = :id AND action = :action '
            .'ORDER BY performed_at DESC',
            ['type' => self::RIB_TYPE, 'id' => (string) $rib['id'], 'action' => 'create'],
        );

        self::assertGreaterThanOrEqual(1, count($rows), 'Un audit_log "create" doit etre genere pour le RIB.');

        /** @var array<string, mixed> $changes */
        $changes = json_decode((string) $rows[0]['changes'], true, flags: JSON_THROW_ON_ERROR);
        self::assertArrayHasKey('iban', $changes, 'iban doit figurer dans le diff audite (pas d\'AuditIgnore).');
        self::assertArrayHasKey('bic', $changes, 'bic doit figurer dans le diff audite (pas d\'AuditIgnore).');
        self::assertSame(self::VALID_IBAN, $changes['iban']);
        self::assertSame(self::VALID_BIC, $changes['bic']);
    }
}
