using System;
using System.Net;
using System.Net.Http;
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.Options;
using MongoDB.Bson;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Vndr.Msrc.Configuration;
using StellaOps.Feedser.Source.Vndr.Msrc.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.Testing;
using Xunit;
using StellaOps.Feedser.Source.Common.Http;

namespace StellaOps.Feedser.Source.Vndr.Msrc.Tests;

[Collection("mongo-fixture")]
public sealed class MsrcConnectorTests : IAsyncLifetime
{
    private static readonly Uri TokenUri = new("https://login.microsoftonline.com/11111111-1111-1111-1111-111111111111/oauth2/v2.0/token");
    private static readonly Uri SummaryUri = new("https://api.msrc.microsoft.com/sug/v2.0/vulnerabilities");
    private static readonly Uri DetailUri = new("https://api.msrc.microsoft.com/sug/v2.0/vulnerability/ADV123456");

    private readonly MongoIntegrationFixture _fixture;
    private readonly CannedHttpMessageHandler _handler;

    public MsrcConnectorTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
        _handler = new CannedHttpMessageHandler();
    }

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

        var connector = provider.GetRequiredService<MsrcConnector>();
        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(5, CancellationToken.None);
        advisories.Should().HaveCount(1);

        var advisory = advisories[0];
        advisory.AdvisoryKey.Should().Be("ADV123456");
        advisory.Severity.Should().Be("critical");
        advisory.Aliases.Should().Contain("CVE-2025-0001");
        advisory.Aliases.Should().Contain("KB5031234");
        advisory.References.Should().Contain(reference => reference.Url == "https://msrc.microsoft.com/update-guide/vulnerability/ADV123456");
        advisory.References.Should().Contain(reference => reference.Url == "https://download.microsoft.com/msrc/2025/ADV123456.cvrf.zip");
        advisory.AffectedPackages.Should().HaveCount(1);
        advisory.AffectedPackages[0].NormalizedVersions.Should().Contain(rule => rule.Scheme == "msrc.build" && rule.Value == "22631.3520");
        advisory.CvssMetrics.Should().Contain(metric => metric.BaseScore == 8.1);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(MsrcConnectorPlugin.SourceName, CancellationToken.None);
        state.Should().NotBeNull();
        state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
        pendingDocs!.AsBsonArray.Should().BeEmpty();

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var cvrfDocument = await documentStore.FindBySourceAndUriAsync(MsrcConnectorPlugin.SourceName, "https://download.microsoft.com/msrc/2025/ADV123456.cvrf.zip", CancellationToken.None);
        cvrfDocument.Should().NotBeNull();
        cvrfDocument!.Status.Should().Be(DocumentStatuses.Mapped);
    }

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

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

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

        services.AddSourceCommon();
        services.AddMsrcConnector(options =>
        {
            options.TenantId = "11111111-1111-1111-1111-111111111111";
            options.ClientId = "client-id";
            options.ClientSecret = "secret";
            options.InitialLastModified = new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero);
            options.RequestDelay = TimeSpan.Zero;
            options.MaxAdvisoriesPerFetch = 10;
            options.CursorOverlap = TimeSpan.FromMinutes(1);
            options.DownloadCvrf = true;
        });

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

        services.Configure<HttpClientFactoryOptions>(MsrcOptions.TokenClientName, 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 SeedResponses()
    {
        var summaryJson = ReadFixture("msrc-summary.json");
        var detailJson = ReadFixture("msrc-detail.json");
        var tokenJson = """{"token_type":"Bearer","expires_in":3600,"access_token":"fake-token"}""";
        var cvrfBytes = Encoding.UTF8.GetBytes("PK\x03\x04FAKECVRF");

        _handler.SetFallback(request =>
        {
            if (request.RequestUri is null)
            {
                return new HttpResponseMessage(HttpStatusCode.BadRequest);
            }

            if (request.RequestUri.Host.Contains("login.microsoftonline.com", StringComparison.OrdinalIgnoreCase))
            {
                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent(tokenJson, Encoding.UTF8, "application/json"),
                };
            }

            if (request.RequestUri.AbsolutePath.EndsWith("/vulnerabilities", StringComparison.OrdinalIgnoreCase))
            {
                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent(summaryJson, Encoding.UTF8, "application/json"),
                };
            }

            if (request.RequestUri.AbsolutePath.Contains("/vulnerability/ADV123456", StringComparison.OrdinalIgnoreCase))
            {
                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent(detailJson, Encoding.UTF8, "application/json"),
                };
            }

            if (request.RequestUri.Host.Contains("download.microsoft.com", StringComparison.OrdinalIgnoreCase))
            {
                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new ByteArrayContent(cvrfBytes)
                    {
                        Headers =
                        {
                            ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/zip"),
                        },
                    },
                };
            }

            return new HttpResponseMessage(HttpStatusCode.NotFound)
            {
                Content = new StringContent($"No canned response for {request.RequestUri}", Encoding.UTF8),
            };
        });
    }

    private static string ReadFixture(string fileName)
        => System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName));

    public Task InitializeAsync() => Task.CompletedTask;

    public Task DisposeAsync() => Task.CompletedTask;
}
