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 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.Bson;
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.Vndr.Adobe;
using StellaOps.Feedser.Source.Vndr.Adobe.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.Storage.Mongo.PsirtFlags;
using StellaOps.Feedser.Testing;

namespace StellaOps.Feedser.Source.Vndr.Adobe.Tests;

[Collection("mongo-fixture")]
public sealed class AdobeConnectorFetchTests : IAsyncLifetime
{
    private readonly MongoIntegrationFixture _fixture;
    private readonly FakeTimeProvider _timeProvider;

    public AdobeConnectorFetchTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
        _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 9, 10, 0, 0, 0, TimeSpan.Zero));
    }

    [Fact]
    public async Task Fetch_WindowsIndexAndPersistsCursor()
    {
        var handler = new CannedHttpMessageHandler();
        await using var provider = await BuildServiceProviderAsync(handler);
        SeedIndex(handler);
        SeedDetail(handler);

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

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        var cursor = state!.Cursor;
        var pendingDocuments = ExtractGuidList(cursor, "pendingDocuments");
        Assert.Equal(2, pendingDocuments.Count);

        // Re-seed responses to simulate unchanged fetch
        SeedIndex(handler);
        SeedDetail(handler);
        await connector.FetchAsync(provider, CancellationToken.None);

        state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        cursor = state!.Cursor;
        var afterPending = ExtractGuidList(cursor, "pendingDocuments");
        Assert.Equal(pendingDocuments.OrderBy(static id => id), afterPending.OrderBy(static id => id));

        var fetchCache = cursor.TryGetValue("fetchCache", out var fetchCacheValue) && fetchCacheValue is BsonDocument cacheDoc
            ? cacheDoc.Elements.Select(static e => e.Name).ToArray()
            : Array.Empty<string>();
        Assert.Contains("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html", fetchCache);
        Assert.Contains("https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html", fetchCache);
    }

    [Fact]
    public async Task Parse_ProducesDtoAndClearsPendingDocuments()
    {
        var handler = new CannedHttpMessageHandler();
        await using var provider = await BuildServiceProviderAsync(handler);
        SeedIndex(handler);
        SeedDetail(handler);

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

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var dtoStore = provider.GetRequiredService<IDtoStore>();
        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var psirtStore = provider.GetRequiredService<IPsirtFlagStore>();
        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();

        var document = await documentStore.FindBySourceAndUriAsync(
            VndrAdobeConnectorPlugin.SourceName,
            "https://helpx.adobe.com/security/products/acrobat/apsb25-85.html",
            CancellationToken.None);

        Assert.NotNull(document);

        var dtoRecord = await dtoStore.FindByDocumentIdAsync(document!.Id, CancellationToken.None);
        Assert.NotNull(dtoRecord);
        Assert.Equal("adobe.bulletin.v1", dtoRecord!.SchemaVersion);
        var payload = dtoRecord.Payload;
        Assert.Equal("APSB25-85", payload.GetValue("advisoryId").AsString);
        Assert.Equal("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html", payload.GetValue("detailUrl").AsString);

        var products = payload.GetValue("products").AsBsonArray
            .Select(static value => value.AsBsonDocument)
            .ToArray();
        Assert.NotEmpty(products);
        var acrobatWindowsProduct = Assert.Single(
            products,
            static doc => string.Equals(doc.GetValue("product").AsString, "Acrobat DC", StringComparison.Ordinal)
                && string.Equals(doc.GetValue("platform").AsString, "Windows", StringComparison.Ordinal));
        Assert.Equal("25.001.20672 and earlier", acrobatWindowsProduct.GetValue("affectedVersion").AsString);
        Assert.Equal("25.001.20680", acrobatWindowsProduct.GetValue("updatedVersion").AsString);

        var acrobatMacProduct = Assert.Single(
            products,
            static doc => string.Equals(doc.GetValue("product").AsString, "Acrobat DC", StringComparison.Ordinal)
                && string.Equals(doc.GetValue("platform").AsString, "macOS", StringComparison.Ordinal));
        Assert.Equal("25.001.20668 and earlier", acrobatMacProduct.GetValue("affectedVersion").AsString);
        Assert.Equal("25.001.20678", acrobatMacProduct.GetValue("updatedVersion").AsString);

        var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        var cursor = state!.Cursor;
        Assert.True(!cursor.TryGetValue("pendingDocuments", out _)
            || cursor.GetValue("pendingDocuments").AsBsonArray.Count == 0);
        Assert.True(!cursor.TryGetValue("pendingMappings", out _)
            || cursor.GetValue("pendingMappings").AsBsonArray.Count == 0);

        var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None);
        Assert.Equal(2, advisories.Count);

        var acrobatAdvisory = advisories.Single(a => a.AdvisoryKey == "APSB25-85");
        Assert.Contains("APSB25-85", acrobatAdvisory.Aliases);
        Assert.Equal(
            acrobatAdvisory.References.Select(static r => r.Url).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
            acrobatAdvisory.References.Length);
        var acrobatWindowsPackage = Assert.Single(
            acrobatAdvisory.AffectedPackages,
            pkg => string.Equals(pkg.Identifier, "Acrobat DC", StringComparison.Ordinal)
                && string.Equals(pkg.Platform, "Windows", StringComparison.Ordinal));
        var acrobatWindowsRange = Assert.Single(acrobatWindowsPackage.VersionRanges);
        Assert.Equal("vendor", acrobatWindowsRange.RangeKind);
        Assert.Equal("25.001.20680", acrobatWindowsRange.FixedVersion);
        Assert.Equal("25.001.20672", acrobatWindowsRange.LastAffectedVersion);
        Assert.NotNull(acrobatWindowsRange.Primitives);
        var windowsExtensions = acrobatWindowsRange.Primitives!.VendorExtensions;
        Assert.NotNull(windowsExtensions);
        Assert.True(windowsExtensions!.TryGetValue("adobe.affected.raw", out var rawAffectedWin));
        Assert.Equal("25.001.20672 and earlier", rawAffectedWin);
        Assert.True(windowsExtensions.TryGetValue("adobe.updated.raw", out var rawUpdatedWin));
        Assert.Equal("25.001.20680", rawUpdatedWin);
        Assert.Contains(
            AffectedPackageStatusCatalog.Fixed,
            acrobatWindowsPackage.Statuses.Select(static status => status.Status));
        var windowsNormalized = Assert.Single(acrobatWindowsPackage.NormalizedVersions.ToArray());
        Assert.Equal(NormalizedVersionSchemes.SemVer, windowsNormalized.Scheme);
        Assert.Equal(NormalizedVersionRuleTypes.LessThan, windowsNormalized.Type);
        Assert.Equal("25.1.20680", windowsNormalized.Max);
        Assert.False(windowsNormalized.MaxInclusive);
        Assert.Equal("adobe:Acrobat DC:Windows", windowsNormalized.Notes);

        var acrobatMacPackage = Assert.Single(
            acrobatAdvisory.AffectedPackages,
            pkg => string.Equals(pkg.Identifier, "Acrobat DC", StringComparison.Ordinal)
                && string.Equals(pkg.Platform, "macOS", StringComparison.Ordinal));
        var acrobatMacRange = Assert.Single(acrobatMacPackage.VersionRanges);
        Assert.Equal("vendor", acrobatMacRange.RangeKind);
        Assert.Equal("25.001.20678", acrobatMacRange.FixedVersion);
        Assert.Equal("25.001.20668", acrobatMacRange.LastAffectedVersion);
        Assert.NotNull(acrobatMacRange.Primitives);
        var macExtensions = acrobatMacRange.Primitives!.VendorExtensions;
        Assert.NotNull(macExtensions);
        Assert.True(macExtensions!.TryGetValue("adobe.affected.raw", out var rawAffectedMac));
        Assert.Equal("25.001.20668 and earlier", rawAffectedMac);
        Assert.True(macExtensions.TryGetValue("adobe.updated.raw", out var rawUpdatedMac));
        Assert.Equal("25.001.20678", rawUpdatedMac);
        Assert.Contains(
            AffectedPackageStatusCatalog.Fixed,
            acrobatMacPackage.Statuses.Select(static status => status.Status));
        var macNormalized = Assert.Single(acrobatMacPackage.NormalizedVersions.ToArray());
        Assert.Equal(NormalizedVersionSchemes.SemVer, macNormalized.Scheme);
        Assert.Equal(NormalizedVersionRuleTypes.LessThan, macNormalized.Type);
        Assert.Equal("25.1.20678", macNormalized.Max);
        Assert.False(macNormalized.MaxInclusive);
        Assert.Equal("adobe:Acrobat DC:macOS", macNormalized.Notes);

        var premiereAdvisory = advisories.Single(a => a.AdvisoryKey == "APSB25-87");
        Assert.Contains("APSB25-87", premiereAdvisory.Aliases);
        Assert.Equal(
            premiereAdvisory.References.Select(static r => r.Url).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
            premiereAdvisory.References.Length);
        var premiereWindowsPackage = Assert.Single(
            premiereAdvisory.AffectedPackages,
            pkg => string.Equals(pkg.Identifier, "Premiere Pro", StringComparison.Ordinal)
                && string.Equals(pkg.Platform, "Windows", StringComparison.Ordinal));
        var premiereWindowsRange = Assert.Single(premiereWindowsPackage.VersionRanges);
        Assert.Equal("24.6", premiereWindowsRange.FixedVersion);
        Assert.Equal("24.5", premiereWindowsRange.LastAffectedVersion);
        Assert.NotNull(premiereWindowsRange.Primitives);
        var premiereWindowsExtensions = premiereWindowsRange.Primitives!.VendorExtensions;
        Assert.NotNull(premiereWindowsExtensions);
        Assert.True(premiereWindowsExtensions!.TryGetValue("adobe.priority", out var premierePriorityWin));
        Assert.Equal("Priority 3", premierePriorityWin);
        Assert.Contains(
            AffectedPackageStatusCatalog.Fixed,
            premiereWindowsPackage.Statuses.Select(static status => status.Status));
        var premiereWinNormalized = Assert.Single(premiereWindowsPackage.NormalizedVersions.ToArray());
        Assert.Equal(NormalizedVersionSchemes.SemVer, premiereWinNormalized.Scheme);
        Assert.Equal(NormalizedVersionRuleTypes.LessThan, premiereWinNormalized.Type);
        Assert.Equal("24.6", premiereWinNormalized.Max);
        Assert.False(premiereWinNormalized.MaxInclusive);
        Assert.Equal("adobe:Premiere Pro:Windows", premiereWinNormalized.Notes);

        var premiereMacPackage = Assert.Single(
            premiereAdvisory.AffectedPackages,
            pkg => string.Equals(pkg.Identifier, "Premiere Pro", StringComparison.Ordinal)
                && string.Equals(pkg.Platform, "macOS", StringComparison.Ordinal));
        var premiereMacRange = Assert.Single(premiereMacPackage.VersionRanges);
        Assert.Equal("24.6", premiereMacRange.FixedVersion);
        Assert.Equal("24.5", premiereMacRange.LastAffectedVersion);
        Assert.NotNull(premiereMacRange.Primitives);
        var premiereMacExtensions = premiereMacRange.Primitives!.VendorExtensions;
        Assert.NotNull(premiereMacExtensions);
        Assert.True(premiereMacExtensions!.TryGetValue("adobe.priority", out var premierePriorityMac));
        Assert.Equal("Priority 3", premierePriorityMac);
        Assert.Contains(
            AffectedPackageStatusCatalog.Fixed,
            premiereMacPackage.Statuses.Select(static status => status.Status));
        var premiereMacNormalized = Assert.Single(premiereMacPackage.NormalizedVersions.ToArray());
        Assert.Equal(NormalizedVersionSchemes.SemVer, premiereMacNormalized.Scheme);
        Assert.Equal(NormalizedVersionRuleTypes.LessThan, premiereMacNormalized.Type);
        Assert.Equal("24.6", premiereMacNormalized.Max);
        Assert.False(premiereMacNormalized.MaxInclusive);
        Assert.Equal("adobe:Premiere Pro:macOS", premiereMacNormalized.Notes);

        var ordered = advisories.OrderBy(static a => a.AdvisoryKey, StringComparer.Ordinal).ToArray();
        var snapshot = SnapshotSerializer.ToSnapshot(ordered);
        var expected = ReadFixture("adobe-advisories.snapshot.json");
        var normalizedSnapshot = NormalizeLineEndings(snapshot);
        var normalizedExpected = NormalizeLineEndings(expected);
        if (!string.Equals(normalizedExpected, normalizedSnapshot, StringComparison.Ordinal))
        {
            var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Adobe", "Fixtures", "adobe-advisories.actual.json");
            Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
            File.WriteAllText(actualPath, snapshot);
        }

        Assert.Equal(normalizedExpected, normalizedSnapshot);

        var flagsCollection = _fixture.Database.GetCollection<BsonDocument>("psirt_flags");
        var rawFlags = await flagsCollection.Find(Builders<BsonDocument>.Filter.Empty).ToListAsync();
        Assert.NotEmpty(rawFlags);

        var flagRecord = rawFlags.Single(doc => doc["_id"].AsString == "APSB25-87");
        Assert.Equal("Adobe", flagRecord["vendor"].AsString);
    }

    [Fact]
    public async Task Fetch_WithNotModifiedResponses_KeepsDocumentsMapped()
    {
        var handler = new CannedHttpMessageHandler();
        await using var provider = await BuildServiceProviderAsync(handler);
        SeedIndex(handler);
        SeedDetail(handler);

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

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var acrobatDoc = await documentStore.FindBySourceAndUriAsync(
            VndrAdobeConnectorPlugin.SourceName,
            "https://helpx.adobe.com/security/products/acrobat/apsb25-85.html",
            CancellationToken.None);
        Assert.NotNull(acrobatDoc);
        Assert.Equal(DocumentStatuses.Mapped, acrobatDoc!.Status);

        var premiereDoc = await documentStore.FindBySourceAndUriAsync(
            VndrAdobeConnectorPlugin.SourceName,
            "https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html",
            CancellationToken.None);
        Assert.NotNull(premiereDoc);
        Assert.Equal(DocumentStatuses.Mapped, premiereDoc!.Status);

        SeedIndex(handler);
        SeedDetailNotModified(handler);

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

        acrobatDoc = await documentStore.FindBySourceAndUriAsync(
            VndrAdobeConnectorPlugin.SourceName,
            "https://helpx.adobe.com/security/products/acrobat/apsb25-85.html",
            CancellationToken.None);
        Assert.NotNull(acrobatDoc);
        Assert.Equal(DocumentStatuses.Mapped, acrobatDoc!.Status);

        premiereDoc = await documentStore.FindBySourceAndUriAsync(
            VndrAdobeConnectorPlugin.SourceName,
            "https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html",
            CancellationToken.None);
        Assert.NotNull(premiereDoc);
        Assert.Equal(DocumentStatuses.Mapped, premiereDoc!.Status);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0);
        Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMap) && pendingMap.AsBsonArray.Count == 0);
    }

    private async Task<ServiceProvider> BuildServiceProviderAsync(CannedHttpMessageHandler handler)
    {
        await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);

        var services = new ServiceCollection();
        services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
        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.AddAdobeConnector(opts =>
        {
            opts.IndexUri = new Uri("https://helpx.adobe.com/security/security-bulletin.html");
            opts.InitialBackfill = TimeSpan.FromDays(30);
            opts.WindowOverlap = TimeSpan.FromDays(2);
        });

        services.Configure<HttpClientFactoryOptions>(AdobeOptions.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 static void SeedIndex(CannedHttpMessageHandler handler)
    {
        var indexUri = new Uri("https://helpx.adobe.com/security/security-bulletin.html");
        var indexHtml = ReadFixture("adobe-index.html");
        handler.AddTextResponse(indexUri, indexHtml, "text/html");
    }

    private static void SeedDetail(CannedHttpMessageHandler handler)
    {
        AddDetailResponse(
            handler,
            new Uri("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html"),
            "adobe-detail-apsb25-85.html",
            "\"apsb25-85\"");

        AddDetailResponse(
            handler,
            new Uri("https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html"),
            "adobe-detail-apsb25-87.html",
            "\"apsb25-87\"");
    }

    private static void SeedDetailNotModified(CannedHttpMessageHandler handler)
    {
        AddNotModifiedResponse(
            handler,
            new Uri("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html"),
            "\"apsb25-85\"");

        AddNotModifiedResponse(
            handler,
            new Uri("https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html"),
            "\"apsb25-87\"");
    }

    private static void AddDetailResponse(CannedHttpMessageHandler handler, Uri uri, string fixture, string? etag)
    {
        handler.AddResponse(uri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/html"),
            };

            if (!string.IsNullOrEmpty(etag))
            {
                response.Headers.ETag = new EntityTagHeaderValue(etag);
            }

            return response;
        });
    }

    private static void AddNotModifiedResponse(CannedHttpMessageHandler handler, Uri uri, string? etag)
    {
        handler.AddResponse(uri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.NotModified);
            if (!string.IsNullOrEmpty(etag))
            {
                response.Headers.ETag = new EntityTagHeaderValue(etag);
            }

            return response;
        });
    }

    private static List<Guid> ExtractGuidList(BsonDocument cursor, string field)
    {
        if (!cursor.TryGetValue(field, out var value) || value is not BsonArray array)
        {
            return new List<Guid>();
        }

        var list = new List<Guid>(array.Count);
        foreach (var element in array)
        {
            if (Guid.TryParse(element.AsString, out var guid))
            {
                list.Add(guid);
            }
        }
        return list;
    }

    private static string ReadFixture(string name)
    {
        var candidate = Path.Combine(AppContext.BaseDirectory, "Adobe", "Fixtures", name);
        if (!File.Exists(candidate))
        {
            candidate = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Adobe", "Fixtures", name);
        }

        return File.ReadAllText(candidate);
    }

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

    public Task InitializeAsync() => Task.CompletedTask;

    public Task DisposeAsync() => Task.CompletedTask;
}
