using System.Collections.Immutable;
using System.Globalization;
using System.Text;
using Microsoft.Extensions.Options;
using Mongo2Go;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Vexer.Core;

namespace StellaOps.Vexer.Storage.Mongo.Tests;

public sealed class MongoVexRepositoryTests : IAsyncLifetime
{
    private readonly MongoDbRunner _runner;
    private readonly MongoClient _client;

    public MongoVexRepositoryTests()
    {
        _runner = MongoDbRunner.Start();
        _client = new MongoClient(_runner.ConnectionString);
    }

    [Fact]
    public async Task RawStore_UsesGridFsForLargePayloads()
    {
        var database = _client.GetDatabase($"vex-raw-gridfs-{Guid.NewGuid():N}");
        var store = CreateRawStore(database, thresholdBytes: 32);

        var payload = Encoding.UTF8.GetBytes(new string('A', 256));
        var document = new VexRawDocument(
            "red-hat",
            VexDocumentFormat.Csaf,
            new Uri("https://example.com/redhat/csaf.json"),
            DateTimeOffset.UtcNow,
            "sha256:large",
            payload,
            ImmutableDictionary<string, string>.Empty);

        await store.StoreAsync(document, CancellationToken.None);

        var rawCollection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Raw);
        var stored = await rawCollection.Find(Builders<BsonDocument>.Filter.Eq("_id", document.Digest))
            .FirstOrDefaultAsync();

        Assert.NotNull(stored);
        Assert.True(stored!.TryGetValue("GridFsObjectId", out var gridId));
        Assert.False(gridId.IsBsonNull);
        Assert.Empty(stored["Content"].AsBsonBinaryData.Bytes);

        var filesCollection = database.GetCollection<BsonDocument>("vex.raw.files");
        var fileCount = await filesCollection.CountDocumentsAsync(FilterDefinition<BsonDocument>.Empty);
        Assert.Equal(1, fileCount);

