using System.Collections.Concurrent;
using System.Reflection;
using adas_core.Application.Services.Caching;

namespace adas_core.Test.Services;

[TestFixture]
public class InMemoryLockProviderTest
{
    private InMemoryLockProvider _provider = null!;

    [SetUp]
    public void SetUp()
    {
        _provider = new InMemoryLockProvider();
    }

    private ConcurrentDictionary<string, SemaphoreSlim> GetLocks()
    {
        var field = typeof(InMemoryLockProvider)
            .GetField("_locks", BindingFlags.NonPublic | BindingFlags.Instance);
        return (ConcurrentDictionary<string, SemaphoreSlim>)field!.GetValue(_provider)!;
    }

    #region TC-50
    [Test]
    public async Task AcquireAsync_ReturnsTrue_AndCreatesEntryInLocksDictionary()
    {
        var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1));

        Assert.That(result, Is.True);

        var locks = GetLocks();
        Assert.That(locks.ContainsKey("key"), Is.True);
    }
    #endregion

    #region TC-51
    [Test]
    public async Task AcquireAsync_ReturnsFalse_WhenSemaphoreOccupiedAndTimeoutExpires()
    {
        await _provider.AcquireAsync("key", TimeSpan.FromSeconds(5));

        var result = await _provider.AcquireAsync("key", TimeSpan.FromMilliseconds(100));

        Assert.That(result, Is.False);

        await _provider.ReleaseAsync("key");

        var locks = GetLocks();
        Assert.That(locks["key"].CurrentCount, Is.EqualTo(1));
    }
    #endregion

    #region TC-52
    [Test]
    public async Task ReleaseAsync_MakesSemaphoreAvailableForNextAcquire()
    {
        await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1));
        await _provider.ReleaseAsync("key");

        var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1));

        Assert.That(result, Is.True);

        var locks = GetLocks();
        Assert.That(locks["key"].CurrentCount, Is.EqualTo(0));

        await _provider.ReleaseAsync("key");

        Assert.That(locks["key"].CurrentCount, Is.EqualTo(1));
    }
    #endregion

    #region TC-53
    [Test]
    public async Task ReleaseAsync_DoesNotThrow_WhenCalledWithoutPriorAcquire()
    {
        await _provider.ReleaseAsync("nonexistent-key");

        await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1));
        await _provider.ReleaseAsync("key");
        await _provider.ReleaseAsync("key");
    }
    #endregion

    #region TC-54
    [Test]
    public async Task AcquireAsync_IsThreadSafe_OnlyOneHolderAtATime_SingleSemaphorePerKey()
    {
        const string key = "same-key";
        const int threadCount = 10;
        var acquiredCount = 0;
        var currentHolders = 0;

        var tasks = Enumerable.Range(0, threadCount).Select(_ => Task.Run(async () =>
        {
            var acquired = await _provider.AcquireAsync(key, TimeSpan.FromSeconds(10));

            Assert.That(acquired, Is.True);

            var current = Interlocked.Increment(ref currentHolders);
            Assert.That(current, Is.EqualTo(1));

            Interlocked.Increment(ref acquiredCount);
            await Task.Delay(5);

            Interlocked.Decrement(ref currentHolders);
            await _provider.ReleaseAsync(key);
        })).ToArray();

        await Task.WhenAll(tasks);

        Assert.That(acquiredCount, Is.EqualTo(threadCount));

        var locks = GetLocks();
        Assert.That(locks.ContainsKey(key), Is.True);
        Assert.That(locks.Keys.Count(k => k == key), Is.EqualTo(1));
    }
    #endregion
}
