<?php

declare(strict_types=1);

namespace App\Tests\Integration\Audit;

use App\Domain\Auth\Role;
use App\Domain\Auth\TokenKind;
use App\Tests\Integration\Support\AppTestCase;

/**
 * SEC_REVIEW F4: every admin write must be transactional with its
 * audit emit. If the audit insert fails for any reason (DB error,
 * lock timeout, JSON encoding failure), the originating mutation
 * MUST roll back so we can never end up with state changes that have
 * no audit row attributing them.
 *
 * The forcing function here is dropping the `audit_log` table so that
 * any subsequent `INSERT INTO audit_log` raises a SQL error. The
 * emitter's `emitOrThrow()` propagates, the enclosing
 * `Connection::transactional()` rolls back, and the target table
 * sees zero new rows.
 */
final class AuditRollbackTest extends AppTestCase
{
    public function testManualBlockCreateRollsBackWhenAuditInsertFails(): void
    {
        $token = $this->createToken(TokenKind::Admin, Role::Admin);

        // Force every emitOrThrow() to fail with a SQL error.
        $this->db->executeStatement('DROP TABLE audit_log');

        $before = (int) $this->db->fetchOne('SELECT COUNT(*) FROM manual_blocks');

        $resp = $this->request(
            'POST',
            '/api/v1/admin/manual-blocks',
            [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json',
            ],
            (string) json_encode(['kind' => 'ip', 'ip' => '203.0.113.99', 'reason' => 'rollback-test']),
        );

        self::assertGreaterThanOrEqual(500, $resp->getStatusCode(), 'request should fail loudly');

        $after = (int) $this->db->fetchOne('SELECT COUNT(*) FROM manual_blocks');
        self::assertSame($before, $after, 'mutation must roll back when audit emit fails');
    }

    public function testReporterCreateRollsBackWhenAuditInsertFails(): void
    {
        $token = $this->createToken(TokenKind::Admin, Role::Admin);
        $this->db->executeStatement('DROP TABLE audit_log');

        $before = (int) $this->db->fetchOne('SELECT COUNT(*) FROM reporters');

        $resp = $this->request(
            'POST',
            '/api/v1/admin/reporters',
            [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json',
            ],
            (string) json_encode(['name' => 'rollback-test-rep']),
        );

        self::assertGreaterThanOrEqual(500, $resp->getStatusCode());

        $after = (int) $this->db->fetchOne('SELECT COUNT(*) FROM reporters');
        self::assertSame($before, $after, 'reporter row must not exist if audit emit fails');
    }

    public function testAllowlistCreateRollsBackWhenAuditInsertFails(): void
    {
        $token = $this->createToken(TokenKind::Admin, Role::Admin);
        $this->db->executeStatement('DROP TABLE audit_log');

        $before = (int) $this->db->fetchOne('SELECT COUNT(*) FROM allowlist');

        $resp = $this->request(
            'POST',
            '/api/v1/admin/allowlist',
            [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json',
            ],
            (string) json_encode(['kind' => 'ip', 'ip' => '203.0.113.77', 'reason' => 'rollback-test']),
        );

        self::assertGreaterThanOrEqual(500, $resp->getStatusCode());

        $after = (int) $this->db->fetchOne('SELECT COUNT(*) FROM allowlist');
        self::assertSame($before, $after);
    }

    public function testCategoryCreateRollsBackWhenAuditInsertFails(): void
    {
        $token = $this->createToken(TokenKind::Admin, Role::Admin);
        $this->db->executeStatement('DROP TABLE audit_log');

        $before = (int) $this->db->fetchOne('SELECT COUNT(*) FROM categories');

        $resp = $this->request(
            'POST',
            '/api/v1/admin/categories',
            [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json',
            ],
            (string) json_encode([
                'slug' => 'rollback_test',
                'name' => 'Rollback Test',
                'decay_function' => 'linear',
                'decay_param' => 30,
            ]),
        );

        self::assertGreaterThanOrEqual(500, $resp->getStatusCode());

        $after = (int) $this->db->fetchOne('SELECT COUNT(*) FROM categories');
        self::assertSame($before, $after, 'category row must not exist if audit emit fails');
    }

    public function testUpsertLocalRollsBackWhenAuditInsertFails(): void
    {
        // SEC_REVIEW F5: user.created emit is transactional with the
        // user row insert. Drop audit_log on a fresh DB (no local user
        // exists yet) → first upsert-local fails → users table stays
        // empty.
        $token = $this->createToken(TokenKind::Service);
        $this->db->executeStatement('DROP TABLE audit_log');

        $before = (int) $this->db->fetchOne('SELECT COUNT(*) FROM users');

        $resp = $this->request(
            'POST',
            '/api/v1/auth/users/upsert-local',
            [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json',
            ],
            (string) json_encode(['username' => 'admin']),
        );

        self::assertGreaterThanOrEqual(500, $resp->getStatusCode());

        $after = (int) $this->db->fetchOne('SELECT COUNT(*) FROM users');
        self::assertSame($before, $after, 'user.created must be transactional with the user insert');
    }
}
