<?php

declare(strict_types=1);

use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;

uses(DatabaseTransactions::class);

/**
 * Race-condition reproduction test for audit_chain_hash() trigger.
 *
 * Two tests:
 *  1. pcntl_fork-based concurrent INSERT test — skipped on Windows (no pcntl).
 *     Expected: FAIL before migration (concurrent inserts branch the chain),
 *               PASS after migration (advisory lock serialises inserts).
 *
 *  2. pg_locks advisory lock presence test — runs on Windows.
 *     Asserts that within an INSERT transaction the advisory lock key derived
 *     from the partition OID is held (proves the lock is actually acquired).
 */
it(
    'audit_chain_hash trigger preserves sequential chain under concurrent INSERTs',
    function (): void {
        $tenant = Tenant::factory()->create();
        DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id);

        $startCount = DB::table('activity_log')
            ->where('tenant_id', $tenant->id)
            ->count();

        // Spawn 5 concurrent processes each inserting into activity_log for the same tenant.
        // Without advisory lock, concurrent reads of prev_hash return the same value
        // → multiple rows hash to the same prev → chain branch → validator fails.
        $pids = [];
        for ($i = 0; $i < 5; $i++) {
            $pid = pcntl_fork();
            if ($pid === 0) {
                // Child: own DB connection, own transaction
                DB::reconnect();
                DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id);
                DB::table('activity_log')->insert([
                    'tenant_id' => $tenant->id,
                    'deal_id' => 1, // activity_log.deal_id NOT NULL (per-deal audit chain); без FK
                    'event' => 'deal.created',
                    'context' => json_encode(['worker' => $i]),
                    'created_at' => now(),
                ]);
                exit(0);
            }
            $pids[] = $pid;
        }
        foreach ($pids as $pid) {
            pcntl_waitpid($pid, $status);
        }

        $rows = DB::table('activity_log')
            ->where('tenant_id', $tenant->id)
            ->orderBy('id')
            ->get(['id', 'log_hash']);

        expect($rows->count())->toBe($startCount + 5);

        // Run the chain validator; it should find no mismatches (after migration).
        $exitCode = $this->artisan('audit:verify-chains')->run();
        expect($exitCode)->toBe(0);
    }
)->skip(! function_exists('pcntl_fork'), 'pcntl required for race-condition test (not available on Windows)');

it('audit_chain_hash holds pg_advisory_xact_lock on the partition OID during INSERT', function (): void {
    $tenant = Tenant::factory()->create();
    DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id);

    // Resolve the OID of the current-month activity_log partition (or parent).
    $partitionName = 'activity_log_y'.date('Y').'_m'.date('m');
    $oid = DB::selectOne(
        "SELECT COALESCE(
            (SELECT c.oid FROM pg_class c WHERE c.relname = ?),
            (SELECT c.oid FROM pg_class c WHERE c.relname = 'activity_log')
        ) AS oid",
        [$partitionName]
    )?->oid;

    expect($oid)->not->toBeNull('Could not resolve partition/parent OID');

    // Compute the lock key using the same formula as the trigger:
    //   ('x' || lpad(to_hex(TG_RELID::int), 16, '0'))::bit(64)::bigint
    $lockKeyRow = DB::selectOne(
        "SELECT ('x' || lpad(to_hex(?::int), 16, '0'))::bit(64)::bigint AS lock_key",
        [(int) $oid]
    );
    $lockKey = $lockKeyRow?->lock_key;
    expect($lockKey)->not->toBeNull();

    // Wrap an INSERT in a transaction and check pg_locks DURING that transaction.
    $lockHeld = false;
    DB::transaction(function () use ($tenant, $lockKey, &$lockHeld): void {
        DB::table('activity_log')->insert([
            'tenant_id' => $tenant->id,
            'deal_id' => 1, // activity_log.deal_id NOT NULL (per-deal audit chain); без FK
            'event' => 'deal.created',
            'context' => json_encode(['test' => 'advisory_lock_check']),
            'created_at' => now(),
        ]);

        // pg_advisory_xact_lock(bigint) → pg_locks: classid = старшие 32 бита ключа,
        // objid = младшие 32. Декомпозицию считаем в PHP: bind-параметр в SQL-сдвиге
        // (? >> 32) не сдвигается корректно (PDO передаёт значение так, что сдвиг
        // даёт сам ключ, а не 0) → classid не совпадал и блокировка «не находилась».
        $classid = (int) (((int) $lockKey >> 32) & 0xFFFFFFFF);
        $objid = (int) ((int) $lockKey & 0xFFFFFFFF);
        $held = DB::selectOne(
            'SELECT EXISTS (
                SELECT 1
                FROM pg_locks
                WHERE locktype = \'advisory\'
                  AND classid  = ?
                  AND objid    = ?
                  AND granted  = true
                  AND pid      = pg_backend_pid()
             ) AS held',
            [$classid, $objid]
        );
        $lockHeld = (bool) ($held->held ?? false);
    });

    expect($lockHeld)->toBeTrue(
        'pg_advisory_xact_lock was not observed in pg_locks during the INSERT transaction. '
        .'This means the migration has not been applied or the lock key formula is wrong.'
    );
});
