using System;
using System.Collections.Generic;
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 FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Bson;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.CertCc;
using StellaOps.Feedser.Source.CertCc.Configuration;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Cursors;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Testing;
using Xunit;

namespace StellaOps.Feedser.Source.CertCc.Tests.CertCc;

[Collection("mongo-fixture")]
public sealed class CertCcConnectorSnapshotTests : IAsyncLifetime
{
    private static readonly Uri SeptemberSummaryUri = new("https://www.kb.cert.org/vuls/api/2025/09/summary/");
    private static readonly Uri OctoberSummaryUri = new("https://www.kb.cert.org/vuls/api/2025/10/summary/");
    private static readonly Uri NovemberSummaryUri = new("https://www.kb.cert.org/vuls/api/2025/11/summary/");
    private static readonly Uri NoteDetailUri = new("https://www.kb.cert.org/vuls/api/294418/");
    private static readonly Uri VendorsDetailUri = new("https://www.kb.cert.org/vuls/api/294418/vendors/");
    private static readonly Uri VulsDetailUri = new("https://www.kb.cert.org/vuls/api/294418/vuls/");
    private static readonly Uri VendorStatusesDetailUri = new("https://www.kb.cert.org/vuls/api/294418/vendors/vuls/");

    private static readonly Uri YearlySummaryUri = new("https://www.kb.cert.org/vuls/api/2025/summary/");

    private readonly MongoIntegrationFixture _fixture;
    private ConnectorTestHarness? _harness;

    public CertCcConnectorSnapshotTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task FetchSummaryAndDetails_ProducesDeterministicSnapshots()
    {
        var initialTime = new DateTimeOffset(2025, 11, 1, 8, 0, 0, TimeSpan.Zero);
        var harness = await EnsureHarnessAsync(initialTime);

        RegisterSummaryResponses(harness.Handler);
        RegisterDetailResponses(harness.Handler);

        var connector = harness.ServiceProvider.GetRequiredService<CertCcConnector>();
        await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);

        var documentsSnapshot = await BuildDocumentsSnapshotAsync(harness.ServiceProvider);
        WriteOrAssertSnapshot(documentsSnapshot, "certcc-documents.snapshot.json");

        var stateSnapshot = await BuildStateSnapshotAsync(harness.ServiceProvider);
        WriteOrAssertSnapshot(stateSnapshot, "certcc-state.snapshot.json");

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

        var advisoriesSnapshot = await BuildAdvisoriesSnapshotAsync(harness.ServiceProvider);
        WriteOrAssertSnapshot(advisoriesSnapshot, "certcc-advisories.snapshot.json");

        harness.TimeProvider.Advance(TimeSpan.FromMinutes(30));
        RegisterSummaryNotModifiedResponses(harness.Handler);

        await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
        var recordedRequests = harness.Handler.Requests
            .Select(request => request.Uri.ToString())
            .ToArray();
        recordedRequests.Should().Equal(new[]
        {
            SeptemberSummaryUri.ToString(),
            OctoberSummaryUri.ToString(),
            NoteDetailUri.ToString(),
            VendorsDetailUri.ToString(),
            VulsDetailUri.ToString(),
            VendorStatusesDetailUri.ToString(),
            YearlySummaryUri.ToString(),
            OctoberSummaryUri.ToString(),
            NovemberSummaryUri.ToString(),
            YearlySummaryUri.ToString(),
        });
        harness.Handler.AssertNoPendingResponses();

