using System.Collections.Generic;
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 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.Driver;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Distro.Debian.Configuration;
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 Xunit.Abstractions;

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

[Collection("mongo-fixture")]
public sealed class DebianConnectorTests : IAsyncLifetime
{
    private static readonly Uri ListUri = new("https://salsa.debian.org/security-tracker-team/security-tracker/-/raw/master/data/DSA/list");
    private static readonly Uri DetailResolved = new("https://security-tracker.debian.org/tracker/DSA-2024-123");
    private static readonly Uri DetailOpen = new("https://security-tracker.debian.org/tracker/DSA-2024-124");

    private readonly MongoIntegrationFixture _fixture;
    private readonly FakeTimeProvider _timeProvider;
    private readonly CannedHttpMessageHandler _handler;
    private readonly Dictionary<Uri, Func<HttpRequestMessage, HttpResponseMessage>> _fallbackFactories = new();
    private readonly ITestOutputHelper _output;

    public DebianConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
    {
        _fixture = fixture;
        _handler = new CannedHttpMessageHandler();
        _handler.SetFallback(request =>
        {
            if (request.RequestUri is null)
            {
                throw new InvalidOperationException("Request URI required for fallback response.");
            }

            if (_fallbackFactories.TryGetValue(request.RequestUri, out var factory))
            {
                return factory(request);
            }

            throw new InvalidOperationException($"No canned or fallback response registered for {request.Method} {request.RequestUri}.");
        });
        _timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 9, 12, 0, 0, 0, TimeSpan.Zero));
        _output = output;
    }

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

        SeedInitialResponses();

        var connector = provider.GetRequiredService<DebianConnector>();
        await connector.FetchAsync(provider, CancellationToken.None);
        _timeProvider.Advance(TimeSpan.FromMinutes(1));
        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.Equal(2, advisories.Count);

        var resolved = advisories.Single(a => a.AdvisoryKey == "DSA-2024-123");
        _output.WriteLine("Resolved aliases: " + string.Join(",", resolved.Aliases));
        var resolvedBookworm = Assert.Single(resolved.AffectedPackages, p => p.Platform == "bookworm");
        var resolvedRange = Assert.Single(resolvedBookworm.VersionRanges);
        Assert.Equal("evr", resolvedRange.RangeKind);
        Assert.Equal("1:1.1.1n-0+deb11u2", resolvedRange.IntroducedVersion);
        Assert.Equal("1:1.1.1n-0+deb11u5", resolvedRange.FixedVersion);
        Assert.NotNull(resolvedRange.Primitives);
        Assert.NotNull(resolvedRange.Primitives!.Evr);
        Assert.Equal(1, resolvedRange.Primitives.Evr!.Introduced!.Epoch);
        Assert.Equal("1.1.1n", resolvedRange.Primitives.Evr.Introduced.UpstreamVersion);

        var open = advisories.Single(a => a.AdvisoryKey == "DSA-2024-124");
        var openBookworm = Assert.Single(open.AffectedPackages, p => p.Platform == "bookworm");
        var openRange = Assert.Single(openBookworm.VersionRanges);
        Assert.Equal("evr", openRange.RangeKind);
        Assert.Equal("1:1.3.1-1", openRange.IntroducedVersion);
        Assert.Null(openRange.FixedVersion);
        Assert.NotNull(openRange.Primitives);
        Assert.NotNull(openRange.Primitives!.Evr);

        // Ensure data persisted through Mongo round-trip.
        var found = await advisoryStore.FindAsync("DSA-2024-123", CancellationToken.None);
        Assert.NotNull(found);
        var persistedRange = Assert.Single(found!.AffectedPackages, pkg => pkg.Platform == "bookworm").VersionRanges.Single();
        Assert.NotNull(persistedRange.Primitives);
        Assert.NotNull(persistedRange.Primitives!.Evr);

        // Second run should issue conditional requests and no additional parsing/mapping.
        SeedNotModifiedResponses();
        await connector.FetchAsync(provider, CancellationToken.None);
        _timeProvider.Advance(TimeSpan.FromMinutes(1));
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var documents = provider.GetRequiredService<IDocumentStore>();
        var listDoc = await documents.FindBySourceAndUriAsync(DebianConnectorPlugin.SourceName, DetailResolved.ToString(), CancellationToken.None);
        Assert.NotNull(listDoc);

        var refreshed = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
        Assert.Equal(2, refreshed.Count);
    }

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

        var services = new ServiceCollection();
        services.AddLogging(builder => builder.AddProvider(new TestOutputLoggerProvider(_output)));
        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.AddDebianConnector(options =>
        {
            options.ListEndpoint = ListUri;
            options.DetailBaseUri = new Uri("https://security-tracker.debian.org/tracker/");
            options.MaxAdvisoriesPerFetch = 10;
            options.RequestDelay = TimeSpan.Zero;
        });

        services.Configure<HttpClientFactoryOptions>(DebianOptions.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 SeedInitialResponses()
    {
        AddListResponse("debian-list.txt", "\"list-v1\"");
        AddDetailResponse(DetailResolved, "debian-detail-dsa-2024-123.html", "\"detail-123\"");
        AddDetailResponse(DetailOpen, "debian-detail-dsa-2024-124.html", "\"detail-124\"");
    }

    private void SeedNotModifiedResponses()
    {
        AddNotModifiedResponse(ListUri, "\"list-v1\"");
        AddNotModifiedResponse(DetailResolved, "\"detail-123\"");
        AddNotModifiedResponse(DetailOpen, "\"detail-124\"");
    }

    private void AddListResponse(string fixture, string etag)
    {
        RegisterResponseFactory(ListUri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/plain"),
            };
            response.Headers.ETag = new EntityTagHeaderValue(etag);
            return response;
        });
    }

    private void AddDetailResponse(Uri uri, string fixture, string etag)
    {
        RegisterResponseFactory(uri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/html"),
            };
            response.Headers.ETag = new EntityTagHeaderValue(etag);
            return response;
        });
    }

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

    private void RegisterResponseFactory(Uri uri, Func<HttpResponseMessage> factory)
    {
        _handler.AddResponse(uri, () => factory());
        _fallbackFactories[uri] = _ => factory();
    }

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

        foreach (var candidate in candidates)
        {
            var fullPath = Path.GetFullPath(candidate);
            if (File.Exists(fullPath))
            {
                return File.ReadAllText(fullPath);
            }
        }

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

    public Task InitializeAsync() => Task.CompletedTask;

    public Task DisposeAsync() => Task.CompletedTask;

    private sealed class TestOutputLoggerProvider : ILoggerProvider
    {
        private readonly ITestOutputHelper _output;

        public TestOutputLoggerProvider(ITestOutputHelper output) => _output = output;

        public ILogger CreateLogger(string categoryName) => new TestOutputLogger(_output);

        public void Dispose()
        {
        }

        private sealed class TestOutputLogger : ILogger
        {
            private readonly ITestOutputHelper _output;

            public TestOutputLogger(ITestOutputHelper output) => _output = output;

            public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullLogger.Instance.BeginScope(state);

            public bool IsEnabled(LogLevel logLevel) => false;

            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
            {
                if (IsEnabled(logLevel))
                {
                    _output.WriteLine(formatter(state, exception));
                }
            }
        }
    }
}
