<?php

declare(strict_types=1);

namespace App\Tests\Functional\Module\Core;

use App\Module\Core\Domain\Entity\User;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

/**
 * @internal
 */
final class AuditListenerTest extends KernelTestCase
{
    private EntityManagerInterface $em;
    private Connection $auditConnection;

    protected function setUp(): void
    {
        self::bootKernel();
        $container             = self::getContainer();
        $this->em              = $container->get(EntityManagerInterface::class);
        $this->auditConnection = $container->get('doctrine.dbal.audit_connection');
        // Clean slate for deterministic assertions: these tests are not wrapped
        // in a rolled-back transaction, so remove any leftover rows from a
        // previous run before each test.
        $this->em->getConnection()->executeStatement("DELETE FROM \"user\" WHERE username LIKE 'audit\\_%'");
        $this->auditConnection->executeStatement('DELETE FROM audit_log');
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        unset($this->em, $this->auditConnection);
    }

    public function testCreateUserIsAudited(): void
    {
        $user = $this->makeUser('audit_create_user');
        $this->em->persist($user);
        $this->em->flush();

        $rows = $this->fetchLogs('core.User', (string) $user->getId());
        self::assertCount(1, $rows);
        self::assertSame('create', $rows[0]['action']);
        $changes = json_decode((string) $rows[0]['changes'], true);
        self::assertArrayHasKey('username', $changes);
        self::assertArrayNotHasKey('password', $changes, 'password must be excluded via #[AuditIgnore]');
        self::assertArrayNotHasKey('apiToken', $changes, 'apiToken must be excluded via #[AuditIgnore]');
    }

    public function testUpdateUserIsAuditedWithDiff(): void
    {
        $user = $this->makeUser('audit_update_user');
        $this->em->persist($user);
        $this->em->flush();
        $this->auditConnection->executeStatement('DELETE FROM audit_log');

        $user->setFirstName('Changed');
        $this->em->flush();

        $rows = $this->fetchLogs('core.User', (string) $user->getId());
        self::assertCount(1, $rows);
        self::assertSame('update', $rows[0]['action']);
        $changes = json_decode((string) $rows[0]['changes'], true);
        self::assertArrayHasKey('firstName', $changes);
        self::assertSame('Changed', $changes['firstName']['new']);
    }

    public function testDeleteUserIsAudited(): void
    {
        $user = $this->makeUser('audit_delete_user');
        $this->em->persist($user);
        $this->em->flush();
        $id = (string) $user->getId();
        $this->auditConnection->executeStatement('DELETE FROM audit_log');

        $this->em->remove($user);
        $this->em->flush();

        $rows = $this->fetchLogs('core.User', $id);
        self::assertCount(1, $rows);
        self::assertSame('delete', $rows[0]['action']);
    }

    private function makeUser(string $username): User
    {
        $user = new User();
        $user->setUsername($username);
        $user->setPassword('hashed-secret');
        $user->setRoles(['ROLE_USER']);

        return $user;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function fetchLogs(string $entityType, string $entityId): array
    {
        return $this->auditConnection->fetchAllAssociative(
            'SELECT action, changes FROM audit_log WHERE entity_type = :t AND entity_id = :id ORDER BY performed_at ASC',
            ['t' => $entityType, 'id' => $entityId],
        );
    }
}
