using Microsoft.Data.Sqlite;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Sqlite;

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

public sealed class SqliteSecretsStoreMigratorTests : IDisposable
{
    private readonly string _dbPath =
        Path.Combine(Path.GetTempPath(), $"zb-secrets-migrator-{Guid.NewGuid():N}.db");

    [Fact]
    public async Task Migrate_IsIdempotent()
    {
        var factory = new SecretsSqliteConnectionFactory(_dbPath);
        var migrator = new SqliteSecretsStoreMigrator(factory);

        // Running twice must not throw and must leave the schema version at CurrentVersion.
        await migrator.MigrateAsync(CancellationToken.None);
        await migrator.MigrateAsync(CancellationToken.None);

        long version = await ReadSchemaVersionAsync(factory);
        Assert.Equal(SqliteSecretsSchema.CurrentVersion, version);
        Assert.Equal(1, version);
    }

    [Fact]
    public async Task Migrate_CreatesSecretTableWithExpectedColumns()
    {
        var factory = new SecretsSqliteConnectionFactory(_dbPath);
        var migrator = new SqliteSecretsStoreMigrator(factory);

        await migrator.MigrateAsync(CancellationToken.None);

        HashSet<string> columns = await ReadSecretColumnsAsync(factory);

        string[] expected =
        [
            "name", "description", "content_type", "ciphertext", "nonce", "tag",
            "wrapped_dek", "wrap_nonce", "wrap_tag", "kek_id", "revision",
            "is_deleted", "deleted_utc", "created_utc", "updated_utc",
            "created_by", "updated_by",
        ];

        Assert.Equal(17, expected.Length);
        Assert.Equal(17, columns.Count);
        foreach (string column in expected)
        {
            Assert.Contains(column, columns);
        }
    }

    [Fact]
    public async Task Migrate_RefusesNewerSchema()
    {
        var factory = new SecretsSqliteConnectionFactory(_dbPath);

        // Manually stamp an on-disk version newer than this build supports.
        await using (SqliteConnection connection = await factory.OpenConnectionAsync(CancellationToken.None))
        {
            await using SqliteCommand create = connection.CreateCommand();
            create.CommandText = SqliteSecretsSchema.CreateSchemaVersionTable;
            await create.ExecuteNonQueryAsync();

            await using SqliteCommand insert = connection.CreateCommand();
            insert.CommandText = """
                INSERT INTO schema_version (id, version, applied_utc)
                VALUES (1, $version, $applied_utc);
                """;
            insert.Parameters.AddWithValue("$version", 999);
            insert.Parameters.AddWithValue("$applied_utc", DateTimeOffset.UtcNow.ToString("O"));
            await insert.ExecuteNonQueryAsync();
        }

        var migrator = new SqliteSecretsStoreMigrator(factory);

        await Assert.ThrowsAsync<SecretStoreMigrationException>(
            () => migrator.MigrateAsync(CancellationToken.None));
    }

    private static async Task<long> ReadSchemaVersionAsync(SecretsSqliteConnectionFactory factory)
    {
        await using SqliteConnection connection = await factory.OpenConnectionAsync(CancellationToken.None);
        await using SqliteCommand command = connection.CreateCommand();
        command.CommandText = "SELECT version FROM schema_version WHERE id = 1;";
        object? result = await command.ExecuteScalarAsync();
        return Convert.ToInt64(result);
    }

    private static async Task<HashSet<string>> ReadSecretColumnsAsync(SecretsSqliteConnectionFactory factory)
    {
        await using SqliteConnection connection = await factory.OpenConnectionAsync(CancellationToken.None);
        await using SqliteCommand command = connection.CreateCommand();
        command.CommandText = $"PRAGMA table_info({SqliteSecretsSchema.SecretTable});";

        var columns = new HashSet<string>(StringComparer.Ordinal);
        await using SqliteDataReader reader = await command.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            // PRAGMA table_info columns: cid(0), name(1), type(2), ...
            columns.Add(reader.GetString(1));
        }

        return columns;
    }

    public void Dispose()
    {
        // Drop pooled connections so the WAL/-shm/-wal sidecars release before we delete.
        SqliteConnection.ClearAllPools();

        foreach (string path in new[] { _dbPath, _dbPath + "-wal", _dbPath + "-shm" })
        {
            try
            {
                if (File.Exists(path))
                {
                    File.Delete(path);
                }
            }
            catch (IOException)
            {
                // Best-effort temp cleanup; a leaked temp file is not a test failure.
            }
        }
    }
}
