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!;

    /// <summary>
    /// Initializes the test environment by creating a mock <see cref="IDatabase"/> and instantiating a <see cref="RedisLockProvider"/> configured to use the mocked database.
    /// </summary>
    [SetUp]
        public void SetUp()
        {
            _mockDb = new Mock<IDatabase>();
            _provider = new RedisLockProvider(() => _mockDb.Object);
        }

    #region TC-45
    /// <summary>
    /// Verifies that <c>AcquireAsync</c> returns <c>true</c>, stores the lock token under the <c>lock:</c> prefixed key, applies the configured TTL, and uses the <c>When.NotExists</c> flag when Redis accepts the NX SET operation. Also asserts that a subsequent <c>ReleaseAsync</c> invokes the Redis release script.
    /// </summary>
    [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
    /// <summary>
    /// Verifies that <c>AcquireAsync</c> retries the underlying Nx-style <c>StringSetAsync</c> call with delays
    /// between attempts when the operation initially fails, continuing until it succeeds.
    /// Asserts the call is retried the expected number of times and that the elapsed time confirms
    /// delays were actually applied between attempts.
    /// </summary>
    [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
    /// <summary>
    /// Verifies that AcquireAsync returns <c>false</c> and performs multiple retry attempts when the underlying lock acquisition operation consistently fails within the specified timeout.
    /// </summary>
    [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
    /// <summary>
    /// Verifies that releasing a lock invokes the Lua script evaluation with the correct lock key (prefixed with "lock:") and the token previously captured during lock acquisition.
    /// </summary>
    [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
    /// <summary>
    /// Verifies that ReleaseAsync is idempotent when invoked without a prior acquire operation,
    /// ensuring that no script evaluation is performed on the underlying database in this scenario.
    /// </summary>
    [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
}