        var requestsSnapshot = BuildRequestsSnapshot(harness.Handler.Requests);
        WriteOrAssertSnapshot(requestsSnapshot, "certcc-requests.snapshot.json");
    }

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

        var harness = new ConnectorTestHarness(_fixture, initialTime, CertCcOptions.HttpClientName);
        await harness.EnsureServiceProviderAsync(services =>
        {
            services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
            services.AddCertCcConnector(options =>
            {
                options.BaseApiUri = new Uri("https://www.kb.cert.org/vuls/api/", UriKind.Absolute);
                options.SummaryWindow = new TimeWindowCursorOptions
                {
                    WindowSize = TimeSpan.FromDays(30),
                    Overlap = TimeSpan.FromDays(3),
                    InitialBackfill = TimeSpan.FromDays(45),
                    MinimumWindowSize = TimeSpan.FromDays(1),
                };
                options.MaxMonthlySummaries = 2;
                options.MaxNotesPerFetch = 1;
                options.DetailRequestDelay = TimeSpan.Zero;
                options.EnableDetailMapping = true;
            });

            services.Configure<HttpClientFactoryOptions>(CertCcOptions.HttpClientName, options =>
            {
                options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = harness.Handler);
            });
        });

        _harness = harness;
        return harness;
    }

    private static async Task<string> BuildDocumentsSnapshotAsync(IServiceProvider provider)
    {
        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var uris = new[]
        {
            SeptemberSummaryUri,
            OctoberSummaryUri,
            NovemberSummaryUri,
            YearlySummaryUri,
            NoteDetailUri,
            VendorsDetailUri,
            VulsDetailUri,
            VendorStatusesDetailUri,
        };

        var records = new List<object>(uris.Length);
        foreach (var uri in uris)
        {
            var record = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, uri.ToString(), CancellationToken.None);
            if (record is null)
            {
                continue;
            }

            var lastModified = record.Headers is not null
                && record.Headers.TryGetValue("Last-Modified", out var lastModifiedHeader)
                && DateTimeOffset.TryParse(lastModifiedHeader, out var parsedLastModified)
                    ? parsedLastModified.ToUniversalTime().ToString("O")
                    : record.LastModified?.ToUniversalTime().ToString("O");

            records.Add(new
            {
                record.Uri,
                record.Status,
                record.Sha256,
                record.ContentType,
                LastModified = lastModified,
                Metadata = record.Metadata is null
                    ? null
                    : record.Metadata
                        .OrderBy(static pair => pair.Key, StringComparer.OrdinalIgnoreCase)
                        .ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.OrdinalIgnoreCase),
                record.Etag,
            });
        }

        var ordered = records
            .OrderBy(static entry => entry.GetType().GetProperty("Uri")?.GetValue(entry)?.ToString(), StringComparer.Ordinal)
            .ToArray();

        return SnapshotSerializer.ToSnapshot(ordered);
    }

    private static async Task<string> BuildStateSnapshotAsync(IServiceProvider provider)
    {
        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);

        var cursor = state!.Cursor ?? new BsonDocument();

        BsonDocument? summaryDocument = null;
        if (cursor.TryGetValue("summary", out var summaryValue) && summaryValue is BsonDocument summaryDoc)
        {
            summaryDocument = summaryDoc;
        }

        var summary = summaryDocument is null
            ? null
            : new
            {
                Start = summaryDocument.TryGetValue("start", out var startValue) ? ToIsoString(startValue) : null,
                End = summaryDocument.TryGetValue("end", out var endValue) ? ToIsoString(endValue) : null,
            };

        var snapshot = new
        {
            Summary = summary,
            PendingNotes = cursor.TryGetValue("pendingNotes", out var pendingNotesValue)
                ? pendingNotesValue.AsBsonArray.Select(static value => value.ToString()).OrderBy(static note => note, StringComparer.OrdinalIgnoreCase).ToArray()
                : Array.Empty<string>(),
            PendingSummaries = cursor.TryGetValue("pendingSummaries", out var pendingSummariesValue)
                ? pendingSummariesValue.AsBsonArray.Select(static value => value.ToString()).OrderBy(static item => item, StringComparer.OrdinalIgnoreCase).ToArray()
                : Array.Empty<string>(),
            LastRun = cursor.TryGetValue("lastRun", out var lastRunValue) ? ToIsoString(lastRunValue) : null,
            state.LastSuccess,
            state.LastFailure,
            state.FailCount,
            state.BackoffUntil,
        };

        return SnapshotSerializer.ToSnapshot(snapshot);
    }

    private static async Task<string> BuildAdvisoriesSnapshotAsync(IServiceProvider provider)
    {
        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var advisories = new List<Advisory>();
        await foreach (var advisory in advisoryStore.StreamAsync(CancellationToken.None))
        {
            advisories.Add(advisory);
        }

        var ordered = advisories
            .OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.Ordinal)
            .ToArray();

        return SnapshotSerializer.ToSnapshot(ordered);
    }

    private static string BuildRequestsSnapshot(IReadOnlyCollection<CannedHttpMessageHandler.CannedRequestRecord> requests)
    {
        var ordered = requests
            .OrderBy(static request => request.Timestamp)
            .Select(static request => new
            {
                request.Method.Method,
                Uri = request.Uri.ToString(),
                Headers = new
                {
                    Accept = TryGetHeader(request.Headers, "Accept"),
                    IfNoneMatch = TryGetHeader(request.Headers, "If-None-Match"),
                    IfModifiedSince = TryGetHeader(request.Headers, "If-Modified-Since"),
                },
            })
            .ToArray();

        return SnapshotSerializer.ToSnapshot(ordered);
    }

    private static void RegisterSummaryResponses(CannedHttpMessageHandler handler)
    {
        AddJsonResponse(handler, SeptemberSummaryUri, "summary-2025-09.json", "\"certcc-summary-2025-09\"", new DateTimeOffset(2025, 9, 30, 12, 0, 0, TimeSpan.Zero));
        AddJsonResponse(handler, OctoberSummaryUri, "summary-2025-10.json", "\"certcc-summary-2025-10\"", new DateTimeOffset(2025, 10, 31, 12, 0, 0, TimeSpan.Zero));
        AddJsonResponse(handler, YearlySummaryUri, "summary-2025.json", "\"certcc-summary-2025\"", new DateTimeOffset(2025, 10, 31, 12, 1, 0, TimeSpan.Zero));
    }

    private static void RegisterSummaryNotModifiedResponses(CannedHttpMessageHandler handler)
    {
        AddNotModified(handler, OctoberSummaryUri, "\"certcc-summary-2025-10\"");
        AddNotModified(handler, NovemberSummaryUri, "\"certcc-summary-2025-11\"");
        AddNotModified(handler, YearlySummaryUri, "\"certcc-summary-2025\"");
    }

    private static void RegisterDetailResponses(CannedHttpMessageHandler handler)
    {
        AddJsonResponse(handler, NoteDetailUri, "vu-294418.json", "\"certcc-note-294418\"", new DateTimeOffset(2025, 10, 9, 16, 52, 0, TimeSpan.Zero));
        AddJsonResponse(handler, VendorsDetailUri, "vu-294418-vendors.json", "\"certcc-vendors-294418\"", new DateTimeOffset(2025, 10, 9, 17, 5, 0, TimeSpan.Zero));
        AddJsonResponse(handler, VulsDetailUri, "vu-294418-vuls.json", "\"certcc-vuls-294418\"", new DateTimeOffset(2025, 10, 9, 17, 10, 0, TimeSpan.Zero));
        AddJsonResponse(handler, VendorStatusesDetailUri, "vendor-statuses-294418.json", "\"certcc-vendor-statuses-294418\"", new DateTimeOffset(2025, 10, 9, 17, 12, 0, TimeSpan.Zero));
    }

    private static void AddJsonResponse(CannedHttpMessageHandler handler, Uri uri, string fixtureName, string etag, DateTimeOffset lastModified)
    {
        var payload = ReadFixture(fixtureName);
        handler.AddResponse(HttpMethod.Get, uri, _ =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(payload, Encoding.UTF8, "application/json"),
            };
            response.Headers.ETag = new EntityTagHeaderValue(etag);
            response.Headers.TryAddWithoutValidation("Last-Modified", lastModified.ToString("R"));
            response.Content.Headers.LastModified = lastModified;
            return response;
        });
    }

    private static void AddNotModified(CannedHttpMessageHandler handler, Uri uri, string etag)
    {
        handler.AddResponse(HttpMethod.Get, uri, _ =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.NotModified);
            response.Headers.ETag = new EntityTagHeaderValue(etag);
            return response;
        });
    }

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

        var fallback = Path.Combine(baseDir, "Source", "CertCc", "Fixtures", filename);
        if (File.Exists(fallback))
        {
            return File.ReadAllText(fallback);
        }

        throw new FileNotFoundException($"Missing CERT/CC fixture '{filename}'.");
    }

    private static string? TryGetHeader(IReadOnlyDictionary<string, string> headers, string key)
        => headers.TryGetValue(key, out var value) ? value : null;

    private static string? ToIsoString(BsonValue value)
    {
        return value.BsonType switch
        {
            BsonType.DateTime => value.ToUniversalTime().ToString("O"),
            BsonType.String when DateTimeOffset.TryParse(value.AsString, out var parsed) => parsed.ToUniversalTime().ToString("O"),
            _ => null,
        };
    }

    private static void WriteOrAssertSnapshot(string snapshot, string filename)
    {
        var normalizedSnapshot = Normalize(snapshot);
        if (ShouldUpdateFixtures() || !FixtureExists(filename))
        {
            var path = GetWritablePath(filename);
            Directory.CreateDirectory(Path.GetDirectoryName(path)!);
            File.WriteAllText(path, normalizedSnapshot);
            return;
        }

        var expected = ReadFixture(filename);
        var normalizedExpected = Normalize(expected);
        if (!string.Equals(normalizedExpected, normalizedSnapshot, StringComparison.Ordinal))
        {
            var actualPath = Path.Combine(Path.GetDirectoryName(GetWritablePath(filename))!, Path.GetFileNameWithoutExtension(filename) + ".actual.json");
            Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
            File.WriteAllText(actualPath, normalizedSnapshot);
        }

        Assert.Equal(normalizedExpected, normalizedSnapshot);
    }

    private static string GetWritablePath(string filename)
    {
        var baseDir = AppContext.BaseDirectory;
        return Path.Combine(baseDir, "Source", "CertCc", "Fixtures", filename);
    }

    private static string Normalize(string value)
        => value.Replace("\r\n", "\n", StringComparison.Ordinal).TrimEnd();

    private static bool ShouldUpdateFixtures()
    {
        var flag = Environment.GetEnvironmentVariable("UPDATE_CERTCC_FIXTURES");
        return string.Equals(flag, "1", StringComparison.Ordinal) || string.Equals(flag, "true", StringComparison.OrdinalIgnoreCase);
    }

    private static bool FixtureExists(string filename)
    {
        var baseDir = AppContext.BaseDirectory;
        var primary = Path.Combine(baseDir, "Source", "CertCc", "Fixtures", filename);
        if (File.Exists(primary))
        {
            return true;
        }

        var fallback = Path.Combine(baseDir, "Fixtures", filename);
        return File.Exists(fallback);
    }

    public Task InitializeAsync() => Task.CompletedTask;

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