using System.Security.Cryptography;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.DependencyInjection;
using ZB.MOM.WW.Secrets.Sqlite;

namespace ZB.MOM.WW.Secrets.Tests.DependencyInjection;

/// <summary>
/// Verifies that <see cref="SecretsServiceCollectionExtensions.AddZbSecrets"/> wires the whole
/// core: every seam resolves, a real secret round-trips through the composed graph, and a missing
/// master key fails closed.
/// </summary>
[Collection("Process environment")]
public sealed class AddZbSecretsTests
{
    private static string NewTempDbPath() =>
        Path.Combine(Path.GetTempPath(), $"zb-secrets-di-{Guid.NewGuid():N}.db");

    private static string Base64Key32() => Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));

    private static IConfiguration BuildConfig(string sqlitePath, string envVarName) =>
        new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Secrets:SqlitePath"] = sqlitePath,
                ["Secrets:MasterKey:Source"] = "Environment",
                ["Secrets:MasterKey:EnvVarName"] = envVarName,
            })
            .Build();

    [Fact]
    public void Registers_All_CoreServices()
    {
        string dbPath = NewTempDbPath();
        string envVar = $"ZB_TEST_KEY_{Guid.NewGuid():N}";
        Environment.SetEnvironmentVariable(envVar, Base64Key32());
        try
        {
            var services = new ServiceCollection();
            services.AddZbSecrets(BuildConfig(dbPath, envVar), "Secrets");
            using ServiceProvider provider = services.BuildServiceProvider();

            Assert.NotNull(provider.GetService<ISecretResolver>());
            Assert.NotNull(provider.GetService<ISecretStore>());
            Assert.NotNull(provider.GetService<IMasterKeyProvider>());
            Assert.NotNull(provider.GetService<ISecretCipher>());

            ISecretReplicator? replicator = provider.GetService<ISecretReplicator>();
            Assert.NotNull(replicator);
            Assert.IsType<NoOpSecretReplicator>(replicator);
        }
        finally
        {
            Environment.SetEnvironmentVariable(envVar, null);
            File.Delete(dbPath);
        }
    }

    [Fact]
    public void CacheInvalidator_And_Resolver_AreSameSingletonInstance()
    {
        string dbPath = NewTempDbPath();
        string envVar = $"ZB_TEST_KEY_{Guid.NewGuid():N}";
        Environment.SetEnvironmentVariable(envVar, Base64Key32());
        try
        {
            var services = new ServiceCollection();
            services.AddZbSecrets(BuildConfig(dbPath, envVar), "Secrets");
            using ServiceProvider provider = services.BuildServiceProvider();

            var resolver = provider.GetService<ISecretResolver>();
            var invalidator = provider.GetService<ISecretCacheInvalidator>();

            Assert.NotNull(resolver);
            Assert.NotNull(invalidator);
            // Same object: an invalidation MUST hit the exact cache the resolver reads from.
            Assert.Same(resolver, invalidator);
        }
        finally
        {
            Environment.SetEnvironmentVariable(envVar, null);
            File.Delete(dbPath);
        }
    }

    [Fact]
    public async Task Resolver_Works_EndToEnd_ThroughDi()
    {
        string dbPath = NewTempDbPath();
        string envVar = $"ZB_TEST_KEY_{Guid.NewGuid():N}";
        Environment.SetEnvironmentVariable(envVar, Base64Key32());
        try
        {
            var services = new ServiceCollection();
            services.AddZbSecrets(BuildConfig(dbPath, envVar), "Secrets");
            using ServiceProvider provider = services.BuildServiceProvider();

            // Run the migration first so the schema exists (as the hosted service would on startup).
            await provider.GetRequiredService<SqliteSecretsStoreMigrator>().MigrateAsync(CancellationToken.None);

            var name = new SecretName("db/password");
            const string plaintext = "super-secret-value";

            ISecretCipher cipher = provider.GetRequiredService<ISecretCipher>();
            ISecretStore store = provider.GetRequiredService<ISecretStore>();
            StoredSecret row = cipher.Encrypt(name, plaintext, SecretContentType.Text);
            await store.UpsertAsync(row, CancellationToken.None);

            ISecretResolver resolver = provider.GetRequiredService<ISecretResolver>();
            string? resolved = await resolver.GetAsync(name, CancellationToken.None);

            Assert.Equal(plaintext, resolved);
        }
        finally
        {
            Environment.SetEnvironmentVariable(envVar, null);
            File.Delete(dbPath);
        }
    }

    [Fact]
    public void MasterKey_Unavailable_FailsClosed()
    {
        string dbPath = NewTempDbPath();
        string envVar = $"ZB_TEST_KEY_{Guid.NewGuid():N}";
        // Deliberately do NOT set the env var — the master key cannot be resolved.
        try
        {
            var services = new ServiceCollection();
            services.AddZbSecrets(BuildConfig(dbPath, envVar), "Secrets");
            using ServiceProvider provider = services.BuildServiceProvider();

            IMasterKeyProvider keyProvider = provider.GetRequiredService<IMasterKeyProvider>();

            Assert.Throws<MasterKeyUnavailableException>(() => keyProvider.GetMasterKey());
        }
        finally
        {
            File.Delete(dbPath);
        }
    }
}
