using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Feedser.Source.CertCc;
using StellaOps.Feedser.Source.CertCc.Configuration;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Cursors;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Testing;
using Xunit;

namespace StellaOps.Feedser.Source.CertCc.Tests.CertCc;

[Collection("mongo-fixture")]
public sealed class CertCcConnectorTests : IAsyncLifetime
{
    private static readonly Uri MonthlySummaryUri = new("https://www.kb.cert.org/vuls/api/2025/10/summary/");
    private static readonly Uri YearlySummaryUri = new("https://www.kb.cert.org/vuls/api/2025/summary/");
    private static readonly Uri NoteDetailUri = new("https://www.kb.cert.org/vuls/api/294418/");
    private static readonly Uri VendorsUri = new("https://www.kb.cert.org/vuls/api/294418/vendors/");
    private static readonly Uri VulsUri = new("https://www.kb.cert.org/vuls/api/294418/vuls/");
    private static readonly Uri VendorStatusesUri = new("https://www.kb.cert.org/vuls/api/294418/vendors/vuls/");

    private readonly MongoIntegrationFixture _fixture;
    private readonly FakeTimeProvider _timeProvider;
    private readonly CannedHttpMessageHandler _handler;

    public CertCcConnectorTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
        _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 11, 9, 30, 0, TimeSpan.Zero));
        _handler = new CannedHttpMessageHandler();
    }

    [Fact]
    public async Task FetchParseMap_ProducesCanonicalAdvisory()
    {
        await using var provider = await BuildServiceProviderAsync();
        SeedSummaryResponses();
        SeedDetailResponses();

        var connector = provider.GetRequiredService<CertCcConnector>();
        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);
        advisories.Should().NotBeNull();
        advisories.Should().HaveCountGreaterThan(0);

        var advisory = advisories.FirstOrDefault(a => a.AdvisoryKey == "certcc/vu-294418");
        advisory.Should().NotBeNull();
        advisory!.Title.Should().ContainEquivalentOf("DrayOS");
        advisory.Summary.Should().NotBeNullOrWhiteSpace();
        advisory.Aliases.Should().Contain("VU#294418");
        advisory.Aliases.Should().Contain("CVE-2025-10547");
        advisory.AffectedPackages.Should().NotBeNull();
        advisory.AffectedPackages.Should().HaveCountGreaterThan(0);
        advisory.AffectedPackages![0].NormalizedVersions.Should().NotBeNull();

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
        state.Should().NotBeNull();
        var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
            ? pendingDocsValue!.AsBsonArray.Count
            : 0;
        pendingDocuments.Should().Be(0);
        var pendingMappings = state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue)
            ? pendingMappingsValue!.AsBsonArray.Count
            : 0;
        pendingMappings.Should().Be(0);
    }

    [Fact]
    public async Task Fetch_PersistsSummaryAndDetailDocuments()
    {
        await using var provider = await BuildServiceProviderAsync();
        SeedSummaryResponses();
        SeedDetailResponses();

        var connector = provider.GetRequiredService<CertCcConnector>();
        await connector.FetchAsync(provider, CancellationToken.None);

        var documentStore = provider.GetRequiredService<IDocumentStore>();

        var summaryDocument = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, MonthlySummaryUri.ToString(), CancellationToken.None);
        summaryDocument.Should().NotBeNull();
        summaryDocument!.Status.Should().Be(DocumentStatuses.PendingParse);

        var noteDocument = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, NoteDetailUri.ToString(), CancellationToken.None);
        noteDocument.Should().NotBeNull();
        noteDocument!.Status.Should().Be(DocumentStatuses.PendingParse);
        noteDocument.Metadata.Should().NotBeNull();
        noteDocument.Metadata!.Should().ContainKey("certcc.endpoint").WhoseValue.Should().Be("note");
        noteDocument.Metadata.Should().ContainKey("certcc.noteId").WhoseValue.Should().Be("294418");
        noteDocument.Metadata.Should().ContainKey("certcc.vuid").WhoseValue.Should().Be("VU#294418");

        var vendorsDocument = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, VendorsUri.ToString(), CancellationToken.None);
        vendorsDocument.Should().NotBeNull();
        vendorsDocument!.Status.Should().Be(DocumentStatuses.PendingParse);
        vendorsDocument.Metadata.Should().NotBeNull();
        vendorsDocument.Metadata!.Should().ContainKey("certcc.endpoint").WhoseValue.Should().Be("vendors");

        var vulsDocument = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, VulsUri.ToString(), CancellationToken.None);
        vulsDocument.Should().NotBeNull();
        vulsDocument!.Status.Should().Be(DocumentStatuses.PendingParse);
        vulsDocument.Metadata.Should().NotBeNull();
        vulsDocument.Metadata!.Should().ContainKey("certcc.endpoint").WhoseValue.Should().Be("vuls");

        var vendorStatusesDocument = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, VendorStatusesUri.ToString(), CancellationToken.None);
        vendorStatusesDocument.Should().NotBeNull();
        vendorStatusesDocument!.Status.Should().Be(DocumentStatuses.PendingParse);
        vendorStatusesDocument.Metadata.Should().NotBeNull();
        vendorStatusesDocument.Metadata!.Should().ContainKey("certcc.endpoint").WhoseValue.Should().Be("vendors-vuls");

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
        state.Should().NotBeNull();
        state!.Cursor.Should().NotBeNull();
        var pendingNotesCount = state.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue)
            ? pendingNotesValue!.AsBsonArray.Count
            : 0;
        pendingNotesCount.Should().Be(0);
        var pendingSummariesCount = state.Cursor.TryGetValue("pendingSummaries", out var pendingSummariesValue)
            ? pendingSummariesValue!.AsBsonArray.Count
            : 0;
        pendingSummariesCount.Should().Be(0);

        var pendingDocumentsCount = state.Cursor.TryGetValue("pendingDocuments", out var pendingDocumentsValue)
            ? pendingDocumentsValue!.AsBsonArray.Count
            : 0;
        pendingDocumentsCount.Should().Be(4);

        var pendingMappingsCount = state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue)
            ? pendingMappingsValue!.AsBsonArray.Count
            : 0;
        pendingMappingsCount.Should().Be(0);

        _handler.Requests.Should().Contain(request => request.Uri == NoteDetailUri);
    }

    [Fact]
    public async Task Fetch_ReusesConditionalRequestsOnSubsequentRun()
    {
        await using var provider = await BuildServiceProviderAsync();
        SeedSummaryResponses(summaryEtag: "\"summary-oct\"", yearlyEtag: "\"summary-year\"");
        SeedDetailResponses(detailEtag: "\"note-etag\"", vendorsEtag: "\"vendors-etag\"", vulsEtag: "\"vuls-etag\"", vendorStatusesEtag: "\"vendor-statuses-etag\"");

        var connector = provider.GetRequiredService<CertCcConnector>();
        await connector.FetchAsync(provider, CancellationToken.None);

        _handler.Clear();
        SeedSummaryNotModifiedResponses("\"summary-oct\"", "\"summary-year\"");
        SeedDetailNotModifiedResponses("\"note-etag\"", "\"vendors-etag\"", "\"vuls-etag\"", "\"vendor-statuses-etag\"");
        _timeProvider.Advance(TimeSpan.FromMinutes(15));

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

        var requests = _handler.Requests.ToArray();
        requests.Should().OnlyContain(r =>
            r.Uri == MonthlySummaryUri
            || r.Uri == YearlySummaryUri
            || r.Uri == NoteDetailUri
            || r.Uri == VendorsUri
            || r.Uri == VulsUri
            || r.Uri == VendorStatusesUri);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
        state.Should().NotBeNull();
        var pendingNotesCount = state!.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue)
            ? pendingNotesValue!.AsBsonArray.Count
            : 0;
        pendingNotesCount.Should().Be(0);
        var pendingSummaries = state.Cursor.TryGetValue("pendingSummaries", out var pendingSummariesValue)
            ? pendingSummariesValue!.AsBsonArray.Count
            : 0;
        pendingSummaries.Should().Be(0);

        var pendingDocuments = state.Cursor.TryGetValue("pendingDocuments", out var pendingDocumentsValue)
            ? pendingDocumentsValue!.AsBsonArray.Count
            : 0;
        pendingDocuments.Should().BeGreaterThan(0);
    }

    [Fact]
    public async Task Fetch_DetailFailureRecordsBackoffAndKeepsPendingNote()
    {
        await using var provider = await BuildServiceProviderAsync();
        SeedSummaryResponses();
        SeedDetailResponses(vendorsStatus: HttpStatusCode.InternalServerError);

        var connector = provider.GetRequiredService<CertCcConnector>();
        var failure = await Assert.ThrowsAnyAsync<Exception>(() => connector.FetchAsync(provider, CancellationToken.None));
        Assert.True(failure is HttpRequestException || failure is InvalidOperationException);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
        state.Should().NotBeNull();
        state!.FailCount.Should().BeGreaterThan(0);
        state.BackoffUntil.Should().NotBeNull();
        state.BackoffUntil.Should().BeAfter(_timeProvider.GetUtcNow());
        state.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue).Should().BeTrue();
        pendingNotesValue!.AsBsonArray.Should().Contain(value => value.AsString == "294418");
        var pendingSummaries = state.Cursor.TryGetValue("pendingSummaries", out var pendingSummariesValue)
            ? pendingSummariesValue!.AsBsonArray.Count
            : 0;
        pendingSummaries.Should().Be(0);
    }

    [Fact]
    public async Task Fetch_PartialDetailEndpointsMissing_CompletesAndMaps()
    {
        await using var provider = await BuildServiceProviderAsync();
        SeedSummaryResponses();
        SeedDetailResponses(
            vulsStatus: HttpStatusCode.NotFound,
            vendorStatusesStatus: HttpStatusCode.NotFound);

        var connector = provider.GetRequiredService<CertCcConnector>();
        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);
        advisories.Should().NotBeNull();
        advisories!.Should().Contain(advisory => advisory.AdvisoryKey == "certcc/vu-294418");

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var vendorsDocument = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, VendorsUri.ToString(), CancellationToken.None);
        vendorsDocument.Should().NotBeNull();
        vendorsDocument!.Status.Should().Be(DocumentStatuses.Mapped);
        var vulsDocument = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, VulsUri.ToString(), CancellationToken.None);
        vulsDocument.Should().BeNull();
        var noteDocument = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, NoteDetailUri.ToString(), CancellationToken.None);
        noteDocument.Should().NotBeNull();
        noteDocument!.Status.Should().Be(DocumentStatuses.Mapped);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
        state.Should().NotBeNull();
        state!.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue).Should().BeTrue();
        pendingNotesValue!.AsBsonArray.Should().BeEmpty();
        state.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue).Should().BeTrue();
        pendingDocsValue!.AsBsonArray.Should().BeEmpty();
        state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue).Should().BeTrue();
        pendingMappingsValue!.AsBsonArray.Should().BeEmpty();
    }

    public Task InitializeAsync() => Task.CompletedTask;

    public async Task DisposeAsync()
    {
        await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
    }

    [Fact]
    public async Task ParseAndMap_SkipWhenDetailMappingDisabled()
    {
        await using var provider = await BuildServiceProviderAsync(enableDetailMapping: false);
        SeedSummaryResponses();
        SeedDetailResponses();

        var connector = provider.GetRequiredService<CertCcConnector>();
        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);
        advisories.Should().BeNullOrEmpty();

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var noteDocument = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, NoteDetailUri.ToString(), CancellationToken.None);
        noteDocument.Should().NotBeNull();
        noteDocument!.Status.Should().Be(DocumentStatuses.PendingParse);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
        state.Should().NotBeNull();
        var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
            ? pendingDocsValue!.AsBsonArray.Count
            : 0;
        pendingDocuments.Should().BeGreaterThan(0);
        var pendingMappings = state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue)
            ? pendingMappingsValue!.AsBsonArray.Count
            : 0;
        pendingMappings.Should().Be(0);
    }

    private async Task<ServiceProvider> BuildServiceProviderAsync(bool enableDetailMapping = true)
    {
        await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
        _handler.Clear();

        var services = new ServiceCollection();
        services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
        services.AddSingleton<TimeProvider>(_timeProvider);
        services.AddSingleton(_handler);

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

        services.AddSourceCommon();
        services.AddCertCcConnector(options =>
        {
            options.BaseApiUri = new Uri("https://www.kb.cert.org/vuls/api/");
            options.SummaryWindow = new TimeWindowCursorOptions
            {
                WindowSize = TimeSpan.FromDays(1),
                Overlap = TimeSpan.Zero,
                InitialBackfill = TimeSpan.FromDays(1),
                MinimumWindowSize = TimeSpan.FromHours(6),
            };
            options.MaxMonthlySummaries = 1;
            options.MaxNotesPerFetch = 5;
            options.DetailRequestDelay = TimeSpan.Zero;
            options.EnableDetailMapping = enableDetailMapping;
        });

        services.Configure<HttpClientFactoryOptions>(CertCcOptions.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 void SeedSummaryResponses(string summaryEtag = "\"summary-oct\"", string yearlyEtag = "\"summary-year\"")
    {
        AddJsonResponse(MonthlySummaryUri, ReadFixture("summary-2025-10.json"), summaryEtag);
        AddJsonResponse(YearlySummaryUri, ReadFixture("summary-2025.json"), yearlyEtag);
    }

    private void SeedSummaryNotModifiedResponses(string summaryEtag, string yearlyEtag)
    {
        AddNotModifiedResponse(MonthlySummaryUri, summaryEtag);
        AddNotModifiedResponse(YearlySummaryUri, yearlyEtag);
    }

    private void SeedDetailResponses(
        string detailEtag = "\"note-etag\"",
        string vendorsEtag = "\"vendors-etag\"",
        string vulsEtag = "\"vuls-etag\"",
        string vendorStatusesEtag = "\"vendor-statuses-etag\"",
        HttpStatusCode vendorsStatus = HttpStatusCode.OK,
        HttpStatusCode vulsStatus = HttpStatusCode.OK,
        HttpStatusCode vendorStatusesStatus = HttpStatusCode.OK)
    {
        AddJsonResponse(NoteDetailUri, ReadFixture("vu-294418.json"), detailEtag);

        if (vendorsStatus == HttpStatusCode.OK)
        {
            AddJsonResponse(VendorsUri, ReadFixture("vu-294418-vendors.json"), vendorsEtag);
        }
        else
        {
            _handler.AddResponse(VendorsUri, () =>
            {
                var response = new HttpResponseMessage(vendorsStatus)
                {
                    Content = new StringContent("vendors error", Encoding.UTF8, "text/plain"),
                };
                response.Headers.ETag = new EntityTagHeaderValue(vendorsEtag);
                return response;
            });
        }

        if (vulsStatus == HttpStatusCode.OK)
        {
            AddJsonResponse(VulsUri, ReadFixture("vu-294418-vuls.json"), vulsEtag);
        }
        else
        {
            _handler.AddResponse(VulsUri, () =>
            {
                var response = new HttpResponseMessage(vulsStatus)
                {
                    Content = new StringContent("vuls error", Encoding.UTF8, "text/plain"),
                };
                response.Headers.ETag = new EntityTagHeaderValue(vulsEtag);
                return response;
            });
        }

        if (vendorStatusesStatus == HttpStatusCode.OK)
        {
            AddJsonResponse(VendorStatusesUri, ReadFixture("vendor-statuses-294418.json"), vendorStatusesEtag);
        }
        else
        {
            _handler.AddResponse(VendorStatusesUri, () =>
            {
                var response = new HttpResponseMessage(vendorStatusesStatus)
                {
                    Content = new StringContent("vendor statuses error", Encoding.UTF8, "text/plain"),
                };
                response.Headers.ETag = new EntityTagHeaderValue(vendorStatusesEtag);
                return response;
            });
        }
    }

    private void SeedDetailNotModifiedResponses(string detailEtag, string vendorsEtag, string vulsEtag, string vendorStatusesEtag)
    {
        AddNotModifiedResponse(NoteDetailUri, detailEtag);
        AddNotModifiedResponse(VendorsUri, vendorsEtag);
        AddNotModifiedResponse(VulsUri, vulsEtag);
        AddNotModifiedResponse(VendorStatusesUri, vendorStatusesEtag);
    }

    private void AddJsonResponse(Uri uri, string json, string etag)
    {
        _handler.AddResponse(uri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(json, Encoding.UTF8, "application/json"),
            };
            response.Headers.ETag = new EntityTagHeaderValue(etag);
            return response;
        });
    }

    private void AddNotModifiedResponse(Uri uri, string etag)
    {
        _handler.AddResponse(uri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.NotModified);
            response.Headers.ETag = new EntityTagHeaderValue(etag);
            return response;
        });
    }

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

        var fallback = Path.Combine(baseDirectory, "Fixtures", filename);
        return File.ReadAllText(fallback);
    }
}
