using System.Collections.Concurrent;
using System.Collections.Immutable;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Vexer.Core;
using StellaOps.Vexer.Export;

namespace StellaOps.Vexer.Export.Tests;

public sealed class S3ArtifactStoreTests
{
    [Fact]
    public async Task SaveAsync_UploadsContentWithMetadata()
    {
        var client = new FakeS3Client();
        var options = Options.Create(new S3ArtifactStoreOptions { BucketName = "exports", Prefix = "vex" });
        var store = new S3ArtifactStore(client, options, NullLogger<S3ArtifactStore>.Instance);

        var content = new byte[] { 1, 2, 3, 4 };
        var artifact = new VexExportArtifact(
            new VexContentAddress("sha256", "deadbeef"),
            VexExportFormat.Json,
            content,
            ImmutableDictionary<string, string>.Empty);

        await store.SaveAsync(artifact, CancellationToken.None);

        Assert.True(client.PutCalls.TryGetValue("exports", out var bucketEntries));
        Assert.NotNull(bucketEntries);
        var entry = bucketEntries!.Single();
        Assert.Equal("vex/json/deadbeef.json", entry.Key);
        Assert.Equal(content, entry.Content);
        Assert.Equal("sha256:deadbeef", entry.Metadata["vex-digest"]);
    }

    [Fact]
    public async Task OpenReadAsync_ReturnsStoredContent()
    {
        var client = new FakeS3Client();
        var options = Options.Create(new S3ArtifactStoreOptions { BucketName = "exports", Prefix = "vex" });
        var store = new S3ArtifactStore(client, options, NullLogger<S3ArtifactStore>.Instance);

        var address = new VexContentAddress("sha256", "cafebabe");
        client.SeedObject("exports", "vex/json/cafebabe.json", new byte[] { 9, 9, 9 });

        var stream = await store.OpenReadAsync(address, CancellationToken.None);
        Assert.NotNull(stream);
        using var ms = new MemoryStream();
        await stream!.CopyToAsync(ms);
        Assert.Equal(new byte[] { 9, 9, 9 }, ms.ToArray());
    }

    private sealed class FakeS3Client : IS3ArtifactClient
    {
        public ConcurrentDictionary<string, List<S3Entry>> PutCalls { get; } = new(StringComparer.Ordinal);
        private readonly ConcurrentDictionary<(string Bucket, string Key), byte[]> _storage = new();

        public void SeedObject(string bucket, string key, byte[] content)
        {
            PutCalls.GetOrAdd(bucket, _ => new List<S3Entry>()).Add(new S3Entry(key, content, new Dictionary<string, string>()));
            _storage[(bucket, key)] = content;
        }

        public Task<bool> ObjectExistsAsync(string bucketName, string key, CancellationToken cancellationToken)
            => Task.FromResult(_storage.ContainsKey((bucketName, key)));

        public Task PutObjectAsync(string bucketName, string key, Stream content, IDictionary<string, string> metadata, CancellationToken cancellationToken)
        {
            using var ms = new MemoryStream();
            content.CopyTo(ms);
            var bytes = ms.ToArray();
            PutCalls.GetOrAdd(bucketName, _ => new List<S3Entry>()).Add(new S3Entry(key, bytes, new Dictionary<string, string>(metadata)));
            _storage[(bucketName, key)] = bytes;
            return Task.CompletedTask;
        }

        public Task<Stream?> GetObjectAsync(string bucketName, string key, CancellationToken cancellationToken)
        {
            if (_storage.TryGetValue((bucketName, key), out var bytes))
            {
                return Task.FromResult<Stream?>(new MemoryStream(bytes, writable: false));
            }

            return Task.FromResult<Stream?>(null);
        }

        public Task DeleteObjectAsync(string bucketName, string key, CancellationToken cancellationToken)
        {
            _storage.TryRemove((bucketName, key), out _);
            return Task.CompletedTask;
        }

        public readonly record struct S3Entry(string Key, byte[] Content, IDictionary<string, string> Metadata);
    }
}
