using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Infrastructure;
using Tiku.Infrastructure.Caching;
using Tiku.Infrastructure.Tenancy;
using ZiggyCreatures.Caching.Fusion;

namespace Tiku.IntegrationTests;

public sealed class BusinessCachingTests
{
    [Fact]
    public async Task Local_cache_coalesces_concurrent_factories_and_uses_safe_defaults()
    {
        await using var provider = BuildLocalProvider("Integration Tests");
        var cache = provider.GetRequiredKeyedService<IFusionCache>(
            BusinessCachingServiceCollectionExtensions.CacheName);
        var options = provider.GetRequiredService<IOptionsMonitor<FusionCacheOptions>>()
            .Get(BusinessCachingServiceCollectionExtensions.CacheName);
        var calls = 0;
        var releaseFactory = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

        var tasks = Enumerable.Range(0, 16).Select(_ => cache.GetOrSetAsync(
                "stampede",
                async cancellationToken =>
                {
                    Interlocked.Increment(ref calls);
                    await releaseFactory.Task.WaitAsync(cancellationToken);
                    return 42;
                },
                entry => entry.Duration = TimeSpan.FromMinutes(1))
            .AsTask()).ToArray();

        Assert.True(SpinWait.SpinUntil(() => Volatile.Read(ref calls) > 0, TimeSpan.FromSeconds(2)));
        releaseFactory.SetResult();

        Assert.All(await Task.WhenAll(tasks), value => Assert.Equal(42, value));
        Assert.Equal(1, calls);
        Assert.Equal(BusinessCachingServiceCollectionExtensions.CacheName, cache.CacheName);
        Assert.Equal("tiku:integration-tests:business:v2:", options.CacheKeyPrefix);
        Assert.Equal("tiku:integration-tests:business:v2", options.BackplaneChannelPrefix);
        Assert.False(cache.DefaultEntryOptions.IsFailSafeEnabled);
        Assert.False(cache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations);
        Assert.False(cache.DefaultEntryOptions.AllowBackgroundBackplaneOperations);
    }

    [Fact]
    public async Task Unavailable_redis_falls_back_to_the_value_factory()
    {
        await using var provider = BuildRedisProvider(
            "localhost:6399,connectTimeout=200,syncTimeout=200,asyncTimeout=200,abortConnect=false",
            $"business-unavailable-{Guid.NewGuid():N}");
        var cache = provider.GetRequiredKeyedService<IFusionCache>(
            BusinessCachingServiceCollectionExtensions.CacheName);

        var value = await cache.GetOrSetAsync(
            "fallback",
            _ => Task.FromResult(42),
            entry => entry.Duration = TimeSpan.FromMinutes(1));

        Assert.Equal(42, value);
    }

    [Fact]
    public async Task Tenant_directory_caches_negative_results()
    {
        await using var factory = new Api.ApiTestFactory();
        _ = factory.Services;
        using var scope = factory.Services.CreateScope();
        var directory = scope.ServiceProvider.GetRequiredService<ITenantDirectory>();
        var cache = scope.ServiceProvider.GetRequiredKeyedService<IFusionCache>(
            BusinessCachingServiceCollectionExtensions.CacheName);
        var host = $"missing-{Guid.NewGuid():N}.example.test";

        Assert.Null(await directory.FindByHostAsync(host));
        Assert.Null(await directory.FindByHostAsync(host));

        var cached = await cache.TryGetAsync<TenantDirectoryCacheEntry>(TenantDirectory.CacheKey("host", host));
        Assert.True(cached.HasValue);
        Assert.False(cached.Value.Found);
        Assert.Null(cached.Value.Entry);
    }

    [Fact]
    public async Task Redis_backplane_removes_another_nodes_local_entry()
    {
        var connectionString = Environment.GetEnvironmentVariable("TIKU_TEST_REDIS");
        if (string.IsNullOrWhiteSpace(connectionString)) return;

        var environment = $"business-backplane-{Guid.NewGuid():N}";
        await using var first = BuildRedisProvider(connectionString, environment);
        await using var second = BuildRedisProvider(connectionString, environment);
        var firstCache = first.GetRequiredKeyedService<IFusionCache>(
            BusinessCachingServiceCollectionExtensions.CacheName);
        var secondCache = second.GetRequiredKeyedService<IFusionCache>(
            BusinessCachingServiceCollectionExtensions.CacheName);
        const string key = "shared-entry";

        try
        {
            await firstCache.SetAsync(key, 1, entry => entry.Duration = TimeSpan.FromMinutes(1));
            Assert.Equal(1, await secondCache.GetOrSetAsync(
                key,
                _ => Task.FromResult(99),
                entry => entry.Duration = TimeSpan.FromMinutes(1)));

            await firstCache.RemoveAsync(key);
            var reloaded = await secondCache.GetOrSetAsync(
                key,
                _ => Task.FromResult(2),
                entry => entry.Duration = TimeSpan.FromMinutes(1));

            Assert.Equal(2, reloaded);
        }
        finally
        {
            var multiplexer = first.GetRequiredService<IConnectionMultiplexer>();
            var server = multiplexer.GetServer(multiplexer.GetEndPoints().Single());
            var keys = server.Keys(pattern: $"tiku:{environment}:business:v2:*").ToArray();
            if (keys.Length > 0) await multiplexer.GetDatabase().KeyDeleteAsync(keys);
        }
    }

    private static ServiceProvider BuildLocalProvider(string environment)
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddBusinessCaching(environment, false);
        return services.BuildServiceProvider();
    }

    private static ServiceProvider BuildRedisProvider(string connectionString, string environment)
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.Configure<AuthorizationCacheOptions>(_ => { });
        services.AddRedisSecurity(connectionString, environment);
        services.AddBusinessCaching(environment, true);
        return services.BuildServiceProvider();
    }
}
