using System;
using System.Globalization;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using System.Text.Json;
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.Source.Common;
using StellaOps.Feedser.Core.Jobs;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Distro.RedHat;
using StellaOps.Feedser.Source.Distro.RedHat.Configuration;
using StellaOps.Feedser.Source.Distro.RedHat.Internal;
using StellaOps.Feedser.Models;
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.Testing;
using StellaOps.Plugin;
using Xunit;
using Xunit.Abstractions;

namespace StellaOps.Feedser.Source.Distro.RedHat.Tests;

[Collection("mongo-fixture")]
public sealed class RedHatConnectorTests : IAsyncLifetime
{
    private readonly MongoIntegrationFixture _fixture;
    private readonly FakeTimeProvider _timeProvider;
    private readonly DateTimeOffset _initialNow;
    private readonly CannedHttpMessageHandler _handler;
    private readonly ITestOutputHelper _output;
    private ServiceProvider? _serviceProvider;
    private const bool ForceUpdateGoldens = false;

    public RedHatConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
    {
        _fixture = fixture;
        _initialNow = new DateTimeOffset(2025, 10, 5, 0, 0, 0, TimeSpan.Zero);
        _timeProvider = new FakeTimeProvider(_initialNow);
        _handler = new CannedHttpMessageHandler();
        _output = output;
    }

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

        var options = new RedHatOptions
        {
            BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
            PageSize = 10,
            MaxPagesPerFetch = 2,
            MaxAdvisoriesPerFetch = 25,
            InitialBackfill = TimeSpan.FromDays(1),
            Overlap = TimeSpan.Zero,
            FetchTimeout = TimeSpan.FromSeconds(30),
            UserAgent = "StellaOps.Tests.RedHat/1.0",
        };

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

        var configuredOptions = provider.GetRequiredService<IOptions<RedHatOptions>>().Value;
        Assert.Equal(10, configuredOptions.PageSize);
        Assert.Equal(TimeSpan.FromDays(1), configuredOptions.InitialBackfill);
        Assert.Equal(TimeSpan.Zero, configuredOptions.Overlap);
        _output.WriteLine($"InitialBackfill configured: {configuredOptions.InitialBackfill}");
        _output.WriteLine($"TimeProvider now: {_timeProvider.GetUtcNow():O}");

        var summaryUriBackfill = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-03&per_page=10&page=1");
        var summaryUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=10&page=1");
        var summaryUriPost = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=1");
        var summaryUriPostPage2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=2");
        var detailUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0001.json");
        var detailUri2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0002.json");

