using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Bson;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Nvd;
using StellaOps.Feedser.Source.Nvd.Configuration;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Testing;
using StellaOps.Feedser.Testing;
using System.Net;

namespace StellaOps.Feedser.Source.Nvd.Tests;

[Collection("mongo-fixture")]
public sealed class NvdConnectorHarnessTests : IAsyncLifetime
{
    private readonly ConnectorTestHarness _harness;

    public NvdConnectorHarnessTests(MongoIntegrationFixture fixture)
    {
        _harness = new ConnectorTestHarness(fixture, new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero), NvdOptions.HttpClientName);
    }

    [Fact]
    public async Task FetchAsync_MultiPagePersistsStartIndexMetadata()
    {
        await _harness.ResetAsync();

        var options = new NvdOptions
        {
            BaseEndpoint = new Uri("https://nvd.example.test/api"),
            WindowSize = TimeSpan.FromHours(1),
            WindowOverlap = TimeSpan.FromMinutes(5),
            InitialBackfill = TimeSpan.FromHours(2),
        };

        var timeProvider = _harness.TimeProvider;
        var handler = _harness.Handler;

        var windowStart = timeProvider.GetUtcNow() - options.InitialBackfill;
        var windowEnd = windowStart + options.WindowSize;

        var firstUri = BuildRequestUri(options, windowStart, windowEnd);
        var secondUri = BuildRequestUri(options, windowStart, windowEnd, startIndex: 2);
        var thirdUri = BuildRequestUri(options, windowStart, windowEnd, startIndex: 4);

        handler.AddJsonResponse(firstUri, ReadFixture("nvd-multipage-1.json"));
        handler.AddJsonResponse(secondUri, ReadFixture("nvd-multipage-2.json"));
        handler.AddJsonResponse(thirdUri, ReadFixture("nvd-multipage-3.json"));

        await _harness.EnsureServiceProviderAsync(services =>
        {
            services.AddNvdConnector(opts =>
            {
                opts.BaseEndpoint = options.BaseEndpoint;
                opts.WindowSize = options.WindowSize;
                opts.WindowOverlap = options.WindowOverlap;
                opts.InitialBackfill = options.InitialBackfill;
            });
        });

        var provider = _harness.ServiceProvider;
        var connector = new NvdConnectorPlugin().Create(provider);

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

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

        var firstDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, firstUri.ToString(), CancellationToken.None);
        Assert.NotNull(firstDocument);
        Assert.Equal("0", firstDocument!.Metadata["startIndex"]);

        var secondDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, secondUri.ToString(), CancellationToken.None);
        Assert.NotNull(secondDocument);
        Assert.Equal("2", secondDocument!.Metadata["startIndex"]);

        var thirdDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, thirdUri.ToString(), CancellationToken.None);
        Assert.NotNull(thirdDocument);
        Assert.Equal("4", thirdDocument!.Metadata["startIndex"]);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pending)
            ? pending.AsBsonArray
            : new BsonArray();
        Assert.Equal(3, pendingDocuments.Count);
    }

    public Task InitializeAsync() => Task.CompletedTask;

    public Task DisposeAsync() => _harness.ResetAsync();

    private static Uri BuildRequestUri(NvdOptions options, DateTimeOffset start, DateTimeOffset end, int startIndex = 0)
    {
        var builder = new UriBuilder(options.BaseEndpoint);
        var parameters = new Dictionary<string, string>
        {
            ["lastModifiedStartDate"] = start.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
            ["lastModifiedEndDate"] = end.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
            ["resultsPerPage"] = "2000",
        };

        if (startIndex > 0)
        {
            parameters["startIndex"] = startIndex.ToString(CultureInfo.InvariantCulture);
        }

        builder.Query = string.Join("&", parameters.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
        return builder.Uri;
    }

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

        var secondary = Path.Combine(baseDirectory, "Nvd", "Fixtures", filename);
        if (File.Exists(secondary))
        {
            return File.ReadAllText(secondary);
        }

        throw new FileNotFoundException($"Fixture '{filename}' was not found in the test output directory.");
    }
}
