using adas_core.Application.Services.Caching;
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Moq;

namespace adas_core.Test.Services;




/// <summary>
/// Provides a fake implementation of the <see cref="ILockProvider"/> interface, typically used as a test double or non-functional placeholder.
/// </summary>
public class FakeLockProvider : ILockProvider
{
    /// <summary>
    /// Asynchronously attempts to acquire a resource identified by the specified key within the given timeout.
    /// This implementation always reports a successful acquisition by returning <c>true</c>.
    /// </summary>
    /// <param name="key">The identifier of the resource to acquire.</param>
    /// <param name="timeout">The maximum time to wait for the resource to become available.</param>
    /// <returns>A <see cref="Task{TResult}"/> that always completes with <c>true</c>, indicating the resource was acquired.</returns>
    public Task<bool> AcquireAsync(string key, TimeSpan timeout) => Task.FromResult(true);
    /// <summary>
    /// Releases the resource associated with the specified key. This implementation completes immediately without performing any additional operation.
    /// </summary>
    /// <param name="key">The identifier of the resource to release.</param>
    public Task ReleaseAsync(string key) => Task.CompletedTask;
}

/// <summary>
/// Represents a fake implementation of <see cref="LockManagerService"/>, typically used as a test double to provide controlled or simplified behavior in unit tests.
/// </summary>
/// <remarks>
/// Inherits all members from <see cref="LockManagerService"/> and is intended to be used in place of the real service when actual locking functionality is not required.
/// </remarks>
public class FakeLockManagerService : LockManagerService
{
    public FakeLockManagerService() : base(
        Mock.Of<ILogger<LockManagerService>>(),
        new FakeLockProvider())
    {
    }
}


/// <summary>
/// Represents a fake or test double implementation of <see cref="RedisService"/> that also implements <see cref="ICacheService"/>, typically used to simulate Redis caching behavior in testing scenarios.
/// </summary>
/// <remarks>
/// This class combines the inheritance of <see cref="RedisService"/> with the contract of <see cref="ICacheService"/>, allowing it to stand in for a real Redis-backed cache during unit tests or development.
/// </remarks>
public class FakeRedisService : RedisService, ICacheService
{
    public bool WasCalled { get; private set; }
    public string? LastKey { get; private set; }

    public FakeRedisService() : base(
        Options.Create(new CacheSettings()),
        Mock.Of<ILogger<RedisService>>(),
        new FakeLockManagerService())
    {
    }


    /// <summary>
    /// Retrieves a cached object associated with the specified key, or creates and stores one using the provided factory if no cached entry exists.
    /// This implementation bypasses caching and directly invokes the factory, recording the invocation for verification purposes while ignoring the TTL and cancellation token.
    /// </summary>
    /// <param name="key">The cache key used to identify the stored object.</param>
    /// <param name="factory">A delegate that asynchronously produces the object to cache when no entry exists for the specified key.</param>
    /// <param name="ttl">An optional time-to-live duration for the cached entry. Not used in this implementation.</param>
    /// <param name="cancellationToken">A token to observe for cancellation requests. Not used in this implementation.</param>
    /// <returns>A task that represents the asynchronous operation, containing the object produced by the factory.</returns>
    Task<T> ICacheService.GetOrSetObjectAsync<T>(
            string key,
            Func<Task<T>> factory,
            TimeSpan? ttl,
            CancellationToken cancellationToken)
        {
            WasCalled = true;
            LastKey = key;
            return factory();
        }

    /// <summary>
    /// Retrieves or sets a cached object associated with the specified grouped field and patient identifier.
    /// Marks the call as executed via the <c>WasCalled</c> flag and returns the value produced by the supplied factory delegate.
    /// </summary>
    /// <param name="groupedField">The grouped field used to identify the cached object.</param>
    /// <param name="patientId">The identifier of the patient associated with the cached object.</param>
    /// <param name="factory">The asynchronous factory delegate invoked to produce the value when no cached entry exists.</param>
    /// <param name="ttl">An optional time-to-live duration for the cached entry.</param>
    /// <param name="cancellationToken">The token used to cancel the asynchronous operation.</param>
    /// <returns>The value of type <typeparamref name="T"/> produced by the <paramref name="factory"/> delegate.</returns>
    Task<T> ICacheService.GetOrSetObjectAsync<T>(
            GroupedField groupedField,
            ObjectId patientId,
            Func<Task<T>> factory,
            TimeSpan? ttl,
            CancellationToken cancellationToken)
        {
            WasCalled = true;
            return factory();
        }
}


