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

namespace StellaOps.Feedser.Source.CertBund.Tests;

[Collection("mongo-fixture")]
public sealed class CertBundConnectorTests : IAsyncLifetime
{
    private static readonly Uri FeedUri = new("https://test.local/content/public/securityAdvisory/rss");
    private static readonly Uri PortalUri = new("https://test.local/portal/");
    private static readonly Uri DetailUri = new("https://test.local/portal/api/securityadvisory?name=WID-SEC-2025-2264");

    private readonly MongoIntegrationFixture _fixture;
    private readonly CannedHttpMessageHandler _handler;

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

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

        var connector = provider.GetRequiredService<CertBundConnector>();
        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("WID-SEC-2025-2264");
        advisory.Aliases.Should().Contain("CVE-2025-1234");
        advisory.AffectedPackages.Should().Contain(package => package.Identifier.Contains("Ivanti"));
        advisory.References.Should().Contain(reference => reference.Url == DetailUri.ToString());
        advisory.Language.Should().Be("de");

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

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

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

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var document = await documentStore.FindBySourceAndUriAsync(CertBundConnectorPlugin.SourceName, DetailUri.ToString(), CancellationToken.None);
        document.Should().NotBeNull();
        document!.Metadata.Should().ContainKey("certbund.advisoryId").WhoseValue.Should().Be("WID-SEC-2025-2264");
        document.Metadata.Should().ContainKey("certbund.category");
        document.Metadata.Should().ContainKey("certbund.published");
        document.Status.Should().Be(DocumentStatuses.PendingParse);

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

    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.AddMongoStorage(options =>
        {
            options.ConnectionString = _fixture.Runner.ConnectionString;
            options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
            options.CommandTimeout = TimeSpan.FromSeconds(5);
        });

        services.AddSourceCommon();
        services.AddCertBundConnector(options =>
        {
            options.FeedUri = FeedUri;
            options.PortalBootstrapUri = PortalUri;
            options.DetailApiUri = new Uri("https://test.local/portal/api/securityadvisory");
            options.RequestDelay = TimeSpan.Zero;
            options.MaxAdvisoriesPerFetch = 10;
            options.MaxKnownAdvisories = 32;
        });

        services.Configure<HttpClientFactoryOptions>(CertBundOptions.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 SeedResponses()
    {
        AddJsonResponse(DetailUri, ReadFixture("certbund-detail.json"));
        AddXmlResponse(FeedUri, ReadFixture("certbund-feed.xml"), "application/rss+xml");
        AddHtmlResponse(PortalUri, "<html><body>OK</body></html>");
    }

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

            return response;
        });
    }

    private void AddXmlResponse(Uri uri, string xml, string contentType)
    {
        _handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
        {
            Content = new StringContent(xml, Encoding.UTF8, contentType),
        });
    }

    private void AddHtmlResponse(Uri uri, string html)
    {
        _handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
        {
            Content = new StringContent(html, Encoding.UTF8, "text/html"),
        });
    }

    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;
}