        _output.WriteLine($"Registering summary URI: {summaryUriBackfill}");
        _output.WriteLine($"Registering summary URI (overlap): {summaryUri}");
        _handler.AddJsonResponse(summaryUriBackfill, ReadFixture("summary-page1.json"));
        _handler.AddJsonResponse(summaryUri, ReadFixture("summary-page1-repeat.json"));
        _handler.AddJsonResponse(summaryUriPost, "[]");
        _handler.AddJsonResponse(summaryUriPostPage2, "[]");
        _handler.AddJsonResponse(detailUri, ReadFixture("csaf-rhsa-2025-0001.json"));
        _handler.AddJsonResponse(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json"));

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        await stateRepository.UpsertAsync(
            new SourceStateRecord(
                RedHatConnectorPlugin.SourceName,
                Enabled: true,
                Paused: false,
                Cursor: new BsonDocument(),
                LastSuccess: null,
                LastFailure: null,
                FailCount: 0,
                BackoffUntil: null,
                UpdatedAt: _timeProvider.GetUtcNow(),
                LastFailureReason: null),
            CancellationToken.None);

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

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


        foreach (var request in _handler.Requests)
        {
            _output.WriteLine($"Captured request: {request.Uri}");
        }

        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
        var advisory = advisories.Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0001", StringComparison.Ordinal));
        Assert.Equal("red hat security advisory: example kernel update", advisory.Title.ToLowerInvariant());
        Assert.Contains("RHSA-2025:0001", advisory.Aliases);
        Assert.Contains("CVE-2025-0001", advisory.Aliases);
        Assert.Equal("high", advisory.Severity);
        Assert.Equal("en", advisory.Language);

        var rpmPackage = advisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Rpm);
        _output.WriteLine($"RPM statuses count: {rpmPackage.Statuses.Length}");
        _output.WriteLine($"RPM ranges count: {rpmPackage.VersionRanges.Length}");
        foreach (var range in rpmPackage.VersionRanges)
        {
            _output.WriteLine($"Range fixed={range.FixedVersion}, last={range.LastAffectedVersion}, expr={range.RangeExpression}");
        }
        Assert.Equal("kernel-0:4.18.0-513.5.1.el8.x86_64", rpmPackage.Identifier);
        var fixedRange = Assert.Single(
            rpmPackage.VersionRanges,
            range => string.Equals(range.FixedVersion, "kernel-0:4.18.0-513.5.1.el8.x86_64", StringComparison.Ordinal));
        Assert.Equal("kernel-0:4.18.0-500.1.0.el8.x86_64", fixedRange.LastAffectedVersion);
        var nevraPrimitive = fixedRange.Primitives?.Nevra;
        Assert.NotNull(nevraPrimitive);
        Assert.Null(nevraPrimitive!.Introduced);
        Assert.Equal("kernel", nevraPrimitive.Fixed?.Name);

