using System.Diagnostics;
using adas_core.Application.Services.Caching;
using Moq;
using StackExchange.Redis;

namespace adas_core.Test.Services;

[TestFixture]
public class RedisLockProviderTest
{
    private Mock<IDatabase> _mockDb = null!;
    private RedisLockProvider _provider = null!;

    [SetUp]
    public void SetUp()
    {
        _mockDb = new Mock<IDatabase>();
        _provider = new RedisLockProvider(() => _mockDb.Object);
    }

    #region TC-45
    [Test]
    public async Task AcquireAsync_ReturnsTrue_AndStoresToken_WhenRedisAcceptsNxSet()
    {
        _mockDb
            .Setup(db => db.StringSetAsync(
                It.IsAny<RedisKey>(),
                It.IsAny<RedisValue>(),
                It.IsAny<TimeSpan?>(),
                It.IsAny<When>()))
            .ReturnsAsync(true);

        _mockDb
            .Setup(db => db.ScriptEvaluateAsync(
                It.IsAny<string>(),
                It.IsAny<RedisKey[]?>(),
                It.IsAny<RedisValue[]?>(),
                It.IsAny<CommandFlags>()))
            .Returns(Task.FromResult<RedisResult>(null!));

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

        Assert.That(result, Is.True);

        _mockDb.Verify(db => db.StringSetAsync(
            It.Is<RedisKey>(k => k == (RedisKey)"lock:key"),
            It.IsAny<RedisValue>(),
            (TimeSpan?)TimeSpan.FromSeconds(5),
            When.NotExists), Times.Once());

        await _provider.ReleaseAsync("key");

        _mockDb.Verify(db => db.ScriptEvaluateAsync(
            It.IsAny<string>(),
            It.IsAny<RedisKey[]?>(),
            It.IsAny<RedisValue[]?>(),
            It.IsAny<CommandFlags>()), Times.Once());
    }
    #endregion

    #region TC-46
    [Test]
    public async Task AcquireAsync_RetriesWithDelays_UntilNxSucceeds()
    {
        _mockDb
            .SetupSequence(db => db.StringSetAsync(
                It.IsAny<RedisKey>(),
                It.IsAny<RedisValue>(),
                It.IsAny<TimeSpan?>(),
                It.IsAny<When>()))
            .ReturnsAsync(false)
            .ReturnsAsync(false)
            .ReturnsAsync(false)
            .ReturnsAsync(true);

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

        Assert.That(result, Is.True);

        _mockDb.Verify(db => db.StringSetAsync(
            It.IsAny<RedisKey>(),
            It.IsAny<RedisValue>(),
            It.IsAny<TimeSpan?>(),
            It.IsAny<When>()), Times.Exactly(4));

        Assert.That(sw.ElapsedMilliseconds, Is.GreaterThan(100));
    }
    #endregion

    #region TC-47
    [Test]
    public async Task AcquireAsync_ReturnsFalse_AfterTimeoutWithMultipleRetries()
    {
        _mockDb
            .Setup(db => db.StringSetAsync(
                It.IsAny<RedisKey>(),
                It.IsAny<RedisValue>(),
                It.IsAny<TimeSpan?>(),
                It.IsAny<When>()))
            .ReturnsAsync(false);

        var sw = Stopwatch.StartNew();
        var result = await _provider.AcquireAsync("key", TimeSpan.FromMilliseconds(200));
        sw.Stop();

        Assert.That(result, Is.False);
        Assert.That(sw.ElapsedMilliseconds, Is.GreaterThan(100));

        _mockDb.Verify(db => db.StringSetAsync(
            It.IsAny<RedisKey>(),
            It.IsAny<RedisValue>(),
            It.IsAny<TimeSpan?>(),
            It.IsAny<When>()), Times.AtLeast(2));
    }
    #endregion

    #region TC-48
    [Test]
    public async Task ReleaseAsync_CallsScriptEvaluateWithCorrectKeyAndToken()
    {
        RedisValue capturedToken = default;

        _mockDb
            .Setup(db => db.StringSetAsync(
                It.IsAny<RedisKey>(),
                It.IsAny<RedisValue>(),
                It.IsAny<TimeSpan?>(),
                It.IsAny<When>()))
            .Callback<RedisKey, RedisValue, TimeSpan?, When>(
                (_, v, _, _) => capturedToken = v)
            .ReturnsAsync(true);

        _mockDb
            .Setup(db => db.ScriptEvaluateAsync(
                It.IsAny<string>(),
                It.IsAny<RedisKey[]?>(),
                It.IsAny<RedisValue[]?>(),
                It.IsAny<CommandFlags>()))
            .Returns(Task.FromResult<RedisResult>(null!));

        await _provider.AcquireAsync("key", TimeSpan.FromSeconds(5));
        await _provider.ReleaseAsync("key");

        _mockDb.Verify(db => db.ScriptEvaluateAsync(
            It.IsAny<string>(),
            It.Is<RedisKey[]?>(keys => keys != null && keys[0] == (RedisKey)"lock:key"),
            It.Is<RedisValue[]?>(vals => vals != null && vals[0] == capturedToken),
            It.IsAny<CommandFlags>()), Times.Once());
    }
    #endregion

    #region TC-49
    [Test]
    public async Task ReleaseAsync_IsIdempotent_WhenCalledWithoutPriorAcquire()
    {
        _mockDb
            .Setup(db => db.ScriptEvaluateAsync(
                It.IsAny<string>(),
                It.IsAny<RedisKey[]?>(),
                It.IsAny<RedisValue[]?>(),
                It.IsAny<CommandFlags>()))
            .Returns(Task.FromResult<RedisResult>(null!));

        await _provider.ReleaseAsync("key");

        _mockDb.Verify(db => db.ScriptEvaluateAsync(
            It.IsAny<string>(),
            It.IsAny<RedisKey[]?>(),
            It.IsAny<RedisValue[]?>(),
            It.IsAny<CommandFlags>()), Times.Never());
    }
    #endregion
}
