using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Feedser.Exporter.Json;
using StellaOps.Feedser.Exporter.TrivyDb;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.Exporting;

namespace StellaOps.Feedser.Exporter.TrivyDb.Tests;

public sealed class TrivyDbFeedExporterTests : IDisposable
{
    private readonly string _root;
    private readonly string _jsonRoot;

    public TrivyDbFeedExporterTests()
    {
        _root = Directory.CreateTempSubdirectory("feedser-trivy-exporter-tests").FullName;
        _jsonRoot = Path.Combine(_root, "tree");
    }

    [Fact]
    public async Task ExportAsync_SortsAdvisoriesByKeyDeterministically()
    {
        var advisoryB = CreateSampleAdvisory("CVE-2024-1002", "Second advisory");
        var advisoryA = CreateSampleAdvisory("CVE-2024-1001", "First advisory");

        var advisoryStore = new StubAdvisoryStore(advisoryB, advisoryA);

        var optionsValue = new TrivyDbExportOptions
        {
            OutputRoot = _root,
            ReferencePrefix = "example/trivy",
            KeepWorkingTree = false,
            Json = new JsonExportOptions
            {
                OutputRoot = _jsonRoot,
                MaintainLatestSymlink = false,
            },
        };

        var options = Options.Create(optionsValue);
        var packageBuilder = new TrivyDbPackageBuilder();
        var ociWriter = new TrivyDbOciWriter();
        var planner = new TrivyDbExportPlanner();
        var stateStore = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-09-20T00:00:00Z", CultureInfo.InvariantCulture));
        var stateManager = new ExportStateManager(stateStore, timeProvider);
        var builderMetadata = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Version = 2,
            NextUpdate = "2024-09-21T00:00:00Z",
            UpdatedAt = "2024-09-20T00:00:00Z",
        });

        var recordingBuilder = new RecordingTrivyDbBuilder(_root, builderMetadata);
        var orasPusher = new StubTrivyDbOrasPusher();
        var exporter = new TrivyDbFeedExporter(
            advisoryStore,
            new VulnListJsonExportPathResolver(),
            options,
            packageBuilder,
            ociWriter,
            stateManager,
            planner,
            recordingBuilder,
            orasPusher,
            NullLogger<TrivyDbFeedExporter>.Instance,
            timeProvider);

        using var provider = new ServiceCollection().BuildServiceProvider();
        await exporter.ExportAsync(provider, CancellationToken.None);

        var paths = recordingBuilder.LastRelativePaths;
        Assert.NotNull(paths);

        var sorted = paths!.OrderBy(static p => p, StringComparer.Ordinal).ToArray();
        Assert.Equal(sorted, paths);

        advisoryStore.SetAdvisories(advisoryA, advisoryB);
        timeProvider.Advance(TimeSpan.FromMinutes(7));
        await exporter.ExportAsync(provider, CancellationToken.None);

        var record = await stateStore.FindAsync(TrivyDbFeedExporter.ExporterId, CancellationToken.None);
        Assert.NotNull(record);
        Assert.Equal("20240920T000000Z", record!.BaseExportId);
        Assert.Single(recordingBuilder.ManifestDigests);
    }

    [Fact]
    public async Task ExportAsync_SmallDatasetProducesDeterministicOciLayout()
    {
        var advisories = new[]
        {
            CreateSampleAdvisory("CVE-2024-3000", "Demo advisory 1"),
            CreateSampleAdvisory("CVE-2024-3001", "Demo advisory 2"),
        };

        var run1 = await RunDeterministicExportAsync(advisories);
        var run2 = await RunDeterministicExportAsync(advisories);

        Assert.Equal(run1.ManifestDigest, run2.ManifestDigest);
        Assert.Equal(run1.IndexJson, run2.IndexJson);
        Assert.Equal(run1.MetadataJson, run2.MetadataJson);
        Assert.Equal(run1.ManifestJson, run2.ManifestJson);

        var digests1 = run1.Blobs.Keys.OrderBy(static d => d, StringComparer.Ordinal).ToArray();
        var digests2 = run2.Blobs.Keys.OrderBy(static d => d, StringComparer.Ordinal).ToArray();
        Assert.Equal(digests1, digests2);

        foreach (var digest in digests1)
        {
            Assert.True(run2.Blobs.TryGetValue(digest, out var other), $"Missing digest {digest} in second run");
            Assert.True(run1.Blobs[digest].SequenceEqual(other), $"Blob {digest} differs between runs");
        }

        using var metadataDoc = JsonDocument.Parse(run1.MetadataJson);
        Assert.Equal(2, metadataDoc.RootElement.GetProperty("advisoryCount").GetInt32());

        using var manifestDoc = JsonDocument.Parse(run1.ManifestJson);
        Assert.Equal(TrivyDbMediaTypes.TrivyConfig, manifestDoc.RootElement.GetProperty("config").GetProperty("mediaType").GetString());
        var layer = manifestDoc.RootElement.GetProperty("layers")[0];
        Assert.Equal(TrivyDbMediaTypes.TrivyLayer, layer.GetProperty("mediaType").GetString());
    }

    [Fact]
    public void ExportOptions_GetExportRoot_NormalizesRelativeRoot()
    {
        var options = new TrivyDbExportOptions
        {
            OutputRoot = Path.Combine("..", "exports", "trivy-test"),
        };

        var exportId = "20240901T000000Z";
        var path = options.GetExportRoot(exportId);

        Assert.True(Path.IsPathRooted(path));
        Assert.EndsWith(Path.Combine("exports", "trivy-test", exportId), path, StringComparison.Ordinal);
    }

    [Fact]
    public async Task ExportAsync_PersistsStateAndSkipsWhenDigestUnchanged()
    {
        var advisory = CreateSampleAdvisory();
        var advisoryStore = new StubAdvisoryStore(advisory);

        var optionsValue = new TrivyDbExportOptions
        {
            OutputRoot = _root,
            ReferencePrefix = "example/trivy",
            Json = new JsonExportOptions
            {
                OutputRoot = _jsonRoot,
                MaintainLatestSymlink = false,
            },
            KeepWorkingTree = false,
        };

        var options = Options.Create(optionsValue);
        var packageBuilder = new TrivyDbPackageBuilder();
        var ociWriter = new TrivyDbOciWriter();
        var planner = new TrivyDbExportPlanner();
        var stateStore = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-09-01T00:00:00Z", CultureInfo.InvariantCulture));
        var stateManager = new ExportStateManager(stateStore, timeProvider);
        var builderMetadata = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Version = 2,
            NextUpdate = "2024-09-02T00:00:00Z",
            UpdatedAt = "2024-09-01T00:00:00Z",
        });
        var builder = new StubTrivyDbBuilder(_root, builderMetadata);
        var orasPusher = new StubTrivyDbOrasPusher();
        var exporter = new TrivyDbFeedExporter(
            advisoryStore,
            new VulnListJsonExportPathResolver(),
            options,
            packageBuilder,
            ociWriter,
            stateManager,
            planner,
            builder,
            orasPusher,
            NullLogger<TrivyDbFeedExporter>.Instance,
            timeProvider);

        using var provider = new ServiceCollection().BuildServiceProvider();
        await exporter.ExportAsync(provider, CancellationToken.None);

        var record = await stateStore.FindAsync(TrivyDbFeedExporter.ExporterId, CancellationToken.None);
        Assert.NotNull(record);
        Assert.Equal("20240901T000000Z", record!.BaseExportId);
        Assert.False(string.IsNullOrEmpty(record.ExportCursor));

        var baseExportId = record.BaseExportId ?? string.Empty;
        Assert.False(string.IsNullOrEmpty(baseExportId));
        var firstExportDirectory = Path.Combine(_root, baseExportId);
        Assert.True(Directory.Exists(firstExportDirectory));

        timeProvider.Advance(TimeSpan.FromMinutes(5));
        await exporter.ExportAsync(provider, CancellationToken.None);

        var updatedRecord = await stateStore.FindAsync(TrivyDbFeedExporter.ExporterId, CancellationToken.None);
        Assert.NotNull(updatedRecord);
        Assert.Equal(record.UpdatedAt, updatedRecord!.UpdatedAt);
        Assert.Equal(record.LastFullDigest, updatedRecord.LastFullDigest);

        var skippedExportDirectory = Path.Combine(_root, "20240901T000500Z");
        Assert.False(Directory.Exists(skippedExportDirectory));

        Assert.Empty(orasPusher.Pushes);
    }

    [Fact]
    public async Task ExportAsync_CreatesOfflineBundle()
    {
        var advisory = CreateSampleAdvisory();
        var advisoryStore = new StubAdvisoryStore(advisory);

        var optionsValue = new TrivyDbExportOptions
        {
            OutputRoot = _root,
            ReferencePrefix = "example/trivy",
            Json = new JsonExportOptions
            {
                OutputRoot = _jsonRoot,
                MaintainLatestSymlink = false,
            },
            KeepWorkingTree = false,
            OfflineBundle = new TrivyDbOfflineBundleOptions
            {
                Enabled = true,
                FileName = "{exportId}.bundle.tar.gz",
            },
        };

        var options = Options.Create(optionsValue);
        var packageBuilder = new TrivyDbPackageBuilder();
        var ociWriter = new TrivyDbOciWriter();
        var planner = new TrivyDbExportPlanner();
        var stateStore = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-09-15T00:00:00Z", CultureInfo.InvariantCulture));
        var stateManager = new ExportStateManager(stateStore, timeProvider);
        var builderMetadata = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Version = 2,
            NextUpdate = "2024-09-16T00:00:00Z",
            UpdatedAt = "2024-09-15T00:00:00Z",
        });
        var builder = new StubTrivyDbBuilder(_root, builderMetadata);
        var orasPusher = new StubTrivyDbOrasPusher();
        var exporter = new TrivyDbFeedExporter(
            advisoryStore,
            new VulnListJsonExportPathResolver(),
            options,
            packageBuilder,
            ociWriter,
            stateManager,
            planner,
            builder,
            orasPusher,
            NullLogger<TrivyDbFeedExporter>.Instance,
            timeProvider);

        using var provider = new ServiceCollection().BuildServiceProvider();
        await exporter.ExportAsync(provider, CancellationToken.None);

        var exportId = "20240915T000000Z";
        var bundlePath = Path.Combine(_root, $"{exportId}.bundle.tar.gz");
        Assert.True(File.Exists(bundlePath));
        Assert.Empty(orasPusher.Pushes);
    }

    [Fact]
    public async Task ExportAsync_SkipsOrasPushWhenDeltaPublishingDisabled()
    {
        var initial = CreateSampleAdvisory("CVE-2024-7100", "Publish toggles");
        var updated = CreateSampleAdvisory("CVE-2024-7100", "Publish toggles delta");
        var advisoryStore = new StubAdvisoryStore(initial);

        var optionsValue = new TrivyDbExportOptions
        {
            OutputRoot = _root,
            ReferencePrefix = "example/trivy",
            Json = new JsonExportOptions
            {
                OutputRoot = _jsonRoot,
                MaintainLatestSymlink = false,
            },
            KeepWorkingTree = true,
        };

        optionsValue.Oras.Enabled = true;
        optionsValue.Oras.PublishFull = false;
        optionsValue.Oras.PublishDelta = false;

        var options = Options.Create(optionsValue);
        var packageBuilder = new TrivyDbPackageBuilder();
        var ociWriter = new TrivyDbOciWriter();
        var planner = new TrivyDbExportPlanner();
        var stateStore = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-10-20T00:00:00Z", CultureInfo.InvariantCulture));
        var stateManager = new ExportStateManager(stateStore, timeProvider);
        var builderMetadata = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Version = 2,
            NextUpdate = "2024-10-21T00:00:00Z",
            UpdatedAt = "2024-10-20T00:00:00Z",
        });
        var builder = new StubTrivyDbBuilder(_root, builderMetadata);
        var orasPusher = new StubTrivyDbOrasPusher();
        var exporter = new TrivyDbFeedExporter(
            advisoryStore,
            new VulnListJsonExportPathResolver(),
            options,
            packageBuilder,
            ociWriter,
            stateManager,
            planner,
            builder,
            orasPusher,
            NullLogger<TrivyDbFeedExporter>.Instance,
            timeProvider);

        using var provider = new ServiceCollection().BuildServiceProvider();
        await exporter.ExportAsync(provider, CancellationToken.None);

        advisoryStore.SetAdvisories(updated);
        timeProvider.Advance(TimeSpan.FromMinutes(15));
        await exporter.ExportAsync(provider, CancellationToken.None);

        Assert.Empty(orasPusher.Pushes);
    }

    [Fact]
    public async Task ExportAsync_SkipsOfflineBundleForDeltaWhenDisabled()
    {
        var initial = CreateSampleAdvisory("CVE-2024-7200", "Offline delta toggles");
        var updated = CreateSampleAdvisory("CVE-2024-7200", "Offline delta toggles updated");
        var advisoryStore = new StubAdvisoryStore(initial);

        var optionsValue = new TrivyDbExportOptions
        {
            OutputRoot = _root,
            ReferencePrefix = "example/trivy",
            Json = new JsonExportOptions
            {
                OutputRoot = _jsonRoot,
                MaintainLatestSymlink = false,
            },
            KeepWorkingTree = true,
            OfflineBundle = new TrivyDbOfflineBundleOptions
            {
                Enabled = true,
                IncludeFull = true,
                IncludeDelta = false,
                FileName = "{exportId}.bundle.tar.gz",
            },
        };

        var options = Options.Create(optionsValue);
        var packageBuilder = new TrivyDbPackageBuilder();
        var ociWriter = new TrivyDbOciWriter();
        var planner = new TrivyDbExportPlanner();
        var stateStore = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-10-21T00:00:00Z", CultureInfo.InvariantCulture));
        var stateManager = new ExportStateManager(stateStore, timeProvider);
        var builderMetadata = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Version = 2,
            NextUpdate = "2024-10-22T00:00:00Z",
            UpdatedAt = "2024-10-21T00:00:00Z",
        });
        var builder = new StubTrivyDbBuilder(_root, builderMetadata);
        var orasPusher = new StubTrivyDbOrasPusher();
        var exporter = new TrivyDbFeedExporter(
            advisoryStore,
            new VulnListJsonExportPathResolver(),
            options,
            packageBuilder,
            ociWriter,
            stateManager,
            planner,
            builder,
            orasPusher,
            NullLogger<TrivyDbFeedExporter>.Instance,
            timeProvider);

        using var provider = new ServiceCollection().BuildServiceProvider();
        await exporter.ExportAsync(provider, CancellationToken.None);

        var fullExportId = timeProvider.GetUtcNow().ToString(optionsValue.TagFormat, CultureInfo.InvariantCulture);
        var fullBundlePath = Path.Combine(_root, $"{fullExportId}.bundle.tar.gz");
        Assert.True(File.Exists(fullBundlePath));

        advisoryStore.SetAdvisories(updated);
        timeProvider.Advance(TimeSpan.FromMinutes(10));
        await exporter.ExportAsync(provider, CancellationToken.None);

        var deltaExportId = timeProvider.GetUtcNow().ToString(optionsValue.TagFormat, CultureInfo.InvariantCulture);
        var deltaBundlePath = Path.Combine(_root, $"{deltaExportId}.bundle.tar.gz");
        Assert.False(File.Exists(deltaBundlePath));
    }

    [Fact]
    public async Task ExportAsync_ResetsBaselineWhenDeltaChainExists()
    {
        var advisory = CreateSampleAdvisory("CVE-2024-5000", "Baseline reset");
        var advisoryStore = new StubAdvisoryStore(advisory);

        var optionsValue = new TrivyDbExportOptions
        {
            OutputRoot = _root,
            ReferencePrefix = "example/trivy",
            Json = new JsonExportOptions
            {
                OutputRoot = _jsonRoot,
                MaintainLatestSymlink = false,
            },
            KeepWorkingTree = false,
            TargetRepository = "registry.example/trivy",
        };

        var options = Options.Create(optionsValue);
        var packageBuilder = new TrivyDbPackageBuilder();
        var ociWriter = new TrivyDbOciWriter();
        var planner = new TrivyDbExportPlanner();
        var stateStore = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-09-22T00:00:00Z", CultureInfo.InvariantCulture));
        var existingRecord = new ExportStateRecord(
            TrivyDbFeedExporter.ExporterId,
            BaseExportId: "20240919T120000Z",
            BaseDigest: "sha256:base",
            LastFullDigest: "sha256:base",
            LastDeltaDigest: "sha256:delta",
            ExportCursor: "sha256:old",
            TargetRepository: "registry.example/trivy",
            ExporterVersion: "0.9.0",
            UpdatedAt: timeProvider.GetUtcNow().AddMinutes(-30),
            Files: Array.Empty<ExportFileRecord>());
        await stateStore.UpsertAsync(existingRecord, CancellationToken.None);

        var stateManager = new ExportStateManager(stateStore, timeProvider);
        var builderMetadata = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Version = 2,
            NextUpdate = "2024-09-23T00:00:00Z",
            UpdatedAt = "2024-09-22T00:00:00Z",
        });
        var builder = new StubTrivyDbBuilder(_root, builderMetadata);
        var orasPusher = new StubTrivyDbOrasPusher();
        var exporter = new TrivyDbFeedExporter(
            advisoryStore,
            new VulnListJsonExportPathResolver(),
            options,
            packageBuilder,
            ociWriter,
            stateManager,
            planner,
            builder,
            orasPusher,
            NullLogger<TrivyDbFeedExporter>.Instance,
            timeProvider);

        using var provider = new ServiceCollection().BuildServiceProvider();
        await exporter.ExportAsync(provider, CancellationToken.None);

        var updated = await stateStore.FindAsync(TrivyDbFeedExporter.ExporterId, CancellationToken.None);
        Assert.NotNull(updated);
        Assert.Equal("20240922T000000Z", updated!.BaseExportId);
        Assert.Equal(updated.BaseDigest, updated.LastFullDigest);
        Assert.Null(updated.LastDeltaDigest);
        Assert.NotEqual("sha256:old", updated.ExportCursor);
        Assert.Equal("registry.example/trivy", updated.TargetRepository);
        Assert.NotEmpty(updated.Files);
    }

    [Fact]
    public async Task ExportAsync_DeltaSequencePromotesBaselineReset()
    {
        var baseline = CreateSampleAdvisory("CVE-2024-8100", "Baseline advisory");
        var firstDelta = CreateSampleAdvisory("CVE-2024-8100", "Baseline advisory updated");
        var secondDelta = CreateSampleAdvisory("CVE-2024-8200", "New advisory triggers full rebuild");

        var advisoryStore = new StubAdvisoryStore(baseline);

        var optionsValue = new TrivyDbExportOptions
        {
            OutputRoot = _root,
            ReferencePrefix = "example/trivy",
            KeepWorkingTree = true,
            Json = new JsonExportOptions
            {
                OutputRoot = _jsonRoot,
                MaintainLatestSymlink = false,
            },
        };

        var options = Options.Create(optionsValue);
        var packageBuilder = new TrivyDbPackageBuilder();
        var ociWriter = new TrivyDbOciWriter();
        var planner = new TrivyDbExportPlanner();
        var stateStore = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-11-01T00:00:00Z", CultureInfo.InvariantCulture));
        var stateManager = new ExportStateManager(stateStore, timeProvider);
        var builderMetadata = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Version = 2,
            NextUpdate = "2024-11-02T00:00:00Z",
            UpdatedAt = "2024-11-01T00:00:00Z",
        });
        var builder = new RecordingTrivyDbBuilder(_root, builderMetadata);
        var orasPusher = new StubTrivyDbOrasPusher();
        var exporter = new TrivyDbFeedExporter(
            advisoryStore,
            new VulnListJsonExportPathResolver(),
            options,
            packageBuilder,
            ociWriter,
            stateManager,
            planner,
            builder,
            orasPusher,
            NullLogger<TrivyDbFeedExporter>.Instance,
            timeProvider);

        using var provider = new ServiceCollection().BuildServiceProvider();

        var initialExportId = timeProvider.GetUtcNow().ToString(optionsValue.TagFormat, CultureInfo.InvariantCulture);
        await exporter.ExportAsync(provider, CancellationToken.None);

        var initialLayout = Path.Combine(optionsValue.OutputRoot, initialExportId);
        var initialMetadata = ReadMetadata(Path.Combine(initialLayout, "metadata.json"));
        Assert.Equal("full", initialMetadata.Mode);
        var initialManifestDigest = ReadManifestDigest(initialLayout);

        advisoryStore.SetAdvisories(firstDelta);
        timeProvider.Advance(TimeSpan.FromMinutes(15));
        var deltaExportId = timeProvider.GetUtcNow().ToString(optionsValue.TagFormat, CultureInfo.InvariantCulture);
        await exporter.ExportAsync(provider, CancellationToken.None);

        var deltaLayout = Path.Combine(optionsValue.OutputRoot, deltaExportId);
        var deltaMetadata = ReadMetadata(Path.Combine(deltaLayout, "metadata.json"));
        Assert.Equal("delta", deltaMetadata.Mode);
        Assert.Equal(initialExportId, deltaMetadata.BaseExportId);
        Assert.Equal(initialManifestDigest, deltaMetadata.BaseManifestDigest);
        Assert.True(deltaMetadata.DeltaChangedCount > 0);

        var reusedManifestPath = Path.Combine(deltaLayout, "blobs", "sha256", initialManifestDigest[7..]);
        Assert.True(File.Exists(reusedManifestPath));

        advisoryStore.SetAdvisories(secondDelta);
        timeProvider.Advance(TimeSpan.FromMinutes(15));
        var finalExportId = timeProvider.GetUtcNow().ToString(optionsValue.TagFormat, CultureInfo.InvariantCulture);
        await exporter.ExportAsync(provider, CancellationToken.None);

        var finalLayout = Path.Combine(optionsValue.OutputRoot, finalExportId);
        var finalMetadata = ReadMetadata(Path.Combine(finalLayout, "metadata.json"));
        Assert.Equal("full", finalMetadata.Mode);
        Assert.True(finalMetadata.ResetBaseline);

        var state = await stateStore.FindAsync(TrivyDbFeedExporter.ExporterId, CancellationToken.None);
        Assert.NotNull(state);
        Assert.Null(state!.LastDeltaDigest);
        Assert.Equal(finalExportId, state.BaseExportId);
    }

    [Fact]
    public async Task ExportAsync_DeltaReusesBaseLayerOnDisk()
    {
        var baseline = CreateSampleAdvisory("CVE-2024-8300", "Layer reuse baseline");
        var delta = CreateSampleAdvisory("CVE-2024-8300", "Layer reuse delta");

        var advisoryStore = new StubAdvisoryStore(baseline);

        var optionsValue = new TrivyDbExportOptions
        {
            OutputRoot = _root,
            ReferencePrefix = "example/trivy",
            KeepWorkingTree = true,
            Json = new JsonExportOptions
            {
                OutputRoot = _jsonRoot,
                MaintainLatestSymlink = false,
            },
        };

        var options = Options.Create(optionsValue);
        var packageBuilder = new TrivyDbPackageBuilder();
        var ociWriter = new TrivyDbOciWriter();
        var planner = new TrivyDbExportPlanner();
        var stateStore = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-11-05T00:00:00Z", CultureInfo.InvariantCulture));
        var stateManager = new ExportStateManager(stateStore, timeProvider);
        var builderMetadata = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Version = 2,
            NextUpdate = "2024-11-06T00:00:00Z",
            UpdatedAt = "2024-11-05T00:00:00Z",
        });
        var builder = new RecordingTrivyDbBuilder(_root, builderMetadata);
        var orasPusher = new StubTrivyDbOrasPusher();
        var exporter = new TrivyDbFeedExporter(
            advisoryStore,
            new VulnListJsonExportPathResolver(),
            options,
            packageBuilder,
            ociWriter,
            stateManager,
            planner,
            builder,
            orasPusher,
            NullLogger<TrivyDbFeedExporter>.Instance,
            timeProvider);

        using var provider = new ServiceCollection().BuildServiceProvider();

        var baselineExportId = timeProvider.GetUtcNow().ToString(optionsValue.TagFormat, CultureInfo.InvariantCulture);
        await exporter.ExportAsync(provider, CancellationToken.None);

        var baselineLayout = Path.Combine(optionsValue.OutputRoot, baselineExportId);
        var baselineManifestDigest = ReadManifestDigest(baselineLayout);
        var baselineLayerDigests = ReadManifestLayerDigests(baselineLayout, baselineManifestDigest);
        var baselineLayerDigest = Assert.Single(baselineLayerDigests);
        var baselineLayerPath = Path.Combine(baselineLayout, "blobs", "sha256", baselineLayerDigest[7..]);
        var baselineLayerBytes = File.ReadAllBytes(baselineLayerPath);

        advisoryStore.SetAdvisories(delta);
        timeProvider.Advance(TimeSpan.FromMinutes(30));
        var deltaExportId = timeProvider.GetUtcNow().ToString(optionsValue.TagFormat, CultureInfo.InvariantCulture);
        await exporter.ExportAsync(provider, CancellationToken.None);

        var deltaLayout = Path.Combine(optionsValue.OutputRoot, deltaExportId);
        var deltaMetadata = ReadMetadata(Path.Combine(deltaLayout, "metadata.json"));
        Assert.Equal("delta", deltaMetadata.Mode);
        Assert.Equal(baselineExportId, deltaMetadata.BaseExportId);
        Assert.Equal(baselineManifestDigest, deltaMetadata.BaseManifestDigest);
        Assert.True(deltaMetadata.DeltaChangedCount > 0);

        var deltaManifestDigest = ReadManifestDigest(deltaLayout);
        Assert.NotEqual(baselineManifestDigest, deltaManifestDigest);
        var deltaLayerDigests = ReadManifestLayerDigests(deltaLayout, deltaManifestDigest);
        Assert.Contains(baselineLayerDigest, deltaLayerDigests);

        var deltaLayerPath = Path.Combine(deltaLayout, "blobs", "sha256", baselineLayerDigest[7..]);
        Assert.True(File.Exists(deltaLayerPath));
        var deltaLayerBytes = File.ReadAllBytes(deltaLayerPath);
        Assert.Equal(baselineLayerBytes, deltaLayerBytes);
    }

    private static Advisory CreateSampleAdvisory(
        string advisoryKey = "CVE-2024-9999",
        string title = "Trivy Export Test")
    {
        var published = DateTimeOffset.Parse("2024-08-01T00:00:00Z", CultureInfo.InvariantCulture);
        var modified = DateTimeOffset.Parse("2024-08-02T00:00:00Z", CultureInfo.InvariantCulture);
        var reference = new AdvisoryReference(
            "https://example.org/advisories/CVE-2024-9999",
            kind: "advisory",
            sourceTag: "EXAMPLE",
            summary: null,
            provenance: new AdvisoryProvenance("ghsa", "map", "CVE-2024-9999", modified, new[] { ProvenanceFieldMasks.References }));
        var weakness = new AdvisoryWeakness(
            taxonomy: "cwe",
            identifier: "CWE-89",
            name: "SQL Injection",
            uri: "https://cwe.mitre.org/data/definitions/89.html",
            provenance: new[] { new AdvisoryProvenance("nvd", "map", "CWE-89", modified, new[] { ProvenanceFieldMasks.Weaknesses }) });
        var cvssMetric = new CvssMetric(
            "3.1",
            "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
            9.8,
            "critical",
            new AdvisoryProvenance("nvd", "map", "CVE-2024-9999", modified, new[] { ProvenanceFieldMasks.CvssMetrics }));

        return new Advisory(
            advisoryKey: advisoryKey,
            title: title,
            summary: "Trivy export fixture",
            language: "en",
            published: published,
            modified: modified,
            severity: "medium",
            exploitKnown: false,
            aliases: new[] { "CVE-2024-9999" },
            credits: Array.Empty<AdvisoryCredit>(),
            references: new[] { reference },
            affectedPackages: Array.Empty<AffectedPackage>(),
            cvssMetrics: new[] { cvssMetric },
            provenance: new[] { new AdvisoryProvenance("nvd", "map", "CVE-2024-9999", modified, new[] { ProvenanceFieldMasks.Advisory }) },
            description: "Detailed description for Trivy exporter testing.",
            cwes: new[] { weakness },
            canonicalMetricId: "3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H");
    }

    public void Dispose()
    {
        try
        {
            if (Directory.Exists(_root))
            {
                Directory.Delete(_root, recursive: true);
            }
        }
        catch
        {
            // best effort cleanup
        }
    }

    private sealed class StubAdvisoryStore : IAdvisoryStore
    {
        private IReadOnlyList<Advisory> _advisories;

        public StubAdvisoryStore(params Advisory[] advisories)
        {
            _advisories = advisories;
        }

        public void SetAdvisories(params Advisory[] advisories)
        {
            _advisories = advisories;
        }

        public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
            => Task.FromResult(_advisories);

        public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
            => Task.FromResult<Advisory?>(_advisories.FirstOrDefault(a => a.AdvisoryKey == advisoryKey));

        public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
            => Task.CompletedTask;

        public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken)
        {
            return EnumerateAsync(cancellationToken);

            async IAsyncEnumerable<Advisory> EnumerateAsync([EnumeratorCancellation] CancellationToken ct)
            {
                foreach (var advisory in _advisories)
                {
                    ct.ThrowIfCancellationRequested();
                    yield return advisory;
                    await Task.Yield();
                }
            }
        }
    }

    private sealed class InMemoryExportStateStore : IExportStateStore
    {
        private ExportStateRecord? _record;

        public Task<ExportStateRecord?> FindAsync(string id, CancellationToken cancellationToken)
            => Task.FromResult(_record);

        public Task<ExportStateRecord> UpsertAsync(ExportStateRecord record, CancellationToken cancellationToken)
        {
            _record = record;
            return Task.FromResult(record);
        }
    }

    private sealed class TestTimeProvider : TimeProvider
    {
        private DateTimeOffset _now;

        public TestTimeProvider(DateTimeOffset start) => _now = start;

        public override DateTimeOffset GetUtcNow() => _now;

        public void Advance(TimeSpan delta) => _now = _now.Add(delta);
    }

    private sealed class StubTrivyDbBuilder : ITrivyDbBuilder
    {
        private readonly string _root;
        private readonly byte[] _metadata;

        public StubTrivyDbBuilder(string root, byte[] metadata)
        {
            _root = root;
            _metadata = metadata;
        }

        public Task<TrivyDbBuilderResult> BuildAsync(
            JsonExportResult jsonTree,
            DateTimeOffset exportedAt,
            string exportId,
            CancellationToken cancellationToken)
        {
            var workingDirectory = Directory.CreateDirectory(Path.Combine(_root, $"builder-{exportId}")).FullName;
            var archivePath = Path.Combine(workingDirectory, "db.tar.gz");
            var payload = new byte[] { 0x1, 0x2, 0x3, 0x4 };
            File.WriteAllBytes(archivePath, payload);
            using var sha256 = SHA256.Create();
            var digest = "sha256:" + Convert.ToHexString(sha256.ComputeHash(payload)).ToLowerInvariant();
            var length = payload.Length;

            return Task.FromResult(new TrivyDbBuilderResult(
                archivePath,
                digest,
                length,
                _metadata,
                workingDirectory));
        }
    }

    private sealed class RecordingTrivyDbBuilder : ITrivyDbBuilder
    {
        private readonly string _root;
        private readonly byte[] _metadata;
        private readonly List<string> _manifestDigests = new();

        public RecordingTrivyDbBuilder(string root, byte[] metadata)
        {
            _root = root;
            _metadata = metadata;
        }

        public IReadOnlyList<string> ManifestDigests => _manifestDigests;
        public string[]? LastRelativePaths { get; private set; }

        public Task<TrivyDbBuilderResult> BuildAsync(
            JsonExportResult jsonTree,
            DateTimeOffset exportedAt,
            string exportId,
            CancellationToken cancellationToken)
        {
            LastRelativePaths = jsonTree.Files.Select(static file => file.RelativePath).ToArray();

            var workingDirectory = Directory.CreateDirectory(Path.Combine(_root, $"builder-{exportId}")).FullName;
            var archivePath = Path.Combine(workingDirectory, "db.tar.gz");
            var payload = new byte[] { 0x5, 0x6, 0x7, 0x8 };
            File.WriteAllBytes(archivePath, payload);
            using var sha256 = SHA256.Create();
            var digest = "sha256:" + Convert.ToHexString(sha256.ComputeHash(payload)).ToLowerInvariant();
            _manifestDigests.Add(digest);

            return Task.FromResult(new TrivyDbBuilderResult(
                archivePath,
                digest,
                payload.Length,
                _metadata,
                workingDirectory));
        }
    }

    private sealed record MetadataView(string Mode, bool ResetBaseline, string? BaseExportId, string? BaseManifestDigest, int DeltaChangedCount);

    private static MetadataView ReadMetadata(string path)
    {
        using var document = JsonDocument.Parse(File.ReadAllText(path));
        var root = document.RootElement;
        var mode = root.TryGetProperty("mode", out var modeNode) ? modeNode.GetString() ?? string.Empty : string.Empty;
        var resetBaseline = root.TryGetProperty("resetBaseline", out var resetNode) && resetNode.ValueKind == JsonValueKind.True;
        string? baseExportId = null;
        if (root.TryGetProperty("baseExportId", out var baseExportNode) && baseExportNode.ValueKind == JsonValueKind.String)
        {
            baseExportId = baseExportNode.GetString();
        }

        string? baseManifestDigest = null;
        if (root.TryGetProperty("baseManifestDigest", out var baseManifestNode) && baseManifestNode.ValueKind == JsonValueKind.String)
        {
            baseManifestDigest = baseManifestNode.GetString();
        }

        var deltaChangedCount = 0;
        if (root.TryGetProperty("delta", out var deltaNode) && deltaNode.ValueKind == JsonValueKind.Object)
        {
            if (deltaNode.TryGetProperty("changedFiles", out var changedFilesNode) && changedFilesNode.ValueKind == JsonValueKind.Array)
            {
                deltaChangedCount = changedFilesNode.GetArrayLength();
            }
        }

        return new MetadataView(mode, resetBaseline, baseExportId, baseManifestDigest, deltaChangedCount);
    }

    private static string ReadManifestDigest(string layoutPath)
    {
        var indexPath = Path.Combine(layoutPath, "index.json");
        using var document = JsonDocument.Parse(File.ReadAllText(indexPath));
        var manifests = document.RootElement.GetProperty("manifests");
        if (manifests.GetArrayLength() == 0)
        {
            throw new InvalidOperationException("No manifests present in OCI index.");
        }

        return manifests[0].GetProperty("digest").GetString() ?? string.Empty;
    }

    private static string[] ReadManifestLayerDigests(string layoutPath, string manifestDigest)
    {
        var manifestPath = Path.Combine(layoutPath, "blobs", "sha256", manifestDigest[7..]);
        using var document = JsonDocument.Parse(File.ReadAllText(manifestPath));
        var layers = document.RootElement.GetProperty("layers");
        var digests = new string[layers.GetArrayLength()];
        var index = 0;
        foreach (var layer in layers.EnumerateArray())
        {
            digests[index++] = layer.GetProperty("digest").GetString() ?? string.Empty;
        }

        return digests;
    }

    private sealed record RunArtifacts(
        string ExportId,
        string ManifestDigest,
        string IndexJson,
        string MetadataJson,
        string ManifestJson,
        IReadOnlyDictionary<string, byte[]> Blobs);

    private async Task<RunArtifacts> RunDeterministicExportAsync(IReadOnlyList<Advisory> advisories)
    {
        var workspace = Path.Combine(_root, $"deterministic-{Guid.NewGuid():N}");
        var jsonRoot = Path.Combine(workspace, "tree");
        Directory.CreateDirectory(workspace);

        var advisoryStore = new StubAdvisoryStore(advisories.ToArray());

        var optionsValue = new TrivyDbExportOptions
        {
            OutputRoot = workspace,
            ReferencePrefix = "example/trivy",
            KeepWorkingTree = true,
            Json = new JsonExportOptions
            {
                OutputRoot = jsonRoot,
                MaintainLatestSymlink = false,
            },
        };

        var exportedAt = DateTimeOffset.Parse("2024-10-01T00:00:00Z", CultureInfo.InvariantCulture);
        var options = Options.Create(optionsValue);
        var packageBuilder = new TrivyDbPackageBuilder();
        var ociWriter = new TrivyDbOciWriter();
        var planner = new TrivyDbExportPlanner();
        var stateStore = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(exportedAt);
        var stateManager = new ExportStateManager(stateStore, timeProvider);
        var builderMetadata = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Version = 2,
            NextUpdate = "2024-10-02T00:00:00Z",
            UpdatedAt = "2024-10-01T00:00:00Z",
        });

        var builder = new DeterministicTrivyDbBuilder(workspace, builderMetadata);
        var orasPusher = new StubTrivyDbOrasPusher();
        var exporter = new TrivyDbFeedExporter(
            advisoryStore,
            new VulnListJsonExportPathResolver(),
            options,
            packageBuilder,
            ociWriter,
            stateManager,
            planner,
            builder,
            orasPusher,
            NullLogger<TrivyDbFeedExporter>.Instance,
            timeProvider);

        using var provider = new ServiceCollection().BuildServiceProvider();
        await exporter.ExportAsync(provider, CancellationToken.None);

        var exportId = exportedAt.ToString(optionsValue.TagFormat, CultureInfo.InvariantCulture);
        var layoutPath = Path.Combine(workspace, exportId);

        var indexJson = await File.ReadAllTextAsync(Path.Combine(layoutPath, "index.json"), Encoding.UTF8);
        var metadataJson = await File.ReadAllTextAsync(Path.Combine(layoutPath, "metadata.json"), Encoding.UTF8);

        using var indexDoc = JsonDocument.Parse(indexJson);
        var manifestNode = indexDoc.RootElement.GetProperty("manifests")[0];
        var manifestDigest = manifestNode.GetProperty("digest").GetString()!;

        var manifestHex = manifestDigest[7..];
        var manifestJson = await File.ReadAllTextAsync(Path.Combine(layoutPath, "blobs", "sha256", manifestHex), Encoding.UTF8);

        var blobs = new Dictionary<string, byte[]>(StringComparer.Ordinal);
        var blobsRoot = Path.Combine(layoutPath, "blobs", "sha256");
        foreach (var file in Directory.GetFiles(blobsRoot))
        {
            var name = Path.GetFileName(file);
            var content = await File.ReadAllBytesAsync(file);
            blobs[name] = content;
        }

        Directory.Delete(workspace, recursive: true);

        return new RunArtifacts(exportId, manifestDigest, indexJson, metadataJson, manifestJson, blobs);
    }

    private sealed class DeterministicTrivyDbBuilder : ITrivyDbBuilder
    {
        private readonly string _root;
        private readonly byte[] _metadata;
        private readonly byte[] _payload;

        public DeterministicTrivyDbBuilder(string root, byte[] metadata)
        {
            _root = root;
            _metadata = metadata;
            _payload = new byte[] { 0x21, 0x22, 0x23, 0x24, 0x25 };
        }

        public Task<TrivyDbBuilderResult> BuildAsync(
            JsonExportResult jsonTree,
            DateTimeOffset exportedAt,
            string exportId,
            CancellationToken cancellationToken)
        {
            var workingDirectory = Directory.CreateDirectory(Path.Combine(_root, $"builder-{exportId}")).FullName;
            var archivePath = Path.Combine(workingDirectory, "db.tar.gz");
            File.WriteAllBytes(archivePath, _payload);
            using var sha256 = SHA256.Create();
            var digest = "sha256:" + Convert.ToHexString(sha256.ComputeHash(_payload)).ToLowerInvariant();

            return Task.FromResult(new TrivyDbBuilderResult(
                archivePath,
                digest,
                _payload.Length,
                _metadata,
                workingDirectory));
        }
    }

    private sealed class StubTrivyDbOrasPusher : ITrivyDbOrasPusher
    {
        public List<(string Layout, string Reference, string ExportId)> Pushes { get; } = new();

        public Task PushAsync(string layoutPath, string reference, string exportId, CancellationToken cancellationToken)
        {
            Pushes.Add((layoutPath, reference, exportId));
            return Task.CompletedTask;
        }
    }
}
