using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Bson;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Nvd;
using StellaOps.Feedser.Source.Nvd.Configuration;
using StellaOps.Feedser.Source.Nvd.Internal;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;
using StellaOps.Feedser.Storage.Mongo.ChangeHistory;
using StellaOps.Feedser.Testing;

namespace StellaOps.Feedser.Source.Nvd.Tests;

[Collection("mongo-fixture")]
public sealed class NvdConnectorTests : IAsyncLifetime
{
    private readonly MongoIntegrationFixture _fixture;
    private FakeTimeProvider _timeProvider;
    private readonly DateTimeOffset _initialNow;
    private readonly CannedHttpMessageHandler _handler;
    private ServiceProvider? _serviceProvider;

    public NvdConnectorTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
        _initialNow = new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero);
        _timeProvider = new FakeTimeProvider(_initialNow);
        _handler = new CannedHttpMessageHandler();
    }

    [Fact]
    public async Task FetchParseMap_FlowProducesCanonicalAdvisories()
    {
        await ResetDatabaseAsync();

        var options = new NvdOptions
        {
            BaseEndpoint = new Uri("https://nvd.example.test/api"),
            WindowSize = TimeSpan.FromHours(1),
            WindowOverlap = TimeSpan.FromMinutes(5),
            InitialBackfill = TimeSpan.FromHours(2),
        };

        var window1Start = _timeProvider.GetUtcNow() - options.InitialBackfill;
        var window1End = window1Start + options.WindowSize;
        _handler.AddJsonResponse(BuildRequestUri(options, window1Start, window1End), ReadFixture("nvd-window-1.json"));

        await EnsureServiceProviderAsync(options);
        var provider = _serviceProvider!;

        var connector = new NvdConnectorPlugin().Create(provider);

        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
        Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0001");
        Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0002");

        var cve1 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0001");
        var package1 = Assert.Single(cve1.AffectedPackages);
        var range1 = Assert.Single(package1.VersionRanges);
        Assert.Equal("cpe", range1.RangeKind);
        Assert.Equal("1.0", range1.IntroducedVersion);
        Assert.Null(range1.FixedVersion);
        Assert.Equal("1.0", range1.LastAffectedVersion);
        Assert.Equal("==1.0", range1.RangeExpression);
        Assert.NotNull(range1.Primitives);
        Assert.Equal("1.0", range1.Primitives!.VendorExtensions!["version"]);
        Assert.Contains(cve1.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-79");
        var cvss1 = Assert.Single(cve1.CvssMetrics);
        Assert.Equal("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", cvss1.Provenance.Value);

        var cve2 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0002");
        var package2 = Assert.Single(cve2.AffectedPackages);
        var range2 = Assert.Single(package2.VersionRanges);
        Assert.Equal("cpe", range2.RangeKind);
        Assert.Equal("2.0", range2.IntroducedVersion);
        Assert.Null(range2.FixedVersion);
        Assert.Equal("2.0", range2.LastAffectedVersion);
        Assert.Equal("==2.0", range2.RangeExpression);
        Assert.NotNull(range2.Primitives);
        Assert.Equal("2.0", range2.Primitives!.VendorExtensions!["version"]);
        Assert.Contains(cve2.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-89");
        var cvss2 = Assert.Single(cve2.CvssMetrics);
        Assert.Equal("CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L", cvss2.Provenance.Value);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        var cursorDocument = state!.Cursor;
        Assert.NotNull(cursorDocument);
        var lastWindowEnd = cursorDocument.TryGetValue("windowEnd", out var endValue) ? ReadDateTime(endValue) : (DateTimeOffset?)null;
        Assert.Equal(window1End.UtcDateTime, lastWindowEnd?.UtcDateTime);

        _timeProvider.Advance(TimeSpan.FromHours(1));
        var now = _timeProvider.GetUtcNow();
        var startCandidate = (lastWindowEnd ?? window1End) - options.WindowOverlap;
        var backfillLimit = now - options.InitialBackfill;
        var window2Start = startCandidate < backfillLimit ? backfillLimit : startCandidate;
        var window2End = window2Start + options.WindowSize;
        if (window2End > now)
        {
            window2End = now;
        }

        _handler.AddJsonResponse(BuildRequestUri(options, window2Start, window2End), ReadFixture("nvd-window-2.json"));

        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
        Assert.Equal(3, advisories.Count);
        Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0003");
        var cve3 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0003");
        var package3 = Assert.Single(cve3.AffectedPackages);
        var range3 = Assert.Single(package3.VersionRanges);
        Assert.Equal("3.5", range3.IntroducedVersion);
        Assert.Equal("3.5", range3.LastAffectedVersion);
        Assert.Equal("==3.5", range3.RangeExpression);
        Assert.NotNull(range3.Primitives);
        Assert.Equal("3.5", range3.Primitives!.VendorExtensions!["version"]);

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var finalState = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(finalState);
        var pendingDocuments = finalState!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
            ? pendingDocs.AsBsonArray
            : new BsonArray();
        Assert.Empty(pendingDocuments);
    }

    [Fact]
    public async Task FetchAsync_MultiPageWindowFetchesAllPages()
    {
        await ResetDatabaseAsync();

        var options = new NvdOptions
        {
            BaseEndpoint = new Uri("https://nvd.example.test/api"),
            WindowSize = TimeSpan.FromHours(1),
            WindowOverlap = TimeSpan.FromMinutes(5),
            InitialBackfill = TimeSpan.FromHours(2),
        };

        var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
        var windowEnd = windowStart + options.WindowSize;

        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd), ReadFixture("nvd-multipage-1.json"));
        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 2), ReadFixture("nvd-multipage-2.json"));
        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));

        await EnsureServiceProviderAsync(options);
        var provider = _serviceProvider!;
        var connector = new NvdConnectorPlugin().Create(provider);

        await connector.FetchAsync(provider, CancellationToken.None);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
            ? pendingDocs.AsBsonArray.Select(v => Guid.Parse(v.AsString)).ToArray()
            : Array.Empty<Guid>();
        Assert.Equal(3, pendingDocuments.Length);

        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
        var advisoryKeys = advisories.Select(advisory => advisory.AdvisoryKey).OrderBy(k => k).ToArray();

        Assert.Equal(new[] { "CVE-2024-1000", "CVE-2024-1001", "CVE-2024-1002", "CVE-2024-1003", "CVE-2024-1004" }, advisoryKeys);
    }

    [Fact]
    public async Task Observability_RecordsCountersForSuccessfulFlow()
    {
        await ResetDatabaseAsync();

        var options = new NvdOptions
        {
            BaseEndpoint = new Uri("https://nvd.example.test/api"),
            WindowSize = TimeSpan.FromHours(1),
            WindowOverlap = TimeSpan.FromMinutes(5),
            InitialBackfill = TimeSpan.FromHours(2),
        };

        using var collector = new MetricCollector(NvdDiagnostics.MeterName);

        var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
        var windowEnd = windowStart + options.WindowSize;

        var handler = new CannedHttpMessageHandler();
        handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd), ReadFixture("nvd-multipage-1.json"));
        handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 2), ReadFixture("nvd-multipage-2.json"));
        handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
        handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
        handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));

        await using var provider = await CreateServiceProviderAsync(options, handler);
        var connector = new NvdConnectorPlugin().Create(provider);

        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        Assert.Equal(3, collector.GetValue("nvd.fetch.attempts"));
        Assert.Equal(3, collector.GetValue("nvd.fetch.documents"));
        Assert.Equal(0, collector.GetValue("nvd.fetch.failures"));
        Assert.Equal(0, collector.GetValue("nvd.fetch.unchanged"));
        Assert.Equal(3, collector.GetValue("nvd.parse.success"));
        Assert.Equal(0, collector.GetValue("nvd.parse.failures"));
        Assert.Equal(0, collector.GetValue("nvd.parse.quarantine"));
        Assert.Equal(5, collector.GetValue("nvd.map.success"));
    }

    [Fact]
    public async Task ChangeHistory_RecordsDifferencesForModifiedCve()
    {
        await ResetDatabaseAsync();

        var options = new NvdOptions
        {
            BaseEndpoint = new Uri("https://nvd.example.test/api"),
            WindowSize = TimeSpan.FromHours(1),
            WindowOverlap = TimeSpan.FromMinutes(5),
            InitialBackfill = TimeSpan.FromHours(2),
        };

        var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
        var windowEnd = windowStart + options.WindowSize;
        _handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd), ReadFixture("nvd-window-1.json"));

        await EnsureServiceProviderAsync(options);
        var provider = _serviceProvider!;
        var connector = new NvdConnectorPlugin().Create(provider);

        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var historyStore = provider.GetRequiredService<IChangeHistoryStore>();
        var historyEntries = await historyStore.GetRecentAsync("nvd", "CVE-2024-0001", 5, CancellationToken.None);
        Assert.Empty(historyEntries);

        _timeProvider.Advance(TimeSpan.FromHours(2));
        var now = _timeProvider.GetUtcNow();

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);

        var cursorDocument = state!.Cursor;
        var lastWindowEnd = cursorDocument.TryGetValue("windowEnd", out var endValue) ? ReadDateTime(endValue) : (DateTimeOffset?)null;
        var startCandidate = (lastWindowEnd ?? windowEnd) - options.WindowOverlap;
        var backfillLimit = now - options.InitialBackfill;
        var window2Start = startCandidate < backfillLimit ? backfillLimit : startCandidate;
        var window2End = window2Start + options.WindowSize;
        if (window2End > now)
        {
            window2End = now;
        }

        _handler.AddJsonResponse(BuildRequestUri(options, window2Start, window2End), ReadFixture("nvd-window-update.json"));

        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var updatedAdvisory = await advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
        Assert.NotNull(updatedAdvisory);
        Assert.Equal("high", updatedAdvisory!.Severity);

        historyEntries = await historyStore.GetRecentAsync("nvd", "CVE-2024-0001", 5, CancellationToken.None);
        Assert.NotEmpty(historyEntries);
        var latest = historyEntries[0];
        Assert.Equal("nvd", latest.SourceName);
        Assert.Equal("CVE-2024-0001", latest.AdvisoryKey);
        Assert.NotNull(latest.PreviousHash);
        Assert.NotEqual(latest.PreviousHash, latest.CurrentHash);
        Assert.Contains(latest.Changes, change => change.Field == "severity" && change.ChangeType == "Modified");
        Assert.Contains(latest.Changes, change => change.Field == "references" && change.ChangeType == "Modified");
    }

    [Fact]
    public async Task ParseAsync_InvalidSchema_QuarantinesDocumentAndEmitsMetric()
    {
        await ResetDatabaseAsync();

        var options = new NvdOptions
        {
            BaseEndpoint = new Uri("https://nvd.example.test/api"),
            WindowSize = TimeSpan.FromHours(1),
            WindowOverlap = TimeSpan.FromMinutes(5),
            InitialBackfill = TimeSpan.FromHours(2),
        };

        using var collector = new MetricCollector(NvdDiagnostics.MeterName);

        var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
        var windowEnd = windowStart + options.WindowSize;
        var requestUri = BuildRequestUri(options, windowStart, windowEnd);

        _handler.AddJsonResponse(requestUri, ReadFixture("nvd-invalid-schema.json"));

        await EnsureServiceProviderAsync(options);
        var provider = _serviceProvider!;
        var connector = new NvdConnectorPlugin().Create(provider);

        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var document = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, requestUri.ToString(), CancellationToken.None);
        Assert.NotNull(document);
        Assert.Equal(DocumentStatuses.Failed, document!.Status);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        var pendingDocs = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue) ? pendingDocsValue.AsBsonArray : new BsonArray();
        Assert.Empty(pendingDocs);
        var pendingMappings = state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue) ? pendingMappingsValue.AsBsonArray : new BsonArray();
        Assert.Empty(pendingMappings);

        Assert.Equal(1, collector.GetValue("nvd.fetch.documents"));
        Assert.Equal(0, collector.GetValue("nvd.parse.success"));
        Assert.Equal(1, collector.GetValue("nvd.parse.quarantine"));
        Assert.Equal(0, collector.GetValue("nvd.map.success"));
    }

    [Fact]
    public async Task ResetDatabase_IsolatesRuns()
    {
        await ResetDatabaseAsync();

        var options = new NvdOptions
        {
            BaseEndpoint = new Uri("https://nvd.example.test/api"),
            WindowSize = TimeSpan.FromHours(1),
            WindowOverlap = TimeSpan.FromMinutes(5),
            InitialBackfill = TimeSpan.FromHours(2),
        };

        var start = _timeProvider.GetUtcNow() - options.InitialBackfill;
        var end = start + options.WindowSize;
        _handler.AddJsonResponse(BuildRequestUri(options, start, end), ReadFixture("nvd-window-1.json"));

        await EnsureServiceProviderAsync(options);
        var provider = _serviceProvider!;
        var connector = new NvdConnectorPlugin().Create(provider);

        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var firstRunKeys = (await advisoryStore.GetRecentAsync(10, CancellationToken.None))
            .Select(advisory => advisory.AdvisoryKey)
            .OrderBy(k => k)
            .ToArray();
        Assert.Equal(new[] { "CVE-2024-0001", "CVE-2024-0002" }, firstRunKeys);

        await ResetDatabaseAsync();

        options = new NvdOptions
        {
            BaseEndpoint = new Uri("https://nvd.example.test/api"),
            WindowSize = TimeSpan.FromHours(1),
            WindowOverlap = TimeSpan.FromMinutes(5),
            InitialBackfill = TimeSpan.FromHours(2),
        };

        start = _timeProvider.GetUtcNow() - options.InitialBackfill;
        end = start + options.WindowSize;
        _handler.AddJsonResponse(BuildRequestUri(options, start, end), ReadFixture("nvd-window-2.json"));

        await EnsureServiceProviderAsync(options);
        provider = _serviceProvider!;
        connector = new NvdConnectorPlugin().Create(provider);

        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var secondRunKeys = (await advisoryStore.GetRecentAsync(10, CancellationToken.None))
            .Select(advisory => advisory.AdvisoryKey)
            .OrderBy(k => k)
            .ToArray();
        Assert.Equal(new[] { "CVE-2024-0003" }, secondRunKeys);
    }

    private async Task EnsureServiceProviderAsync(NvdOptions options)
    {
        if (_serviceProvider is not null)
        {
            return;
        }

        _serviceProvider = await CreateServiceProviderAsync(options, _handler);
    }

    [Fact]
    public async Task Resume_CompletesPendingDocumentsAfterRestart()
    {
        await ResetDatabaseAsync();

        var options = new NvdOptions
        {
            BaseEndpoint = new Uri("https://nvd.example.test/api"),
            WindowSize = TimeSpan.FromHours(1),
            WindowOverlap = TimeSpan.FromMinutes(5),
            InitialBackfill = TimeSpan.FromHours(2),
        };

        var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
        var windowEnd = windowStart + options.WindowSize;
        var requestUri = BuildRequestUri(options, windowStart, windowEnd);

        var fetchHandler = new CannedHttpMessageHandler();
        fetchHandler.AddJsonResponse(requestUri, ReadFixture("nvd-window-1.json"));

        Guid[] pendingDocumentIds;
        await using (var fetchProvider = await CreateServiceProviderAsync(options, fetchHandler))
        {
            var connector = new NvdConnectorPlugin().Create(fetchProvider);
            await connector.FetchAsync(fetchProvider, CancellationToken.None);

            var stateRepository = fetchProvider.GetRequiredService<ISourceStateRepository>();
            var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
            Assert.NotNull(state);
            var pending = state!.Cursor.TryGetValue("pendingDocuments", out var value)
                ? value.AsBsonArray
                : new BsonArray();
            Assert.NotEmpty(pending);
            pendingDocumentIds = pending.Select(v => Guid.Parse(v.AsString)).ToArray();
        }

        var resumeHandler = new CannedHttpMessageHandler();
        await using (var resumeProvider = await CreateServiceProviderAsync(options, resumeHandler))
        {
            var resumeConnector = new NvdConnectorPlugin().Create(resumeProvider);

            await resumeConnector.ParseAsync(resumeProvider, CancellationToken.None);
            await resumeConnector.MapAsync(resumeProvider, CancellationToken.None);

            var documentStore = resumeProvider.GetRequiredService<IDocumentStore>();
            foreach (var documentId in pendingDocumentIds)
            {
                var document = await documentStore.FindAsync(documentId, CancellationToken.None);
                Assert.NotNull(document);
                Assert.Equal(DocumentStatuses.Mapped, document!.Status);
            }

            var advisoryStore = resumeProvider.GetRequiredService<IAdvisoryStore>();
            var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
            Assert.NotEmpty(advisories);

            var stateRepository = resumeProvider.GetRequiredService<ISourceStateRepository>();
            var finalState = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
            Assert.NotNull(finalState);
            var cursor = finalState!.Cursor;
            var finalPendingDocs = cursor.TryGetValue("pendingDocuments", out var pendingDocs) ? pendingDocs.AsBsonArray : new BsonArray();
            Assert.Empty(finalPendingDocs);
            var finalPendingMappings = cursor.TryGetValue("pendingMappings", out var pendingMappings) ? pendingMappings.AsBsonArray : new BsonArray();
            Assert.Empty(finalPendingMappings);
        }
    }

    private Task ResetDatabaseAsync()
    {
        return ResetDatabaseInternalAsync();
    }

    private async Task<ServiceProvider> CreateServiceProviderAsync(NvdOptions options, CannedHttpMessageHandler handler)
    {
        var services = new ServiceCollection();
        services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
        services.AddSingleton<TimeProvider>(_timeProvider);
        services.AddSingleton(handler);

        services.AddMongoStorage(storageOptions =>
        {
            storageOptions.ConnectionString = _fixture.Runner.ConnectionString;
            storageOptions.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
            storageOptions.CommandTimeout = TimeSpan.FromSeconds(5);
        });

        services.AddSourceCommon();
        services.AddNvdConnector(configure: opts =>
        {
            opts.BaseEndpoint = options.BaseEndpoint;
            opts.WindowSize = options.WindowSize;
            opts.WindowOverlap = options.WindowOverlap;
            opts.InitialBackfill = options.InitialBackfill;
        });

        services.Configure<HttpClientFactoryOptions>(NvdOptions.HttpClientName, builderOptions =>
        {
            builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
            {
                builder.PrimaryHandler = handler;
            });
        });

        var provider = services.BuildServiceProvider();
        var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
        await bootstrapper.InitializeAsync(CancellationToken.None);
        return provider;
    }

    private async Task ResetDatabaseInternalAsync()
    {
        if (_serviceProvider is not null)
        {
            if (_serviceProvider is IAsyncDisposable asyncDisposable)
            {
                await asyncDisposable.DisposeAsync();
            }
            else
            {
                _serviceProvider.Dispose();
            }

            _serviceProvider = null;
        }

        await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
        _handler.Clear();
        _timeProvider = new FakeTimeProvider(_initialNow);
    }

    private sealed class MetricCollector : IDisposable
    {
        private readonly MeterListener _listener;
        private readonly ConcurrentDictionary<string, long> _measurements = new(StringComparer.OrdinalIgnoreCase);

        public MetricCollector(string meterName)
        {
            _listener = new MeterListener
            {
                InstrumentPublished = (instrument, listener) =>
                {
                    if (instrument.Meter.Name == meterName)
                    {
                        listener.EnableMeasurementEvents(instrument);
                    }
                }
            };

            _listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
            {
                _measurements.AddOrUpdate(instrument.Name, measurement, (_, existing) => existing + measurement);
            });

            _listener.Start();
        }

        public long GetValue(string instrumentName)
            => _measurements.TryGetValue(instrumentName, out var value) ? value : 0;

        public void Dispose()
        {
            _listener.Dispose();
        }
    }

    private static Uri BuildRequestUri(NvdOptions options, DateTimeOffset start, DateTimeOffset end, int startIndex = 0)
    {
        var builder = new UriBuilder(options.BaseEndpoint);
        var parameters = new Dictionary<string, string>
        {
            ["lastModifiedStartDate"] = start.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
            ["lastModifiedEndDate"] = end.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
            ["resultsPerPage"] = "2000",
        };

        if (startIndex > 0)
        {
            parameters["startIndex"] = startIndex.ToString(CultureInfo.InvariantCulture);
        }

        builder.Query = string.Join("&", parameters.Select(static kvp => $"{System.Net.WebUtility.UrlEncode(kvp.Key)}={System.Net.WebUtility.UrlEncode(kvp.Value)}"));
        return builder.Uri;
    }

    private static DateTimeOffset? ReadDateTime(BsonValue value)
    {
        return value.BsonType switch
        {
            BsonType.DateTime => DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc),
            BsonType.String when DateTimeOffset.TryParse(value.AsString, out var parsed) => parsed.ToUniversalTime(),
            _ => null,
        };
    }

    private static string ReadFixture(string filename)
    {
        var baseDirectory = AppContext.BaseDirectory;
        var primary = Path.Combine(baseDirectory, "Source", "Nvd", "Fixtures", filename);
        if (File.Exists(primary))
        {
            return File.ReadAllText(primary);
        }

        var secondary = Path.Combine(baseDirectory, "Nvd", "Fixtures", filename);
        if (File.Exists(secondary))
        {
            return File.ReadAllText(secondary);
        }

        throw new FileNotFoundException($"Fixture '{filename}' was not found in the test output directory.");
    }

    public Task InitializeAsync() => Task.CompletedTask;

    public async Task DisposeAsync()
    {
        await ResetDatabaseInternalAsync();
    }
}