/// <summary>
/// Represents a fake implementation of <see cref="CacheService"/> that also implements the <see cref="ICacheService"/> interface, typically used for testing or stubbing scenarios.
/// </summary>
/// <remarks>
/// This class combines inheritance from the concrete <see cref="CacheService"/> base class with the <see cref="ICacheService"/> contract, allowing it to be used wherever an <see cref="ICacheService"/> is required.
/// </remarks>
public class FakeCacheService : CacheService, ICacheService
{
    public bool WasCalled { get; private set; }
    public string? LastKey { get; private set; }

    public FakeCacheService() : base(new FakeLockManagerService())
    {
    }

    Task<T> ICacheService.GetOrSetObjectAsync<T>(
        string key,
        Func<Task<T>> factory,
        TimeSpan? ttl,
        CancellationToken cancellationToken)
    {
        WasCalled = true;
        LastKey = key;
        return factory();
    }

    Task<T> ICacheService.GetOrSetObjectAsync<T>(
        GroupedField groupedField,
        ObjectId patientId,
        Func<Task<T>> factory,
        TimeSpan? ttl,
        CancellationToken cancellationToken)
    {
        WasCalled = true;
        return factory();
    }
}


/// <summary>
/// Represents a fake implementation of the cache service, used for testing or scenarios where a no-op cache behavior is required.
/// </summary>
/// <remarks>
/// This class inherits from <see cref="NoCacheService"/> and implements the <see cref="ICacheService"/> interface, providing a non-functional cache suitable for unit tests or environments where caching should be bypassed.
/// </remarks>
public class FakeNoCacheService : NoCacheService, ICacheService
{
    public bool WasCalled { get; private set; }
    public string? LastKey { get; private set; }

    Task<T> ICacheService.GetOrSetObjectAsync<T>(
        string key,
        Func<Task<T>> factory,
        TimeSpan? ttl,
        CancellationToken cancellationToken)
    {
        WasCalled = true;
        LastKey = key;
        return factory();
    }

    Task<T> ICacheService.GetOrSetObjectAsync<T>(
        GroupedField groupedField,
        ObjectId patientId,
        Func<Task<T>> factory,
        TimeSpan? ttl,
        CancellationToken cancellationToken)
    {
        WasCalled = true;
        return factory();
    }
}


[TestFixture]
public class CacheDispatcherTest
{
    private FakeRedisService _redisServiceFake = null!;
    private FakeCacheService _memoryServiceFake = null!;
    private FakeNoCacheService _noopServiceFake = null!;
    private CacheSettings _cacheSettings = null!;
    private CacheDispatcher _cacheDispatcher = null!;

    /// <summary>
    /// Initializes the test environment by instantiating fake implementations of the Redis, in-memory, and no-op cache services, along with default <see cref="CacheSettings"/>, and constructs a <see cref="CacheDispatcher"/> under test using these dependencies.
    /// </summary>
    [SetUp]
        public void SetUp()
        {
            _redisServiceFake = new FakeRedisService();
            _memoryServiceFake = new FakeCacheService();
            _noopServiceFake = new FakeNoCacheService();
    
            _cacheSettings = new CacheSettings();
    
            _cacheDispatcher = new CacheDispatcher(
                _redisServiceFake,
                _memoryServiceFake,
                _noopServiceFake,
                _cacheSettings);
        }

    #region TC-23

