using DodoSSH.Contracts;
using DodoSSH.Crypto;

namespace DodoSSH.Client.Storage.Tests;

/// <summary>
/// A migrated, unlocked cache for one test.
/// </summary>
/// <remarks>
/// Each harness gets its own in-memory database, so tests cannot interfere and can run in parallel.
/// The Argon2id cost is deliberately far below the shipped profile — 8 MiB and one pass rather than
/// 256 MiB and four. The stretching is what makes a stolen wrap expensive to attack, and none of these
/// tests attack one; paying 320 ms per test to prove nothing would only encourage sharing state
/// between them.
/// </remarks>
internal sealed class CacheHarness : IDisposable
{
    /// <remarks>
    /// Fixed rather than "now", because a bundle's encoding includes its creation timestamp and the cache
    /// key derives from that encoding. Nothing here depends on the value; it only has to be a constant.
    /// </remarks>
    private static readonly DateTimeOffset IdentityCreatedAt =
        new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);

    private CacheHarness(ClientCacheFactory factory, LocalCacheProtector protector)
    {
        Factory = factory;
        Protector = protector;

        Items = new ItemStore(factory, protector);
        Outbox = new OutboxStore(factory, protector, TimeProvider.System);
        Vaults = new VaultStore(factory, TimeProvider.System);
        Unlock = new UnlockStore(factory, TimeProvider.System);
        SyncState = new SyncStateStore(factory);
        Conflicts = new ConflictStore(factory, protector, TimeProvider.System);
    }

    internal static Guid VaultId { get; } = Guid.Parse("0192f0c8-aaaa-7c3d-8e4f-5a6b7c8d9e0f");

    internal static Guid UserId { get; } = Guid.Parse("0192f0c8-bbbb-7c3d-8e4f-5a6b7c8d9e0f");

    internal ClientCacheFactory Factory { get; }

    internal LocalCacheProtector Protector { get; }

    internal ItemStore Items { get; }

    internal OutboxStore Outbox { get; }

    internal VaultStore Vaults { get; }

    internal UnlockStore Unlock { get; }

    internal SyncStateStore SyncState { get; }

    internal ConflictStore Conflicts { get; }

    /// <remarks>
    /// Each harness gets a freshly generated identity, so two of them are two different users and their
    /// cache keys differ. There is no passphrase here at all any more: the cache key derives from the
    /// bundle, so a passphrase is not part of the question this harness sets up.
    /// </remarks>
    internal static async Task<CacheHarness> CreateAsync()
    {
        var factory = ClientCacheFactory.ForMemory($"cache-{Guid.CreateVersion7():N}");

        try
        {
            await factory.MigrateAsync(TestContext.Current.CancellationToken);

            // Disposed immediately: the protector copies the derived key, so the identity itself is not
            // needed once it has answered.
            using var identity = UserSecretBundle.Create(IdentityCreatedAt);

            return new CacheHarness(factory, LocalCacheProtector.From(identity));
        }
        catch
        {
            factory.Dispose();
            throw;
        }
    }

    /// <inheritdoc />
    public void Dispose()
    {
        Protector.Dispose();
        Factory.Dispose();
    }

    // ---- Builders ----

    internal static EncryptedPayload Payload(byte seed = 1, uint keyGeneration = 1) =>
        new(
            Envelope: [seed, (byte)(seed + 1), (byte)(seed + 2)],
            WrappedDataKey: [(byte)(seed + 10), (byte)(seed + 11)],
            DataKeyId: Guid.Parse($"0192f0c8-cccc-7c3d-8e4f-5a6b7c8d9e{seed:x2}"),
            KeyGeneration: keyGeneration,
            AadVersion: CryptoSpec.CurrentAadVersion);

    internal static StoredItem Item(
        Guid entityId,
        int version = 1,
        long changeSequence = 1,
        byte seed = 1,
        bool deleted = false,
        SyncPlaintextFields? fields = null) =>
        new(
            VaultId,
            SyncEntityType.Host,
            entityId,
            version,
            changeSequence,
            deleted ? null : Payload(seed),
            deleted ? null : fields ?? new SyncPlaintextFields(),
            deleted,
            DateTimeOffset.FromUnixTimeSeconds(1_750_000_000 + changeSequence));

    internal static QueuedChange Change(
        Guid entityId,
        SyncOperation operation = SyncOperation.Upsert,
        int? expectedVersion = null,
        byte seed = 1,
        StoredAncestor? ancestor = null,
        SyncPlaintextFields? fields = null) =>
        new(
            VaultId,
            SyncEntityType.Host,
            entityId,
            operation,
            expectedVersion,
            operation == SyncOperation.Delete ? null : Payload(seed),
            operation == SyncOperation.Delete ? null : fields ?? new SyncPlaintextFields(),
            ancestor);
}
