<?php
/*
 * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * See the LICENSE file in the project root for the full license text.
 */

declare(strict_types=1);

namespace App\Tests\Services;

use App\Services\AuditLogger;
use App\Tests\TestCase;

final class AuditLoggerTest extends TestCase
{
    public function testRealUpdateProducesExactlyOneRow(): void
    {
        $pdo    = $this->makeDb();
        $logger = new AuditLogger($pdo);

        $id = $logger->record(
            action:     'UPDATE',
            entityType: 'worker',
            entityId:   1,
            before:     ['name' => 'Alice', 'is_active' => 1],
            after:      ['name' => 'Alice Cooper', 'is_active' => 1],
        );

        $this->assertIsInt($id);
        $this->assertSame(1, (int) $pdo->query('SELECT COUNT(*) FROM audit_log')->fetchColumn());
    }

    public function testNoopUpdateWritesNoRow(): void
    {
        $pdo    = $this->makeDb();
        $logger = new AuditLogger($pdo);

        $snap = ['name' => 'Alice', 'is_active' => 1, 'default_rtb' => 0.1];

        $result = $logger->record(
            action:     'UPDATE',
            entityType: 'worker',
            entityId:   1,
            before:     $snap,
            after:      $snap,
        );

        $this->assertNull($result);
        $this->assertSame(0, (int) $pdo->query('SELECT COUNT(*) FROM audit_log')->fetchColumn());
    }

    public function testNoopUpdateIsKeyOrderInsensitive(): void
    {
        $pdo    = $this->makeDb();
        $logger = new AuditLogger($pdo);

        $before = ['name' => 'Alice', 'is_active' => 1];
        $after  = ['is_active' => 1, 'name' => 'Alice']; // reordered keys

        $result = $logger->record('UPDATE', 'worker', 1, $before, $after);
        $this->assertNull($result);
        $this->assertSame(0, (int) $pdo->query('SELECT COUNT(*) FROM audit_log')->fetchColumn());
    }

    public function testCreateActionAlwaysWritesEvenWhenBeforeEqualsAfter(): void
    {
        // The no-op rule applies to UPDATE only. CREATE/DELETE always write.
        $pdo    = $this->makeDb();
        $logger = new AuditLogger($pdo);

        $logger->record(
            action:     'CREATE',
            entityType: 'worker',
            entityId:   1,
            before:     null,
            after:      ['name' => 'Alice'],
        );

        $this->assertSame(1, (int) $pdo->query('SELECT COUNT(*) FROM audit_log')->fetchColumn());
    }

    public function testDeleteCarriesBeforeJsonAndNullAfterJson(): void
    {
        $pdo    = $this->makeDb();
        $logger = new AuditLogger($pdo);

        $logger->record(
            action:     'DELETE',
            entityType: 'worker',
            entityId:   42,
            before:     ['id' => 42, 'name' => 'Bob'],
            after:      null,
            userId:     7,
            userEmail:  'admin@x',
            ipAddress:  '1.2.3.4',
            userAgent:  'test-ua',
        );

        $row = $pdo->query('SELECT * FROM audit_log LIMIT 1')->fetch();
        $this->assertSame('DELETE', $row['action']);
        $this->assertSame('worker', $row['entity_type']);
        $this->assertSame(42, (int) $row['entity_id']);
        $this->assertSame(7,  (int) $row['user_id']);
        $this->assertSame('admin@x', $row['user_email']);
        $this->assertSame('1.2.3.4', $row['ip_address']);
        $this->assertSame('test-ua', $row['user_agent']);
        $this->assertIsString($row['before_json']);
        $this->assertSame('{"id":42,"name":"Bob"}', $row['before_json']);
        $this->assertNull($row['after_json']);
    }

    public function testLoginEventWritesWithNullBeforeAndAfter(): void
    {
        // LOGIN events are not mutations and carry no before/after.
        $pdo    = $this->makeDb();
        $logger = new AuditLogger($pdo);

        $logger->record(
            action:     'LOGIN',
            entityType: 'user',
            entityId:   1,
            before:     null,
            after:      null,
        );

        $row = $pdo->query('SELECT * FROM audit_log LIMIT 1')->fetch();
        $this->assertSame('LOGIN', $row['action']);
        $this->assertNull($row['before_json']);
        $this->assertNull($row['after_json']);
    }

    public function testLoginFailedSupportsNullUser(): void
    {
        $pdo    = $this->makeDb();
        $logger = new AuditLogger($pdo);

        $logger->record(
            action:     'LOGIN_FAILED',
            entityType: 'user',
            entityId:   null,
            before:     null,
            after:      ['reason' => 'bad_signature'],
        );

        $row = $pdo->query('SELECT * FROM audit_log LIMIT 1')->fetch();
        $this->assertSame('LOGIN_FAILED', $row['action']);
        $this->assertNull($row['entity_id']);
        $this->assertNull($row['user_id']);
        $this->assertSame('{"reason":"bad_signature"}', $row['after_json']);
    }

    public function testOccurredAtIsIsoUtc(): void
    {
        $pdo    = $this->makeDb();
        $logger = new AuditLogger($pdo);
        $logger->record('CREATE', 'worker', 1, null, ['name' => 'Alice']);

        $ts = (string) $pdo->query('SELECT occurred_at FROM audit_log LIMIT 1')->fetchColumn();
        $this->assertMatchesRegularExpression(
            '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/',
            $ts,
        );
    }
}
