<?php

declare(strict_types=1);

namespace App\Tests\Module\Core\Infrastructure\Audit;

use App\Module\Core\Infrastructure\Audit\AuditLogWriter;
use App\Module\Core\Infrastructure\Audit\RequestIdProvider;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\User\InMemoryUser;

/**
 * Tests unitaires de l'AuditLogWriter.
 *
 * Verifie les invariants critiques :
 *  - filtrage des cles sensibles (defense-in-depth par rapport a #[AuditIgnore]) ;
 *  - utilisation du username courant ou "system" en CLI ;
 *  - captation IP + request_id si requete HTTP presente ;
 *  - generation d'un UUID v7 (tri chronologique implicite en PK).
 *
 * Aucune BDD : la connexion DBAL est mockee pour capturer l'insert.
 *
 * @internal
 */
#[AllowMockObjectsWithoutExpectations]
final class AuditLogWriterTest extends TestCase
{
    /**
     * @var null|array{0: string, 1: array<string, mixed>, 2: array<string, mixed>}
     *
     * Capture de l'appel `insert()` : [$table, $data, $types]
     */
    private ?array $capturedInsert = null;

    private Connection $connection;

    private RequestStack $requestStack;

    private RequestIdProvider $requestIdProvider;

    protected function setUp(): void
    {
        $this->capturedInsert = null;

        $this->connection = $this->createMock(Connection::class);
        $this->connection
            ->method('insert')
            ->willReturnCallback(function (string $table, array $data, array $types = []): int {
                $this->capturedInsert = [$table, $data, $types];

                return 1;
            })
        ;

        $this->requestStack      = new RequestStack();
        $this->requestIdProvider = new RequestIdProvider();
    }

    public function testLogsCreateWithAuthenticatedUser(): void
    {
        $security = $this->buildSecurityWithUser('alice');
        $writer   = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider);

        $writer->log('core.User', '42', 'create', ['username' => 'alice']);

        $this->assertNotNull($this->capturedInsert);
        [$table, $data] = $this->capturedInsert;
        $this->assertSame('audit_log', $table);
        $this->assertSame('core.User', $data['entity_type']);
        $this->assertSame('42', $data['entity_id']);
        $this->assertSame('create', $data['action']);
        $this->assertSame(['username' => 'alice'], $data['changes']);
        $this->assertSame('alice', $data['performed_by']);
    }

    public function testUsesSystemWhenNoAuthenticatedUser(): void
    {
        $security = $this->buildSecurityWithUser(null);
        $writer   = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider);

        $writer->log('core.User', '1', 'update', ['isAdmin' => ['old' => false, 'new' => true]]);

        $this->assertSame('system', $this->capturedInsert[1]['performed_by']);
    }

    public function testStripsSensitiveKeys(): void
    {
        $security = $this->buildSecurityWithUser('alice');
        $writer   = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider);

        $writer->log('core.User', '1', 'create', [
            'username'      => 'bob',
            'password'      => 'topsecrethash',
            'plainPassword' => 'clear',
            'token'         => 'abc',
            'secret'        => 'xyz',
            'email'         => 'bob@example.com',
        ]);

        $changes = $this->capturedInsert[1]['changes'];
        $this->assertArrayNotHasKey('password', $changes);
        $this->assertArrayNotHasKey('plainPassword', $changes);
        $this->assertArrayNotHasKey('token', $changes);
        $this->assertArrayNotHasKey('secret', $changes);
        $this->assertSame('bob', $changes['username']);
        $this->assertSame('bob@example.com', $changes['email']);
    }

    public function testStripsSensitiveKeysRecursively(): void
    {
        // Defense-in-depth : un appelant direct peut passer un payload
        // imbrique (ex: relations embarquees). Les cles sensibles doivent
        // etre supprimees a tous les niveaux de profondeur.
        $security = $this->buildSecurityWithUser('alice');
        $writer   = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider);

        $writer->log('core.User', '1', 'create', [
            'username' => 'bob',
            'profile'  => [
                'email'    => 'bob@example.com',
                'password' => 'leaked_in_nested',
                'nested'   => [
                    'token'    => 'should_be_stripped',
                    'harmless' => 'kept',
                ],
            ],
        ]);

        $changes = $this->capturedInsert[1]['changes'];
        $this->assertArrayNotHasKey('password', $changes['profile']);
        $this->assertArrayNotHasKey('token', $changes['profile']['nested']);
        $this->assertSame('kept', $changes['profile']['nested']['harmless']);
        $this->assertSame('bob@example.com', $changes['profile']['email']);
    }

    public function testCapturesIpAddressWhenRequestPresent(): void
    {
        $request = Request::create('/api/users', 'POST');
        $request->server->set('REMOTE_ADDR', '203.0.113.42');
        $this->requestStack->push($request);

        $security = $this->buildSecurityWithUser('alice');
        $writer   = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider);

        $writer->log('core.User', '1', 'create', []);

        $this->assertSame('203.0.113.42', $this->capturedInsert[1]['ip_address']);
    }

    public function testIpAddressNullInCli(): void
    {
        $security = $this->buildSecurityWithUser(null);
        $writer   = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider);

        $writer->log('core.User', '1', 'create', []);

        $this->assertNull($this->capturedInsert[1]['ip_address']);
        $this->assertNull($this->capturedInsert[1]['request_id']);
    }

    public function testGeneratesUuidV7PrimaryKey(): void
    {
        $security = $this->buildSecurityWithUser('alice');
        $writer   = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider);

        $writer->log('core.User', '1', 'create', []);

        $id = $this->capturedInsert[1]['id'];
        // UUID v7 : le 13e caractere (apres les tirets) vaut "7".
        // Format : xxxxxxxx-xxxx-7xxx-xxxx-xxxxxxxxxxxx
        $this->assertMatchesRegularExpression(
            '/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i',
            $id
        );
    }

    private function buildSecurityWithUser(?string $username): Security
    {
        $security = $this->createMock(Security::class);
        $user     = null !== $username ? new InMemoryUser($username, 'pwd') : null;
        $security->method('getUser')->willReturn($user);

        return $security;
    }
}
