using System.Diagnostics.Metrics;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Cve.Configuration;
using StellaOps.Feedser.Source.Cve.Internal;
using StellaOps.Feedser.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 Xunit.Abstractions;

namespace StellaOps.Feedser.Source.Cve.Tests;

[Collection("mongo-fixture")]
public sealed class CveConnectorTests : IAsyncLifetime
{
    private readonly MongoIntegrationFixture _fixture;
    private readonly ITestOutputHelper _output;
    private ConnectorTestHarness? _harness;

    public CveConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
    {
        _fixture = fixture;
        _output = output;
    }

    [Fact]
    public async Task FetchParseMap_EmitsCanonicalAdvisory()
    {
        var initialTime = new DateTimeOffset(2024, 10, 1, 0, 0, 0, TimeSpan.Zero);
        await EnsureHarnessAsync(initialTime);
        var harness = _harness!;

        var since = initialTime - TimeSpan.FromDays(30);
        var listUri = new Uri($"https://cve.test/api/cve?time_modified.gte={Uri.EscapeDataString(since.ToString("O"))}&time_modified.lte={Uri.EscapeDataString(initialTime.ToString("O"))}&page=1&size=5");
        harness.Handler.AddJsonResponse(listUri, ReadFixture("Fixtures/cve-list.json"));
        harness.Handler.SetFallback(request =>
        {
            if (request.RequestUri is null)
            {
                return new HttpResponseMessage(HttpStatusCode.NotFound);
            }

            if (request.RequestUri.AbsoluteUri.Equals("https://cve.test/api/cve/CVE-2024-0001", StringComparison.OrdinalIgnoreCase))
            {
                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent(ReadFixture("Fixtures/cve-CVE-2024-0001.json"), Encoding.UTF8, "application/json")
                };
            }

            return new HttpResponseMessage(HttpStatusCode.NotFound);
        });

        var metrics = new Dictionary<string, long>(StringComparer.Ordinal);
        using var listener = new MeterListener
        {
            InstrumentPublished = (instrument, meterListener) =>
            {
                if (instrument.Meter.Name == CveDiagnostics.MeterName)
                {
                    meterListener.EnableMeasurementEvents(instrument);
                }
            }
        };
        listener.SetMeasurementEventCallback<long>((instrument, value, tags, state) =>
        {
            if (metrics.TryGetValue(instrument.Name, out var existing))
            {
                metrics[instrument.Name] = existing + value;
            }
            else
            {
                metrics[instrument.Name] = value;
            }
        });
        listener.Start();

        var connector = new CveConnectorPlugin().Create(harness.ServiceProvider);

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

        listener.Dispose();

        var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
        var advisory = await advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
        Assert.NotNull(advisory);

        var snapshot = SnapshotSerializer.ToSnapshot(advisory!).Replace("\r\n", "\n").TrimEnd();
        var expected = ReadFixture("Fixtures/expected-CVE-2024-0001.json").Replace("\r\n", "\n").TrimEnd();

        if (!string.Equals(expected, snapshot, StringComparison.Ordinal))
        {
            var actualPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "expected-CVE-2024-0001.actual.json");
            Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
            File.WriteAllText(actualPath, snapshot);
        }

        Assert.Equal(expected, snapshot);
        harness.Handler.AssertNoPendingResponses();

        _output.WriteLine("CVE connector smoke metrics:");
        foreach (var entry in metrics.OrderBy(static pair => pair.Key, StringComparer.Ordinal))
        {
            _output.WriteLine($"  {entry.Key} = {entry.Value}");
        }
    }

    [Fact]
    public async Task FetchWithoutCredentials_SeedsFromDirectory()
    {
        var initialTime = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
        var projectRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));
        var repositoryRoot = Path.GetFullPath(Path.Combine(projectRoot, "..", ".."));
        var seedDirectory = Path.Combine(repositoryRoot, "seed-data", "cve", "2025-10-15");
        Assert.True(Directory.Exists(seedDirectory), $"Seed directory '{seedDirectory}' was not found.");

        await using var harness = new ConnectorTestHarness(_fixture, initialTime, CveOptions.HttpClientName);
        await harness.EnsureServiceProviderAsync(services =>
        {
            services.AddLogging(builder =>
            {
                builder.ClearProviders();
                builder.AddProvider(new TestOutputLoggerProvider(_output, LogLevel.Information));
                builder.SetMinimumLevel(LogLevel.Information);
            });
            services.AddCveConnector(options =>
            {
                options.BaseEndpoint = new Uri("https://cve.test/api/", UriKind.Absolute);
                options.SeedDirectory = seedDirectory;
                options.PageSize = 5;
                options.MaxPagesPerFetch = 1;
                options.InitialBackfill = TimeSpan.FromDays(30);
                options.RequestDelay = TimeSpan.Zero;
            });
        });

        var connector = new CveConnectorPlugin().Create(harness.ServiceProvider);
        await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);

        Assert.Empty(harness.Handler.Requests);

        var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
        var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
        var keys = advisories.Select(advisory => advisory.AdvisoryKey).ToArray();

        Assert.Contains("CVE-2024-0001", keys);
        Assert.Contains("CVE-2024-4567", keys);
    }

    private async Task EnsureHarnessAsync(DateTimeOffset initialTime)
    {
        if (_harness is not null)
        {
            return;
        }

        var harness = new ConnectorTestHarness(_fixture, initialTime, CveOptions.HttpClientName);
        await harness.EnsureServiceProviderAsync(services =>
        {
            services.AddLogging(builder =>
            {
                builder.ClearProviders();
                builder.AddProvider(new TestOutputLoggerProvider(_output, LogLevel.Information));
                builder.SetMinimumLevel(LogLevel.Information);
            });
            services.AddCveConnector(options =>
            {
                options.BaseEndpoint = new Uri("https://cve.test/api/", UriKind.Absolute);
                options.ApiOrg = "test-org";
                options.ApiUser = "test-user";
                options.ApiKey = "test-key";
                options.InitialBackfill = TimeSpan.FromDays(30);
                options.PageSize = 5;
                options.MaxPagesPerFetch = 2;
                options.RequestDelay = TimeSpan.Zero;
            });
        });

        _harness = harness;
    }

    private static string ReadFixture(string relativePath)
    {
        var path = Path.Combine(AppContext.BaseDirectory, relativePath);
        return File.ReadAllText(path);
    }

    public async Task InitializeAsync()
    {
        await Task.CompletedTask;
    }

    public async Task DisposeAsync()
    {
        if (_harness is not null)
        {
            await _harness.DisposeAsync();
        }
    }

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

        public TestOutputLoggerProvider(ITestOutputHelper output, LogLevel minLevel)
        {
            _output = output;
            _minLevel = minLevel;
        }

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

        public void Dispose()
        {
        }

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

            public TestOutputLogger(ITestOutputHelper output, LogLevel minLevel)
            {
                _output = output;
                _minLevel = minLevel;
            }

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

            public bool IsEnabled(LogLevel logLevel) => logLevel >= _minLevel;

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