    [Test]
    public async Task GetOrSetObjectAsync_UsesRedisService_WhenPatientsModeIsRedis()
    {
        // Arrange
        var key = "patients:latestObs:abc";
        var expectedResult = new { Name = "TestPatient" };
        Func<Task<object>> factory = () => Task.FromResult<object>(expectedResult);

        _cacheSettings.Patients = CacheEnum.Mode.Redis;

        // Act
        var result = await _cacheDispatcher.GetOrSetObjectAsync<object>(key, factory);

        // Assert
        Assert.That(_redisServiceFake.WasCalled,  Is.True,        "RedisService debería haber sido invocado");
        Assert.That(_redisServiceFake.LastKey,     Is.EqualTo(key));
        Assert.That(_memoryServiceFake.WasCalled,  Is.False,       "CacheService NO debería haber sido invocado");
        Assert.That(_noopServiceFake.WasCalled,    Is.False,       "NoCacheService NO debería haber sido invocado");
    }
    #endregion

    #region TC-24
    [Test]
    public async Task GetOrSetObjectAsync_UsesCacheService_WhenAppointmentsModeIsCache()
    {
        // Arrange
        var key = "appointments:patient:id:20260224";
        var expectedResult = new { Name = "TestAppointment" };
        Func<Task<object>> factory = () => Task.FromResult<object>(expectedResult);

        _cacheSettings.Appointments = CacheEnum.Mode.Cache;

        // Act
        var result = await _cacheDispatcher.GetOrSetObjectAsync<object>(key, factory);

        // Assert
        Assert.That(_memoryServiceFake.WasCalled, Is.True,         "CacheService debería haber sido invocado");
        Assert.That(_memoryServiceFake.LastKey,    Is.EqualTo(key));
        Assert.That(_redisServiceFake.WasCalled, Is.False,        "RedisService NO debería haber sido invocado");
        Assert.That(_noopServiceFake.WasCalled,  Is.False,        "NoCacheService NO debería haber sido invocado");
    }
    #endregion

    #region TC-25
    [Test]
    public async Task GetOrSetObjectAsync_UsesNoCacheService_WhenPumpObservationsModeIsNone()
    {
        // Arrange
        var key = "pumpObs:latest:abc:10";
        var expectedResult = new { Name = "TestPump" };
        Func<Task<object>> factory = () => Task.FromResult<object>(expectedResult);

        _cacheSettings.PumpObservations = CacheEnum.Mode.None;

        // Act
        var result = await _cacheDispatcher.GetOrSetObjectAsync<object>(key, factory);

        // Assert
        Assert.That(_noopServiceFake.WasCalled,   Is.True,         "NoCacheService debería haber sido invocado");
        Assert.That(_noopServiceFake.LastKey,    Is.EqualTo(key));
        Assert.That(_redisServiceFake.WasCalled, Is.False,        "RedisService NO debería haber sido invocado");
        Assert.That(_memoryServiceFake.WasCalled, Is.False,       "CacheService NO debería haber sido invocado");
    }
    #endregion

    #region TC-26
    [Test]
    public async Task GetOrSetObjectAsync_UsesCacheService_WhenKeyIsUnrecognized()
    {
        // Arrange
        var key = "unknown:prefix:key";
        var expectedResult = new { Name = "TestUnknown" };
        Func<Task<object>> factory = () => Task.FromResult<object>(expectedResult);

        // Act
        var result = await _cacheDispatcher.GetOrSetObjectAsync<object>(key, factory);

        // Assert - Unknown keys fallback to InMemory (Cache)
        Assert.That(_memoryServiceFake.WasCalled,  Is.True,        "CacheService debería haber sido invocado como fallback");
        Assert.That(_memoryServiceFake.LastKey,    Is.EqualTo(key));
        Assert.That(_redisServiceFake.WasCalled,   Is.False,       "RedisService NO debería haber sido invocado");
        Assert.That(_noopServiceFake.WasCalled,     Is.False,       "NoCacheService NO debería haber sido invocado");
    }

    /// <summary>
    /// Verifies that <see cref="CacheKeyClassifier.Classify"/> returns <see cref="CacheEnum.EntityType.Unknown"/> when the provided cache key does not start with any recognized prefix.
    /// </summary>
    [Test]
        public void Classify_ReturnsUnknown_ForUnrecognizedPrefix()
        {
            // Arrange
            var key = "unknown:prefix:key";
    
            // Act
            var result = CacheKeyClassifier.Classify(key);
    
            // Assert
            Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Unknown));
        }
    #endregion
}
