using System.Reflection;
using adas_core.Application.Services.Caching;
using adas_core.Domain.Models.AppSettings;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json;
using StackExchange.Redis;
using Options = Microsoft.Extensions.Options.Options;

namespace adas_core.Test.Services;

[TestFixture]
public class RedisServiceTest
{
    private record TestModel(string Name);

    private Mock<IDatabase> _mockDb = null!;
    private LockManagerService _lockMgr = null!;

    [SetUp]
    public void SetUp()
    {
        _mockDb = new Mock<IDatabase>();
        _lockMgr = new LockManagerService(
            new Mock<ILogger<LockManagerService>>().Object,
            new InMemoryLockProvider());
    }

    private RedisService CreateSut(CacheSettings? settings = null, bool redisAvailable = true)
    {
        var sut = new RedisService(
            Options.Create(settings ?? new CacheSettings()),
            new Mock<ILogger<RedisService>>().Object,
            _lockMgr);

        SetField(sut, "_database", _mockDb.Object);
        SetField(sut, "_isRedisAvailable", redisAvailable);

        return sut;
    }

    private static void SetField(object target, string name, object? value)
        => typeof(RedisService)
            .GetField(name, BindingFlags.NonPublic | BindingFlags.Instance)!
            .SetValue(target, value);

    private void SetupStringSetAsync()
        => _mockDb
            .Setup(db => db.StringSetAsync(
                It.IsAny<RedisKey>(), It.IsAny<RedisValue>(), It.IsAny<TimeSpan?>(),
                It.IsAny<bool>(), It.IsAny<When>(), It.IsAny<CommandFlags>()))
            .ReturnsAsync(true);

    private void SetupKeyExpire()
        => _mockDb
            .Setup(db => db.KeyExpire(
                It.IsAny<RedisKey>(), It.IsAny<TimeSpan?>(),
                It.IsAny<ExpireWhen>(), It.IsAny<CommandFlags>()))
            .Returns(true);