        var cpePackage = advisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Cpe);
        Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:8:*:*:*:*:*:*:*", cpePackage.Identifier);

        Assert.Contains(advisory.References, reference => reference.Url == "https://access.redhat.com/errata/RHSA-2025:0001");
        Assert.Contains(advisory.References, reference => reference.Url == "https://www.cve.org/CVERecord?id=CVE-2025-0001");

        var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n");
        _output.WriteLine("-- RHSA-2025:0001 snapshot --\n" + snapshot);
        var snapshotPath = ProjectFixturePath("rhsa-2025-0001.snapshot.json");
        if (ShouldUpdateGoldens())
        {
            File.WriteAllText(snapshotPath, snapshot);
            return;
        }

        var expectedSnapshot = File.ReadAllText(snapshotPath);
        Assert.Equal(NormalizeLineEndings(expectedSnapshot), NormalizeLineEndings(snapshot));

        var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs2) && pendingDocs2.AsBsonArray.Count == 0);
        Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings2) && pendingMappings2.AsBsonArray.Count == 0);

        const string fetchKind = "source:redhat:fetch";
        const string parseKind = "source:redhat:parse";
        const string mapKind = "source:redhat:map";

        var schedulerOptions = provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<JobSchedulerOptions>>().Value;
        Assert.True(schedulerOptions.Definitions.TryGetValue(fetchKind, out var fetchDefinition));
        Assert.True(schedulerOptions.Definitions.TryGetValue(parseKind, out var parseDefinition));
        Assert.True(schedulerOptions.Definitions.TryGetValue(mapKind, out var mapDefinition));

        Assert.Equal("RedHatFetchJob", fetchDefinition.JobType.Name);
        Assert.Equal(TimeSpan.FromMinutes(12), fetchDefinition.Timeout);
        Assert.Equal(TimeSpan.FromMinutes(6), fetchDefinition.LeaseDuration);
        Assert.Equal("0,15,30,45 * * * *", fetchDefinition.CronExpression);
        Assert.True(fetchDefinition.Enabled);

        Assert.Equal("RedHatParseJob", parseDefinition.JobType.Name);
        Assert.Equal(TimeSpan.FromMinutes(15), parseDefinition.Timeout);
        Assert.Equal(TimeSpan.FromMinutes(6), parseDefinition.LeaseDuration);
        Assert.Equal("5,20,35,50 * * * *", parseDefinition.CronExpression);
        Assert.True(parseDefinition.Enabled);

        Assert.Equal("RedHatMapJob", mapDefinition.JobType.Name);
        Assert.Equal(TimeSpan.FromMinutes(20), mapDefinition.Timeout);
        Assert.Equal(TimeSpan.FromMinutes(6), mapDefinition.LeaseDuration);
        Assert.Equal("10,25,40,55 * * * *", mapDefinition.CronExpression);
        Assert.True(mapDefinition.Enabled);

        var summaryUriRepeat = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-03&per_page=10&page=1");
        var summaryUriSecondPage = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-03&per_page=10&page=2");
        var summaryUriRepeatOverlap = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=10&page=1");
        var summaryUriSecondPageOverlap = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=10&page=2");

        _output.WriteLine($"Registering repeat summary URI: {summaryUriRepeat}");
        _output.WriteLine($"Registering second page summary URI: {summaryUriSecondPage}");
        _output.WriteLine($"Registering overlap repeat summary URI: {summaryUriRepeatOverlap}");
        _output.WriteLine($"Registering overlap second page summary URI: {summaryUriSecondPageOverlap}");
        _handler.AddJsonResponse(summaryUriRepeat, ReadFixture("summary-page1-repeat.json"));
        _handler.AddJsonResponse(summaryUriSecondPage, ReadFixture("summary-page2.json"));
        _handler.AddJsonResponse(summaryUriRepeatOverlap, ReadFixture("summary-page1-repeat.json"));
        _handler.AddJsonResponse(summaryUriSecondPageOverlap, ReadFixture("summary-page2.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(2, advisories.Count);

        var secondAdvisory = advisories.Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0002", StringComparison.Ordinal));
        var rpm2 = secondAdvisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Rpm);
        Assert.Equal("kernel-0:5.14.0-400.el9.x86_64", rpm2.Identifier);
        const string knownNotAffected = "known_not_affected";

        foreach (var status in rpm2.Statuses)
        {
            _output.WriteLine($"RPM2 status: {status.Status}");
        }

        Assert.DoesNotContain(rpm2.VersionRanges, range => string.Equals(range.RangeExpression, knownNotAffected, StringComparison.Ordinal));
        Assert.Contains(rpm2.Statuses, status => status.Status == knownNotAffected);

        var cpe2 = secondAdvisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Cpe);
        Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*", cpe2.Identifier);
        Assert.Empty(cpe2.VersionRanges);
        Assert.Contains(cpe2.Statuses, status => status.Status == knownNotAffected);

        state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs3) && pendingDocs3.AsBsonArray.Count == 0);
        Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings3) && pendingMappings3.AsBsonArray.Count == 0);
    }

    [Fact]
    public void GoldenFixturesMatchSnapshots()
    {
        var fixtures = new[]
        {
            new GoldenFixtureCase(
                AdvisoryId: "RHSA-2025:0001",
                InputFile: "csaf-rhsa-2025-0001.json",
                SnapshotFile: "rhsa-2025-0001.snapshot.json",
                ValidatedAt: DateTimeOffset.Parse("2025-10-05T00:00:00Z")),
            new GoldenFixtureCase(
                AdvisoryId: "RHSA-2025:0002",
                InputFile: "csaf-rhsa-2025-0002.json",
                SnapshotFile: "rhsa-2025-0002.snapshot.json",
                ValidatedAt: DateTimeOffset.Parse("2025-10-05T12:00:00Z")),
            new GoldenFixtureCase(
                AdvisoryId: "RHSA-2025:0003",
                InputFile: "csaf-rhsa-2025-0003.json",
                SnapshotFile: "rhsa-2025-0003.snapshot.json",
                ValidatedAt: DateTimeOffset.Parse("2025-10-06T09:00:00Z")),
        };

        var updateGoldens = ShouldUpdateGoldens();

        foreach (var fixture in fixtures)
        {
            var snapshot = MapFixtureToSnapshot(fixture);
            var snapshotPath = ProjectFixturePath(fixture.SnapshotFile);

            if (updateGoldens)
            {
                File.WriteAllText(snapshotPath, snapshot);
                continue;
            }

            var expected = File.ReadAllText(snapshotPath).Replace("\r\n", "\n");
            Assert.Equal(expected, snapshot);
        }
    }

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

        var options = new RedHatOptions
        {
            BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
            PageSize = 10,
            MaxPagesPerFetch = 2,
            MaxAdvisoriesPerFetch = 25,
            InitialBackfill = TimeSpan.FromDays(1),
            Overlap = TimeSpan.Zero,
            FetchTimeout = TimeSpan.FromSeconds(30),
            UserAgent = "StellaOps.Tests.RedHat/1.0",
        };

        var summaryUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=10&page=1");
        var summaryUriPost = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=1");
        var summaryUriPostPage2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=2");
        var detailUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0001.json");
        var detailUri2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0002.json");

        var fetchHandler = new CannedHttpMessageHandler();
        fetchHandler.AddJsonResponse(summaryUri, ReadFixture("summary-page1-repeat.json"));
        fetchHandler.AddJsonResponse(summaryUriPost, "[]");
        fetchHandler.AddJsonResponse(summaryUriPostPage2, "[]");
        fetchHandler.AddJsonResponse(detailUri, ReadFixture("csaf-rhsa-2025-0001.json"));
        fetchHandler.AddJsonResponse(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json"));

        Guid[] pendingDocumentIds;
        await using (var fetchProvider = await CreateServiceProviderAsync(options, fetchHandler))
        {
            var stateRepository = fetchProvider.GetRequiredService<ISourceStateRepository>();
            await stateRepository.UpsertAsync(
                new SourceStateRecord(
                    RedHatConnectorPlugin.SourceName,
                    Enabled: true,
                    Paused: false,
                    Cursor: new BsonDocument(),
                    LastSuccess: null,
                    LastFailure: null,
                    FailCount: 0,
                    BackoffUntil: null,
                    UpdatedAt: _timeProvider.GetUtcNow(),
                    LastFailureReason: null),
                CancellationToken.None);

            var connector = new RedHatConnectorPlugin().Create(fetchProvider);
            await connector.FetchAsync(fetchProvider, CancellationToken.None);

            var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
            Assert.NotNull(state);
            var pendingDocs = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
                ? pendingDocsValue.AsBsonArray
                : new BsonArray();
            Assert.NotEmpty(pendingDocs);
            pendingDocumentIds = pendingDocs.Select(value => Guid.Parse(value.AsString)).ToArray();
        }

        var resumeHandler = new CannedHttpMessageHandler();
        await using (var resumeProvider = await CreateServiceProviderAsync(options, resumeHandler))
        {
            var resumeConnector = new RedHatConnectorPlugin().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(RedHatConnectorPlugin.SourceName, CancellationToken.None);
            Assert.NotNull(finalState);
            var finalPendingDocs = finalState!.Cursor.TryGetValue("pendingDocuments", out var docsValue) ? docsValue.AsBsonArray : new BsonArray();
            Assert.Empty(finalPendingDocs);
            var finalPendingMappings = finalState.Cursor.TryGetValue("pendingMappings", out var mappingsValue) ? mappingsValue.AsBsonArray : new BsonArray();
            Assert.Empty(finalPendingMappings);
        }
    }

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

        var options = new RedHatOptions
        {
            BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
            PageSize = 10,
            MaxPagesPerFetch = 2,
            MaxAdvisoriesPerFetch = 10,
            InitialBackfill = TimeSpan.FromDays(7),
            Overlap = TimeSpan.Zero,
            FetchTimeout = TimeSpan.FromSeconds(30),
            UserAgent = "StellaOps.Tests.RedHat/1.0",
        };

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

        var summaryUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-09-28&per_page=10&page=1");
        var summaryUriPost = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=1");
        var detailUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0003.json");

        _handler.AddJsonResponse(summaryUri, ReadFixture("summary-page3.json"));
        _handler.AddJsonResponse(summaryUriPost, "[]");
        _handler.AddJsonResponse(detailUri, ReadFixture("csaf-rhsa-2025-0003.json"));

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        await stateRepository.UpsertAsync(
            new SourceStateRecord(
                RedHatConnectorPlugin.SourceName,
                Enabled: true,
                Paused: false,
                Cursor: new BsonDocument(),
                LastSuccess: null,
                LastFailure: null,
                FailCount: 0,
                BackoffUntil: null,
                UpdatedAt: _timeProvider.GetUtcNow(),
                LastFailureReason: null),
            CancellationToken.None);

        var connector = new RedHatConnectorPlugin().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 advisory = (await advisoryStore.GetRecentAsync(10, CancellationToken.None))
            .Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0003", StringComparison.Ordinal));

        var references = advisory.References.ToArray();
        Assert.Collection(
            references,
            reference =>
            {
                Assert.Equal("self", reference.Kind);
                Assert.Equal("https://access.redhat.com/errata/RHSA-2025:0003", reference.Url);
                Assert.Equal("Primary advisory", reference.Summary);
            },
            reference =>
            {
                Assert.Equal("mitigation", reference.Kind);
                Assert.Equal("https://access.redhat.com/solutions/999999", reference.Url);
                Assert.Equal("Knowledge base guidance", reference.Summary);
            },
            reference =>
            {
                Assert.Equal("exploit", reference.Kind);
                Assert.Equal("https://bugzilla.redhat.com/show_bug.cgi?id=2222222", reference.Url);
                Assert.Equal("Exploit tracking", reference.Summary);
            },
            reference =>
            {
                Assert.Equal("external", reference.Kind);
                Assert.Equal("https://www.cve.org/CVERecord?id=CVE-2025-0003", reference.Url);
                Assert.Equal("CVE record", reference.Summary);
            });
        Assert.Equal(4, references.Length);

        Assert.Equal("self", references[0].Kind);
        Assert.Equal("https://access.redhat.com/errata/RHSA-2025:0003", references[0].Url);
        Assert.Equal("Primary advisory", references[0].Summary);

        Assert.Equal("mitigation", references[1].Kind);
        Assert.Equal("https://access.redhat.com/solutions/999999", references[1].Url);
        Assert.Equal("Knowledge base guidance", references[1].Summary);

        Assert.Equal("exploit", references[2].Kind);
        Assert.Equal("https://bugzilla.redhat.com/show_bug.cgi?id=2222222", references[2].Url);

        Assert.Equal("external", references[3].Kind);
        Assert.Equal("https://www.cve.org/CVERecord?id=CVE-2025-0003", references[3].Url);
        Assert.Equal("CVE record", references[3].Summary);
    }

    private static string MapFixtureToSnapshot(GoldenFixtureCase fixture)
    {
        var jsonPath = ProjectFixturePath(fixture.InputFile);
        var json = File.ReadAllText(jsonPath);

        using var jsonDocument = JsonDocument.Parse(json);
        var bson = BsonDocument.Parse(json);

        var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["advisoryId"] = fixture.AdvisoryId,
        };

        var document = new DocumentRecord(
            Guid.NewGuid(),
            RedHatConnectorPlugin.SourceName,
            $"https://access.redhat.com/hydra/rest/securitydata/csaf/{fixture.AdvisoryId}.json",
            fixture.ValidatedAt,
            new string('0', 64),
            DocumentStatuses.Mapped,
            "application/json",
            Headers: null,
            Metadata: metadata,
            Etag: null,
            LastModified: fixture.ValidatedAt,
            GridFsId: null);

        var dto = new DtoRecord(Guid.NewGuid(), document.Id, RedHatConnectorPlugin.SourceName, "redhat.csaf.v2", bson, fixture.ValidatedAt);

        var advisory = RedHatMapper.Map(RedHatConnectorPlugin.SourceName, dto, document, jsonDocument);
        Assert.NotNull(advisory);

        return SnapshotSerializer.ToSnapshot(advisory!).Replace("\r\n", "\n");
    }

    private static bool ShouldUpdateGoldens()
        => ForceUpdateGoldens
            || IsTruthy(Environment.GetEnvironmentVariable("UPDATE_GOLDENS"))
            || IsTruthy(Environment.GetEnvironmentVariable("DOTNET_TEST_UPDATE_GOLDENS"));

    private static bool IsTruthy(string? value)
        => !string.IsNullOrWhiteSpace(value)
            && (string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
                || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
                || string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase));

    private sealed record GoldenFixtureCase(string AdvisoryId, string InputFile, string SnapshotFile, DateTimeOffset ValidatedAt);

    private static string ProjectFixturePath(string filename)
        => Path.Combine(GetProjectRoot(), "RedHat", "Fixtures", filename);

    private static string GetProjectRoot()
        => Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));

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

        _serviceProvider = await CreateServiceProviderAsync(options, _handler);
    }

    private async Task<ServiceProvider> CreateServiceProviderAsync(RedHatOptions 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.AddRedHatConnector(opts =>
        {
            opts.BaseEndpoint = options.BaseEndpoint;
            opts.SummaryPath = options.SummaryPath;
            opts.PageSize = options.PageSize;
            opts.MaxPagesPerFetch = options.MaxPagesPerFetch;
            opts.MaxAdvisoriesPerFetch = options.MaxAdvisoriesPerFetch;
            opts.InitialBackfill = options.InitialBackfill;
            opts.Overlap = options.Overlap;
            opts.FetchTimeout = options.FetchTimeout;
            opts.UserAgent = options.UserAgent;
        });

        services.Configure<JobSchedulerOptions>(schedulerOptions =>
        {
            var fetchType = Type.GetType("StellaOps.Feedser.Source.Distro.RedHat.RedHatFetchJob, StellaOps.Feedser.Source.Distro.RedHat", throwOnError: true)!;
            var parseType = Type.GetType("StellaOps.Feedser.Source.Distro.RedHat.RedHatParseJob, StellaOps.Feedser.Source.Distro.RedHat", throwOnError: true)!;
            var mapType = Type.GetType("StellaOps.Feedser.Source.Distro.RedHat.RedHatMapJob, StellaOps.Feedser.Source.Distro.RedHat", throwOnError: true)!;

            schedulerOptions.Definitions["source:redhat:fetch"] = new JobDefinition("source:redhat:fetch", fetchType, TimeSpan.FromMinutes(12), TimeSpan.FromMinutes(6), "0,15,30,45 * * * *", true);
            schedulerOptions.Definitions["source:redhat:parse"] = new JobDefinition("source:redhat:parse", parseType, TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(6), "5,20,35,50 * * * *", true);
            schedulerOptions.Definitions["source:redhat:map"] = new JobDefinition("source:redhat:map", mapType, TimeSpan.FromMinutes(20), TimeSpan.FromMinutes(6), "10,25,40,55 * * * *", true);
        });

        services.Configure<HttpClientFactoryOptions>(RedHatOptions.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 Task ResetDatabaseAsync()
    {
        return ResetDatabaseInternalAsync();
    }

    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.SetUtcNow(_initialNow);
    }

    private static string ReadFixture(string name)
        => File.ReadAllText(ResolveFixturePath(name));

    private static string ResolveFixturePath(string filename)
    {
        var candidates = new[]
        {
            Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "RedHat", "Fixtures", filename),
            Path.Combine(AppContext.BaseDirectory, "RedHat", "Fixtures", filename),
        };

        foreach (var candidate in candidates)
        {
            if (File.Exists(candidate))
            {
                return candidate;
            }
        }

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

    private static string NormalizeLineEndings(string value)
    {
        var normalized = value.Replace("\r\n", "\n").Replace('\r', '\n');
        return normalized.TrimEnd('\n');
    }

    public Task InitializeAsync() => Task.CompletedTask;

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