using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
using Tiku.Application.Security;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure;
using Tiku.Infrastructure.Security;

namespace Tiku.IntegrationTests;

public sealed class RedisSecurityStoreTests
{
    [Fact]
    public async Task Unavailable_redis_fails_closed_for_security_operations()
    {
        await using var provider = BuildProvider(
            "localhost:6399,connectTimeout=200,syncTimeout=200,asyncTimeout=200,abortConnect=false",
            $"integration-unavailable-{Guid.NewGuid():N}");
        var store = provider.GetRequiredService<IRedisSecurityStore>();

        await Assert.ThrowsAsync<RedisSecurityUnavailableException>(() => store.ConsumeAsync(
        [
            new DistributedRateLimitBucket("password:ip:test", 1, TimeSpan.FromSeconds(1))
        ]));
        Assert.False(await store.PingAsync());
    }

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

        var environment = $"integration-{Guid.NewGuid():N}";
        await using var first = BuildProvider(connectionString, environment);
        await using var second = BuildProvider(connectionString, environment);
        var store1 = first.GetRequiredService<IRedisSecurityStore>();
        var store2 = second.GetRequiredService<IRedisSecurityStore>();
        const string phone = "13812345678";
        var phoneHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(phone))).ToLowerInvariant();
        var bucket = new DistributedRateLimitBucket(
            $"sms-verify:tenant-id:login:{phoneHash}", 1, TimeSpan.FromMinutes(1));

        try
        {
            var attempts = await Task.WhenAll(Enumerable.Range(0, 12).Select(index =>
                (index & 1) == 0
                    ? store1.ConsumeAsync([bucket])
                    : store2.ConsumeAsync([bucket])));

            Assert.Single(attempts, result => result.Allowed);
            Assert.All(attempts.Where(result => !result.Allowed), result => Assert.NotNull(result.RetryAfter));
            var multiplexer = first.GetRequiredService<IConnectionMultiplexer>();
            var server = multiplexer.GetServer(multiplexer.GetEndPoints().Single());
            var keys = server.Keys(pattern: $"tiku:{environment}:*").Select(key => key.ToString()).ToArray();
            Assert.NotEmpty(keys);
            Assert.DoesNotContain(keys, key => key.Contains(phone, StringComparison.Ordinal));
        }
        finally
        {
            var multiplexer = first.GetRequiredService<IConnectionMultiplexer>();
            var server = multiplexer.GetServer(multiplexer.GetEndPoints().Single());
            var keys = server.Keys(pattern: $"tiku:{environment}:*").ToArray();
            if (keys.Length > 0) await multiplexer.GetDatabase().KeyDeleteAsync(keys);
        }
    }

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

        var environment = $"integration-authz-{Guid.NewGuid():N}";
        await using var first = BuildProvider(connectionString, environment);
        await using var second = BuildProvider(connectionString, environment);
        var writer = first.GetRequiredService<IAccessSecurityCache>();
        var reader = second.GetRequiredService<IAccessSecurityCache>();
        var tenantId = Guid.NewGuid();
        var userId = Guid.NewGuid();
        var sessionId = Guid.NewGuid();
        var lookup = new AccessSecurityCacheLookup(sessionId, userId, AuthRealm.Tenant, tenantId);
        await writer.SetAsync(new AccessSecurityCacheState(
            new CachedSessionSecurityState(sessionId, userId, AuthRealm.Tenant, tenantId, "stamp",
                DateTimeOffset.UtcNow.AddMinutes(15), false),
            new CachedUserSecurityState(userId, UserStatus.Active, "stamp"),
            new CachedTenantSecurityState(tenantId, TenantStatus.Active),
            new CachedMembershipSecurityState(tenantId, userId, MembershipStatus.Active),
            null,
            new CachedAuthorizationVersion(AuthRealm.Tenant, tenantId, 1)));

        Assert.True((await reader.GetAsync(lookup))?.Complete);
        await writer.SetAuthorizationVersionAsync(AuthRealm.Tenant, tenantId, 5);
        await writer.SetAuthorizationVersionAsync(AuthRealm.Tenant, tenantId, 4);
        Assert.Equal(5, (await reader.GetAsync(lookup))?.AuthorizationVersion?.Version);
    }

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