    #region TC-38
    [Test]
    public async Task GetOrSetObjectAsync_ExecutesFactory_WhenRedisUnavailable()
    {
        var sut = CreateSut(redisAvailable: false);
        var factoryInvoked = false;

        var result = await sut.GetOrSetObjectAsync<string>(
            "patients:latestObs:abc",
            () => { factoryInvoked = true; return Task.FromResult("result"); });

        Assert.That(factoryInvoked, Is.True);
        Assert.That(result, Is.EqualTo("result"));
        _mockDb.Verify(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()), Times.Never);
        _mockDb.Verify(db => db.StringSetAsync(It.IsAny<RedisKey>(), It.IsAny<RedisValue>(), It.IsAny<TimeSpan?>(), It.IsAny<bool>(), It.IsAny<When>(), It.IsAny<CommandFlags>()), Times.Never);
    }
    #endregion

    #region TC-39
    [Test]
    public async Task GetOrSetObjectAsync_ReturnsCachedObject_WhenRedisHit()
    {
        var cached = new TestModel("cached");

        _mockDb.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
               .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(cached));
        SetupKeyExpire();

        var sut = CreateSut();
        var factoryInvoked = false;

        var result = await sut.GetOrSetObjectAsync<TestModel>(
            "patients:latestObs:abc",
            () => { factoryInvoked = true; return Task.FromResult(new TestModel("fresh")); });

        Assert.That(factoryInvoked, Is.False);
        Assert.That(result?.Name, Is.EqualTo("cached"));
        _mockDb.Verify(db => db.StringSetAsync(It.IsAny<RedisKey>(), It.IsAny<RedisValue>(), It.IsAny<TimeSpan?>(), It.IsAny<bool>(), It.IsAny<When>(), It.IsAny<CommandFlags>()), Times.Never);
    }
    #endregion

    #region TC-40
    [Test]
    public async Task GetObjectAsync_CallsKeyExpire_WhenUpdateExpirationIsTrue()
    {
        var settings = new CacheSettings
        {
            Redis = new RedisSettings { Ttl = new TtlSettings { PatientsSeconds = 300 } }
        };

        _mockDb.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
               .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(new TestModel("hit")));
        SetupKeyExpire();

        var sut = CreateSut(settings);

        await sut.GetObjectAsync<TestModel>("patients:latestObs:abc", updateExpiration: true);

        _mockDb.Verify(
            db => db.KeyExpire(
                It.IsAny<RedisKey>(),
                It.Is<TimeSpan?>(t => t == TimeSpan.FromSeconds(300)),
                It.IsAny<ExpireWhen>(),
                It.IsAny<CommandFlags>()),
            Times.Once);
    }

    [Test]
    public async Task GetObjectAsync_DoesNotCallKeyExpire_WhenUpdateExpirationIsFalse()
    {
        _mockDb.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
               .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(new TestModel("hit")));

        var sut = CreateSut();

        await sut.GetObjectAsync<TestModel>("patients:latestObs:abc", updateExpiration: false);

        _mockDb.Verify(
            db => db.KeyExpire(It.IsAny<RedisKey>(), It.IsAny<TimeSpan?>(), It.IsAny<ExpireWhen>(), It.IsAny<CommandFlags>()),
            Times.Never);
    }
    #endregion

    #region TC-41
    [Test]
    public async Task GetOrSetObjectAsync_InvokesFactoryAndPersists_WhenCacheMiss()
    {
        var settings = new CacheSettings
        {
            Redis = new RedisSettings { Ttl = new TtlSettings { PatientsSeconds = 300, PatientObservationsSeconds = 300} }
        };

        _mockDb.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
               .ReturnsAsync(RedisValue.Null);
        SetupStringSetAsync();

        var sut = CreateSut(settings);
        var expected = new TestModel("fresh");
        var factoryInvoked = false;

        var result = await sut.GetOrSetObjectAsync<TestModel>(
            "patients:latestObs:abc",
            () => { factoryInvoked = true; return Task.FromResult(expected); });

        var expectedJson = JsonConvert.SerializeObject(expected);

        Assert.That(factoryInvoked, Is.True);
        Assert.That(result?.Name, Is.EqualTo("fresh"));
        _mockDb.Verify(
            db => db.StringSetAsync(
                It.IsAny<RedisKey>(),
                It.IsAny<RedisValue>(),
                It.Is<TimeSpan?>(t => t == TimeSpan.FromSeconds(300)),
                It.IsAny<bool>(),
                It.IsAny<When>(),
                It.IsAny<CommandFlags>()),
            Times.Once);
        It.Is<RedisValue>(v => v.Equals(expectedJson));
    }
    #endregion

    #region TC-42
    [Test]
    public async Task GetOrSetObjectAsync_DoesNotPersist_WhenFactoryReturnsNull()
    {
        _mockDb.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
               .ReturnsAsync(RedisValue.Null);

        var sut = CreateSut();

        var result = await sut.GetOrSetObjectAsync<TestModel?>(
            "patients:latestObs:abc",
            () => Task.FromResult<TestModel?>(null));

        Assert.That(result, Is.Null);
        _mockDb.Verify(
            db => db.StringSetAsync(It.IsAny<RedisKey>(), It.IsAny<RedisValue>(), It.IsAny<TimeSpan?>(), It.IsAny<bool>(), It.IsAny<When>(), It.IsAny<CommandFlags>()),
            Times.Never);
    }
    #endregion

    #region TC-43
    [Test]
    public async Task GetObjectAsync_ReturnsObject_AndSkipsKeyExpire_WhenUpdateExpirationFalse()
    {
        var expected = new TestModel("data");

        _mockDb.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
               .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(expected));

        var sut = CreateSut();

        var result = await sut.GetObjectAsync<TestModel>("patients:latestObs:abc", updateExpiration: false);

        Assert.That(result?.Name, Is.EqualTo("data"));
        _mockDb.Verify(
            db => db.KeyExpire(It.IsAny<RedisKey>(), It.IsAny<TimeSpan?>(), It.IsAny<ExpireWhen>(), It.IsAny<CommandFlags>()),
            Times.Never);
    }
    #endregion

    #region TC-44
    [Test]
    public async Task SetObjectAsync_SerializesToJson_AndPersistsWithEntityTtl()
    {
        var settings = new CacheSettings
        {
            Redis = new RedisSettings { Ttl = new TtlSettings { PatientsSeconds = 600 } }
        };

        SetupStringSetAsync();

        var sut = CreateSut(settings);
        var obj = new TestModel("save-me");
        var expectedJson = JsonConvert.SerializeObject(obj);

        await sut.SetObjectAsync("patients:latestObs:abc", obj, null, true);
        _mockDb.Verify(
            db => db.StringSetAsync(
                It.IsAny<RedisKey>(),
                It.IsAny<RedisValue>(),
                It.Is<TimeSpan?>(t => t == TimeSpan.FromSeconds(600)),
                It.IsAny<bool>(),
                It.IsAny<When>(),
                It.IsAny<CommandFlags>()),
            Times.Once);
        It.Is<RedisValue>(v => v.Equals(expectedJson));

    }
    #endregion
}
