using DodoSSH.Contracts;
using DodoSSH.Crypto;

namespace DodoSSH.Client.Storage.Tests;

/// <summary>
/// The cache as it is actually deployed: a file on disk.
/// </summary>
/// <remarks>
/// Every other suite here uses an in-memory database because it is faster and isolated. That leaves the
/// production path — <see cref="ClientCacheFactory.ForFile"/>, a real migration against a file that does
/// not exist yet, and data surviving the process that wrote it — untested, which is exactly the shape of
/// bug that only appears on a user's first launch.
/// </remarks>
public sealed class FileBackedCacheTests : IDisposable
{
    private readonly string directory =
        Path.Combine(Path.GetTempPath(), $"dodossh-cache-{Guid.CreateVersion7():N}");

    /// <inheritdoc />
    public void Dispose()
    {
        if (Directory.Exists(directory))
        {
            Directory.Delete(directory, recursive: true);
        }
    }

    [Fact]
    public async Task AMigrationCreatesTheFileAndTheDataOutlivesTheFactory()
    {
        Directory.CreateDirectory(directory);
        var path = Path.Combine(directory, "cache.db");

        var material = Material();

        using (var first = ClientCacheFactory.ForFile(path))
        {
            await first.MigrateAsync(Token);

            File.Exists(path).ShouldBeTrue("the migration should have created the database");

            await new UnlockStore(first, TimeProvider.System).SaveAsync(material, Token);
        }

        // A second factory over the same file, as a later launch of the application is.
        using var second = ClientCacheFactory.ForFile(path);

        // Migrating again is what every launch does, and it has to be a no-op rather than an error.
        await second.MigrateAsync(Token);

        var read = await new UnlockStore(second, TimeProvider.System).ReadAsync(Token);

        read.ShouldNotBeNull();
        read.UserId.ShouldBe(material.UserId);
        read.WrappedPrivateKey.ShouldBe(material.WrappedPrivateKey);
        read.KdfParameters.Salt.ShouldBe(material.KdfParameters.Salt);
    }

    [Fact]
    public async Task AMissingDirectory_FailsClearlyRatherThanSilently()
    {
        // The application creates the profile directory before opening the cache. If that order were ever
        // reversed, this is the error it would produce — worth pinning so the failure stays diagnosable
        // instead of turning into an empty vault.
        using var factory = ClientCacheFactory.ForFile(
            Path.Combine(directory, "missing", "cache.db"));

        await Should.ThrowAsync<Microsoft.Data.Sqlite.SqliteException>(
            async () => await factory.MigrateAsync(Token));
    }

    private static CancellationToken Token => TestContext.Current.CancellationToken;

    private static StoredUnlockMaterial Material() =>
        new(
            "https://dodossh.example",
            Guid.CreateVersion7(),
            "https://idp.example",
            "alice",
            "alice@example.com",
            "Alice",
            KeyGeneration: 1,
            WrappedPrivateKey: [1, 2, 3, 4],
            new KdfParameters("argon2id", [5, 6, 7, 8], 262144, 4, 1),
            DateTimeOffset.FromUnixTimeSeconds(1_750_000_000));
}
