using System.Security.Cryptography;
using Microsoft.Extensions.Configuration;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Cli.Interactive;
using ZB.MOM.WW.Secrets.MasterKey;
using ZB.MOM.WW.Secrets.Replicator.SqlServer;

namespace ZB.MOM.WW.Secrets.Tests.Cli.Interactive;

/// <summary>
/// Exercises <see cref="SecretsSessionFactory"/> against real temp-dir SQLite targets: the happy path
/// (env KEK present → full session + seal/upsert/get/decrypt round-trip), the lockout-recovery path
/// (KEK unresolvable → degraded session that still lists metadata), the operator-override upgrade, and
/// the SQL-Server-with-unresolved-secret-ref fall-back to the local store.
/// </summary>
[Collection("Process environment")]
public sealed class SecretsSessionFactoryTests : IDisposable
{
    private readonly string _dir =
        Path.Combine(Path.GetTempPath(), $"zb-secrets-session-{Guid.NewGuid():N}");
    private readonly List<string> _envVars = [];

    private string DbPath => Path.Combine(_dir, "secrets.db");

    public SecretsSessionFactoryTests() => Directory.CreateDirectory(_dir);

    public void Dispose()
    {
        foreach (string name in _envVars)
        {
            Environment.SetEnvironmentVariable(name, null);
        }

        if (Directory.Exists(_dir))
        {
            try { Directory.Delete(_dir, recursive: true); } catch (IOException) { /* best-effort temp cleanup */ }
        }
    }

    private string SetTempKeyEnv()
    {
        byte[] key = new byte[32];
        RandomNumberGenerator.Fill(key);
        string name = $"ZB_TEST_KEK_{Guid.NewGuid():N}";
        Environment.SetEnvironmentVariable(name, Convert.ToBase64String(key));
        _envVars.Add(name);
        return name;
    }

    private static MasterKeyOptions UnsetEnvKey() => new()
    {
        Source = MasterKeySource.Environment,
        EnvVarName = $"ZB_TEST_UNSET_{Guid.NewGuid():N}",
    };

    [Fact]
    public async Task Opens_full_session_when_env_kek_present()
    {
        string envVar = SetTempKeyEnv();
        StoreTarget target = new TargetConfigReader().Manual(DbPath, new MasterKeyOptions
        {
            Source = MasterKeySource.Environment,
            EnvVarName = envVar,
        });

        SecretsSession session =
            await new SecretsSessionFactory().OpenAsync(target, overrideKek: null, CancellationToken.None);

        Assert.True(session.KekAvailable);
        Assert.Equal("sqlite", session.StoreKind);
        Assert.NotNull(session.Cipher);

        var name = new SecretName("test/roundtrip");
        StoredSecret row = session.Cipher!.Encrypt(name, "hunter2", SecretContentType.Text);
        await session.Store.UpsertAsync(row, CancellationToken.None);
        StoredSecret? fetched = await session.Store.GetAsync(name, CancellationToken.None);
        Assert.NotNull(fetched);
        Assert.Equal("hunter2", session.Cipher!.Decrypt(fetched!));
    }

    [Fact]
    public async Task Opens_degraded_session_when_env_var_unset()
    {
        StoreTarget target = new TargetConfigReader().Manual(DbPath, UnsetEnvKey());

        SecretsSession session =
            await new SecretsSessionFactory().OpenAsync(target, overrideKek: null, CancellationToken.None);

        Assert.False(session.KekAvailable);
        Assert.Null(session.Cipher);
        Assert.Null(session.MasterKey);
        Assert.NotEmpty(session.Warnings);

        // Store is still usable for metadata ops even without a KEK.
        IReadOnlyList<SecretMetadata> list = await session.Store.ListAsync(includeDeleted: false, CancellationToken.None);
        Assert.Empty(list);
    }

    [Fact]
    public async Task Opens_degraded_session_when_master_key_source_is_out_of_range()
    {
        StoreTarget target = new TargetConfigReader().Manual(DbPath, new MasterKeyOptions
        {
            Source = (MasterKeySource)999,
        });

        SecretsSession session =
            await new SecretsSessionFactory().OpenAsync(target, overrideKek: null, CancellationToken.None);

        Assert.False(session.KekAvailable);
        Assert.Null(session.Cipher);
        Assert.NotEmpty(session.Warnings);
        Assert.Contains(session.Warnings, w => w.Contains("999", StringComparison.Ordinal));

        // Store is still usable despite the malformed KEK source.
        IReadOnlyList<SecretMetadata> list = await session.Store.ListAsync(includeDeleted: false, CancellationToken.None);
        Assert.Empty(list);
    }

    [Fact]
    public async Task Override_kek_upgrades_degraded_session()
    {
        StoreTarget target = new TargetConfigReader().Manual(DbPath, UnsetEnvKey());
        byte[] key = new byte[32];
        RandomNumberGenerator.Fill(key);
        var overrideKek = new LiteralMasterKeyProvider(Convert.ToBase64String(key));

        SecretsSession session =
            await new SecretsSessionFactory().OpenAsync(target, overrideKek, CancellationToken.None);

        Assert.True(session.KekAvailable);
        Assert.NotNull(session.Cipher);
        Assert.Same(overrideKek, session.MasterKey);
    }

    [Fact]
    public async Task SqlServer_target_with_unresolved_secret_ref_connstr_falls_back_to_sqlite_with_warning()
    {
        string envVar = SetTempKeyEnv();
        SecretsOptions secrets = new()
        {
            SqlitePath = DbPath,
            MasterKey = new MasterKeyOptions { Source = MasterKeySource.Environment, EnvVarName = envVar },
        };
        SqlServerSecretsOptions sqlServer = new()
        {
            ConnectionString = "Server=hub;Database=secrets;User Id=app;Password=${secret:sql/hub-pw}",
        };
        StoreTarget target = new(
            "sql-hub", AppSettingsPath: null, secrets, sqlServer, new ConfigurationBuilder().Build());

        SecretsSession session =
            await new SecretsSessionFactory().OpenAsync(target, overrideKek: null, CancellationToken.None);

        Assert.Equal("sqlite", session.StoreKind);
        Assert.Contains(session.Warnings, w => w.Contains("SQLite", StringComparison.OrdinalIgnoreCase));
    }
}
