<?php

declare(strict_types=1);

namespace App\Tests\Integration\Jobs;

use App\Infrastructure\Jobs\JobLockRepository;
use App\Tests\Integration\Support\AppTestCase;
use DateTimeImmutable;
use DateTimeZone;

/**
 * Lock semantics tested directly against SQLite. Two scenarios that matter
 * for SPEC §4 (`tryAcquire`):
 *  1. Two concurrent acquires → one wins, one returns false.
 *  2. An expired lock from a crashed prior owner is reclaimed on next
 *     acquire (the delete-if-expired pre-step inside the transaction).
 */
final class JobLockRepositoryTest extends AppTestCase
{
    private JobLockRepository $locks;

    protected function setUp(): void
    {
        parent::setUp();
        $this->locks = new JobLockRepository($this->db);
    }

    public function testAcquireReleaseAcquireRoundTrip(): void
    {
        $now = new DateTimeImmutable('2026-04-29T00:00:00Z', new DateTimeZone('UTC'));
        $expires = $now->modify('+5 minutes');

        self::assertTrue($this->locks->tryAcquire('demo', $now, $expires, 'A'));
        $this->locks->release('demo', 'A');
        self::assertTrue($this->locks->tryAcquire('demo', $now, $expires, 'B'));
    }

    public function testSecondAcquireWhileHeldReturnsFalse(): void
    {
        $now = new DateTimeImmutable('2026-04-29T00:00:00Z', new DateTimeZone('UTC'));
        $expires = $now->modify('+5 minutes');

        self::assertTrue($this->locks->tryAcquire('demo', $now, $expires, 'A'));
        self::assertFalse($this->locks->tryAcquire('demo', $now, $expires, 'B'));
    }

    public function testExpiredLockReclaimedOnNextAcquire(): void
    {
        $past = new DateTimeImmutable('2026-04-29T00:00:00Z', new DateTimeZone('UTC'));
        $expires = $past->modify('+5 minutes');

        self::assertTrue($this->locks->tryAcquire('demo', $past, $expires, 'A'));

        // jump forward past expires_at — the next acquire must succeed
        // because the in-transaction delete sweeps the stale row first.
        $now = $past->modify('+1 hour');
        self::assertTrue($this->locks->tryAcquire('demo', $now, $now->modify('+5 minutes'), 'B'));

        // The lock is now held by B.
        $status = $this->locks->status('demo');
        self::assertNotNull($status);
        self::assertSame('B', $status['acquired_by']);
    }

    public function testReleaseWithDifferentOwnerIsNoOp(): void
    {
        $now = new DateTimeImmutable('2026-04-29T00:00:00Z', new DateTimeZone('UTC'));
        self::assertTrue($this->locks->tryAcquire('demo', $now, $now->modify('+5 minutes'), 'A'));

        // Defensive: B can't kick A out by releasing.
        $this->locks->release('demo', 'B');
        self::assertFalse($this->locks->tryAcquire('demo', $now, $now->modify('+5 minutes'), 'C'));
    }
}