        var fetched = await store.FindByDigestAsync(document.Digest, CancellationToken.None);
        Assert.NotNull(fetched);
        Assert.Equal(payload, fetched!.Content.ToArray());
    }

    [Fact]
    public async Task RawStore_ReplacesGridFsWithInlinePayload()
    {
        var database = _client.GetDatabase($"vex-raw-inline-{Guid.NewGuid():N}");
        var store = CreateRawStore(database, thresholdBytes: 16);

        var largePayload = Encoding.UTF8.GetBytes(new string('B', 128));
        var digest = "sha256:inline";
        var largeDocument = new VexRawDocument(
            "cisco",
            VexDocumentFormat.CycloneDx,
            new Uri("https://example.com/cyclonedx.json"),
            DateTimeOffset.UtcNow,
            digest,
            largePayload,
            ImmutableDictionary<string, string>.Empty);

        await store.StoreAsync(largeDocument, CancellationToken.None);

        var smallDocument = largeDocument with
        {
            RetrievedAt = DateTimeOffset.UtcNow.AddMinutes(1),
            Content = Encoding.UTF8.GetBytes("small"),
        };

        await store.StoreAsync(smallDocument, CancellationToken.None);

        var rawCollection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Raw);
        var stored = await rawCollection.Find(Builders<BsonDocument>.Filter.Eq("_id", digest))
            .FirstOrDefaultAsync();

        Assert.NotNull(stored);
        Assert.True(stored!.TryGetValue("GridFsObjectId", out var gridId));
        Assert.True(gridId.IsBsonNull);
        Assert.Equal("small", Encoding.UTF8.GetString(stored["Content"].AsBsonBinaryData.Bytes));

        var filesCollection = database.GetCollection<BsonDocument>("vex.raw.files");
        var fileCount = await filesCollection.CountDocumentsAsync(FilterDefinition<BsonDocument>.Empty);
        Assert.Equal(0, fileCount);
    }

    [Fact]
    public async Task ExportStore_SavesManifestAndCacheTransactionally()
    {
        var database = _client.GetDatabase($"vex-export-save-{Guid.NewGuid():N}");
        var options = Options.Create(new VexMongoStorageOptions
        {
            ExportCacheTtl = TimeSpan.FromHours(6),
            GridFsInlineThresholdBytes = 64,
        });

        var store = new MongoVexExportStore(_client, database, options);
        var signature = new VexQuerySignature("format=csaf|provider=redhat");
        var manifest = new VexExportManifest(
            "exports/20251016/redhat",
            signature,
            VexExportFormat.Csaf,
            DateTimeOffset.UtcNow,
            new VexContentAddress("sha256", "abcdef123456"),
            claimCount: 5,
            sourceProviders: new[] { "red-hat" },
            fromCache: false,
            consensusRevision: "rev-1",
            attestation: null,
            sizeBytes: 1024);

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

        var exportsCollection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Exports);
        var exportKey = BuildExportKey(signature, VexExportFormat.Csaf);
        var exportDoc = await exportsCollection.Find(Builders<BsonDocument>.Filter.Eq("_id", exportKey))
            .FirstOrDefaultAsync();
        Assert.NotNull(exportDoc);

        var cacheCollection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Cache);
        var cacheKey = BuildExportKey(signature, VexExportFormat.Csaf);
        var cacheDoc = await cacheCollection.Find(Builders<BsonDocument>.Filter.Eq("_id", cacheKey))
            .FirstOrDefaultAsync();

        Assert.NotNull(cacheDoc);
        Assert.Equal(manifest.ExportId, cacheDoc!["ManifestId"].AsString);
        Assert.True(cacheDoc.TryGetValue("ExpiresAt", out var expiresValue));
        Assert.False(expiresValue.IsBsonNull);
    }

    [Fact]
    public async Task ExportStore_FindAsync_ExpiresCacheEntries()
    {
        var database = _client.GetDatabase($"vex-export-expire-{Guid.NewGuid():N}");
        var options = Options.Create(new VexMongoStorageOptions
        {
            ExportCacheTtl = TimeSpan.FromMinutes(5),
            GridFsInlineThresholdBytes = 64,
        });

        var store = new MongoVexExportStore(_client, database, options);
        var signature = new VexQuerySignature("format=json|provider=cisco");
        var manifest = new VexExportManifest(
            "exports/20251016/cisco",
            signature,
            VexExportFormat.Json,
            DateTimeOffset.UtcNow,
            new VexContentAddress("sha256", "deadbeef"),
            claimCount: 3,
            sourceProviders: new[] { "cisco" },
            fromCache: false,
            consensusRevision: "rev-2",
            attestation: null,
            sizeBytes: 2048);

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

        var cacheCollection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Cache);
        var cacheId = BuildExportKey(signature, VexExportFormat.Json);
        var update = Builders<BsonDocument>.Update.Set("ExpiresAt", DateTime.UtcNow.AddMinutes(-10));
        await cacheCollection.UpdateOneAsync(Builders<BsonDocument>.Filter.Eq("_id", cacheId), update);

        var cached = await store.FindAsync(signature, VexExportFormat.Json, CancellationToken.None);
        Assert.Null(cached);

        var remaining = await cacheCollection.Find(Builders<BsonDocument>.Filter.Eq("_id", cacheId))
            .FirstOrDefaultAsync();
        Assert.Null(remaining);
    }

    private MongoVexRawStore CreateRawStore(IMongoDatabase database, int thresholdBytes)
    {
        var options = Options.Create(new VexMongoStorageOptions
        {
            RawBucketName = "vex.raw",
            GridFsInlineThresholdBytes = thresholdBytes,
            ExportCacheTtl = TimeSpan.FromHours(1),
        });

        return new MongoVexRawStore(_client, database, options);
    }

    private static string BuildExportKey(VexQuerySignature signature, VexExportFormat format)
        => string.Format(CultureInfo.InvariantCulture, "{0}|{1}", signature.Value, format.ToString().ToLowerInvariant());

    public Task InitializeAsync() => Task.CompletedTask;

    public Task DisposeAsync()
    {
        _runner.Dispose();
        return Task.CompletedTask;
    }
}
