namespace ScadaLink.DeploymentManager.Tests;

/// <summary>
/// WP-3: Tests for per-instance operation lock.
/// </summary>
public class OperationLockManagerTests
{
    private readonly OperationLockManager _lockManager = new();

    [Fact]
    public async Task AcquireAsync_ReturnsDisposable()
    {
        using var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
        Assert.NotNull(lockHandle);
    }

    [Fact]
    public async Task AcquireAsync_SameInstance_BlocksSecondCaller()
    {
        using var firstLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));

        // Second acquire should time out
        await Assert.ThrowsAsync<TimeoutException>(() =>
            _lockManager.AcquireAsync("inst1", TimeSpan.FromMilliseconds(50)));
    }

    [Fact]
    public async Task AcquireAsync_DifferentInstances_BothSucceed()
    {
        using var lock1 = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
        using var lock2 = await _lockManager.AcquireAsync("inst2", TimeSpan.FromSeconds(5));

        Assert.NotNull(lock1);
        Assert.NotNull(lock2);
    }

    [Fact]
    public async Task AcquireAsync_AfterRelease_CanReacquire()
    {
        var firstLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
        firstLock.Dispose();

        // Should succeed now
        using var secondLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
        Assert.NotNull(secondLock);
    }

    [Fact]
    public async Task IsLocked_ReturnsTrueWhileLocked()
    {
        Assert.False(_lockManager.IsLocked("inst1"));

        using var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
        Assert.True(_lockManager.IsLocked("inst1"));
    }

    [Fact]
    public async Task IsLocked_ReturnsFalseAfterRelease()
    {
        var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
        lockHandle.Dispose();

        Assert.False(_lockManager.IsLocked("inst1"));
    }

    [Fact]
    public async Task AcquireAsync_DoubleDispose_DoesNotThrow()
    {
        var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
        lockHandle.Dispose();
        lockHandle.Dispose(); // Should not throw
    }

    [Fact]
    public async Task AcquireAsync_CancellationToken_Respected()
    {
        using var firstLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(30));

        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
            _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(30), cts.Token));
    }

    [Fact]
    public async Task AcquireAsync_ConcurrentDifferentInstances_AllSucceed()
    {
        var tasks = Enumerable.Range(0, 10).Select(async i =>
        {
            using var lockHandle = await _lockManager.AcquireAsync($"inst{i}", TimeSpan.FromSeconds(5));
            await Task.Delay(10);
        });

        await Task.WhenAll(tasks);
    }

    // ── DeploymentManager-005: semaphore must not leak ──

    [Fact]
    public async Task AcquireAsync_ReleasedLock_RemovesSemaphoreEntry()
    {
        // A semaphore that is created, used, and fully released must not be
        // retained — otherwise every distinct instance name leaks a
        // SemaphoreSlim (a kernel handle) for the life of the process.
        using (await _lockManager.AcquireAsync("transient-inst", TimeSpan.FromSeconds(5)))
        {
            Assert.Equal(1, _lockManager.TrackedLockCount);
        }

        // After release, the entry is reclaimed.
        Assert.Equal(0, _lockManager.TrackedLockCount);
    }

    [Fact]
    public async Task AcquireAsync_ManyDistinctInstances_DoesNotAccumulateSemaphores()
    {
        // Simulates the long-running central process: many instances are
        // deployed/disabled over time. Their semaphores must be reclaimed.
        for (var i = 0; i < 100; i++)
        {
            using var handle = await _lockManager.AcquireAsync($"churn-{i}", TimeSpan.FromSeconds(5));
        }

        Assert.Equal(0, _lockManager.TrackedLockCount);
    }

    [Fact]
    public async Task AcquireAsync_ContendedLock_KeepsSemaphoreUntilLastReleaseThenReclaims()
    {
        // While a second caller is waiting, the semaphore must survive the
        // first release; only when the last holder releases is it reclaimed.
        var first = await _lockManager.AcquireAsync("contended", TimeSpan.FromSeconds(5));

        var secondAcquire = _lockManager.AcquireAsync("contended", TimeSpan.FromSeconds(5));

        first.Dispose(); // hands the lock to the waiter; entry must NOT be removed
        var second = await secondAcquire;

        Assert.Equal(1, _lockManager.TrackedLockCount);
        second.Dispose();

        Assert.Equal(0, _lockManager.TrackedLockCount);
    }
}
