using DodoSSH.Contracts;

namespace DodoSSH.Client.Storage.Tests;

/// <summary>
/// The sign-in a machine may resume, and what emptying the cache does to it.
/// </summary>
/// <remarks>
/// Two behaviours meet here for a reason: the refresh token is the one thing in this cache that is a
/// credential for the <em>account</em> rather than for the vault, so both halves of its life — sealed
/// while it is kept, gone when the user signs out — belong under one test class.
/// </remarks>
public sealed class RememberedSignInTests
{
    private static CancellationToken Token => TestContext.Current.CancellationToken;

    [Fact]
    public async Task ARememberedTokenRoundTrips()
    {
        using var harness = await CacheHarness.CreateAsync();

        var store = Store(harness);

        (await store.ReadAsync(Token)).ShouldBeNull("nothing has been remembered yet");

        await store.SaveAsync("refresh-token-1", Token);

        (await store.ReadAsync(Token)).ShouldBe("refresh-token-1");
    }

    [Fact]
    public async Task RememberingAgain_ReplacesRatherThanAdds()
    {
        // What a rotating provider does on every refresh. A second row would be a constraint violation;
        // keeping the first would leave the next launch presenting a token the provider has retired.
        using var harness = await CacheHarness.CreateAsync();

        var store = Store(harness);

        await store.SaveAsync("refresh-token-1", Token);
        await store.SaveAsync("refresh-token-2", Token);

        (await store.ReadAsync(Token)).ShouldBe("refresh-token-2");
    }

    [Fact]
    public async Task AnotherUsersCacheKey_DoesNotOpenIt()
    {
        // The whole reason this is sealed rather than stored. A cache file lifted off a machine cannot be
        // made to yield an account credential without the key that only an unlocked vault holds.
        using var owner = await CacheHarness.CreateAsync();
        using var stranger = await CacheHarness.CreateAsync();

        await Store(owner).SaveAsync("refresh-token-1", Token);

        var strangersView = new RememberedSignInStore(
            owner.Factory, stranger.Protector, CacheHarness.UserId, TimeProvider.System);

        (await strangersView.ReadAsync(Token)).ShouldBeNull();
    }

    [Fact]
    public async Task ForgettingIt_LeavesNothingToResume()
    {
        using var harness = await CacheHarness.CreateAsync();

        var store = Store(harness);

        await store.SaveAsync("refresh-token-1", Token);
        await store.ForgetAsync(Token);

        (await store.ReadAsync(Token)).ShouldBeNull();

        // And forgetting what is not there is not an error: it runs on a sign-out from a machine that
        // never remembered one.
        await store.ForgetAsync(Token);
    }

    [Fact]
    public async Task ResettingTheCache_EmptiesEveryTableAndKeepsTheSchema()
    {
        // What signing out does on disk. Every row goes — the profile an unlock reads, the item mirror,
        // the outbox, the remembered sign-in — and the database is immediately usable again, because the
        // application has to be able to be set up afresh without being restarted.
        using var harness = await CacheHarness.CreateAsync();

        var entityId = Guid.CreateVersion7();

        await harness.Unlock.SaveAsync(Material(), Token);
        await harness.Items.SaveAsync(CacheHarness.Item(entityId), Token);
        await harness.Outbox.QueueAsync(CacheHarness.Change(entityId), Token);
        await Store(harness).SaveAsync("refresh-token-1", Token);

        await harness.Factory.ResetAsync(Token);

        (await harness.Unlock.ReadAsync(Token)).ShouldBeNull("the profile is what makes a machine enrolled");

        (await harness.Items
                .ListAsync(CacheHarness.VaultId, SyncEntityType.Host, includeDeleted: true, Token))
            .ShouldBeEmpty();
        (await harness.Outbox.ListAllAsync(CacheHarness.VaultId, Token)).ShouldBeEmpty();
        (await Store(harness).ReadAsync(Token)).ShouldBeNull();

        // Usable, not merely empty: writing to it again must not need a migration.
        await harness.Unlock.SaveAsync(Material(), Token);
        (await harness.Unlock.ReadAsync(Token)).ShouldNotBeNull();
    }

    private static RememberedSignInStore Store(CacheHarness harness) =>
        new(harness.Factory, harness.Protector, CacheHarness.UserId, TimeProvider.System);

    private static StoredUnlockMaterial Material() =>
        new(
            "https://dodossh.example",
            CacheHarness.UserId,
            "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));
}
