using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Driver;
using StellaOps.Feedser.Merge.Services;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.MergeEvents;
using StellaOps.Feedser.Testing;

namespace StellaOps.Feedser.Merge.Tests;

[Collection("mongo-fixture")]
public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime
{
    private readonly MongoIntegrationFixture _fixture;
    private MergeEventStore? _mergeEventStore;
    private MergeEventWriter? _mergeEventWriter;
    private AdvisoryPrecedenceMerger? _merger;
    private FakeTimeProvider? _timeProvider;

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

    [Fact]
    public async Task MergePipeline_PsirtOverridesNvd_AndKevOnlyTogglesExploitKnown()
    {
        await EnsureInitializedAsync();

        var merger = _merger!;
        var writer = _mergeEventWriter!;
        var store = _mergeEventStore!;
        var timeProvider = _timeProvider!;

        var expectedTimestamp = timeProvider.GetUtcNow();

        var nvd = CreateNvdBaseline();
        var vendor = CreateVendorOverride();
        var kev = CreateKevSignal();

        var merged = merger.Merge(new[] { nvd, vendor, kev });

        Assert.Equal("CVE-2025-1000", merged.AdvisoryKey);
        Assert.Equal("Vendor Security Advisory", merged.Title);
        Assert.Equal("Critical impact on supported platforms.", merged.Summary);
        Assert.Equal("critical", merged.Severity);
        Assert.True(merged.ExploitKnown);

        var affected = Assert.Single(merged.AffectedPackages);
        Assert.Empty(affected.VersionRanges);
        Assert.Contains(affected.Statuses, status => status.Status == "known_affected" && status.Provenance.Source == "vendor");

        var mergeProvenance = Assert.Single(merged.Provenance, p => p.Source == "merge");
        Assert.Equal("precedence", mergeProvenance.Kind);
        Assert.Equal(expectedTimestamp, mergeProvenance.RecordedAt);
        Assert.Contains("vendor", mergeProvenance.Value, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("kev", mergeProvenance.Value, StringComparison.OrdinalIgnoreCase);

        var inputDocumentIds = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
        var record = await writer.AppendAsync(merged.AdvisoryKey, nvd, merged, inputDocumentIds, Array.Empty<MergeFieldDecision>(), CancellationToken.None);

        Assert.Equal(expectedTimestamp, record.MergedAt);
        Assert.Equal(inputDocumentIds, record.InputDocumentIds);
        Assert.NotEqual(record.BeforeHash, record.AfterHash);

        var records = await store.GetRecentAsync(merged.AdvisoryKey, 5, CancellationToken.None);
        var persisted = Assert.Single(records);
        Assert.Equal(record.Id, persisted.Id);
        Assert.Equal(merged.AdvisoryKey, persisted.AdvisoryKey);
        Assert.True(persisted.AfterHash.Length > 0);
        Assert.True(persisted.BeforeHash.Length > 0);
    }

    [Fact]
    public async Task MergePipeline_IsDeterministicAcrossRuns()
    {
        await EnsureInitializedAsync();

        var merger = _merger!;
        var calculator = new CanonicalHashCalculator();

        var first = merger.Merge(new[] { CreateNvdBaseline(), CreateVendorOverride() });
        var second = merger.Merge(new[] { CreateNvdBaseline(), CreateVendorOverride() });

        var firstHash = calculator.ComputeHash(first);
        var secondHash = calculator.ComputeHash(second);

        Assert.Equal(firstHash, secondHash);
        Assert.Equal(first.AdvisoryKey, second.AdvisoryKey);
        Assert.Equal(first.Aliases.Length, second.Aliases.Length);
        Assert.True(first.Aliases.SequenceEqual(second.Aliases));
    }

    public async Task InitializeAsync()
    {
        _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero))
        {
            AutoAdvanceAmount = TimeSpan.Zero,
        };
        _merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), _timeProvider);
        _mergeEventStore = new MergeEventStore(_fixture.Database, NullLogger<MergeEventStore>.Instance);
        _mergeEventWriter = new MergeEventWriter(_mergeEventStore, new CanonicalHashCalculator(), _timeProvider, NullLogger<MergeEventWriter>.Instance);
        await DropMergeCollectionAsync();
    }

    public Task DisposeAsync() => Task.CompletedTask;

    private async Task EnsureInitializedAsync()
    {
        if (_mergeEventWriter is null)
        {
            await InitializeAsync();
        }
    }

    private async Task DropMergeCollectionAsync()
    {
        try
        {
            await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.MergeEvent);
        }
        catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase))
        {
            // Collection has not been created yet – safe to ignore.
        }
    }

    private static Advisory CreateNvdBaseline()
    {
        var provenance = new AdvisoryProvenance("nvd", "document", "https://nvd.nist.gov/vuln/detail/CVE-2025-1000", DateTimeOffset.Parse("2025-02-10T00:00:00Z"));
        return new Advisory(
            "CVE-2025-1000",
            "CVE-2025-1000",
            "Baseline description from NVD.",
            "en",
            DateTimeOffset.Parse("2025-02-05T00:00:00Z"),
            DateTimeOffset.Parse("2025-02-10T12:00:00Z"),
            "medium",
            exploitKnown: false,
            aliases: new[] { "CVE-2025-1000" },
            references: new[]
            {
                new AdvisoryReference("https://nvd.nist.gov/vuln/detail/CVE-2025-1000", "advisory", "nvd", "NVD reference", provenance),
            },
            affectedPackages: new[]
            {
                new AffectedPackage(
                    AffectedPackageTypes.Cpe,
                    "cpe:2.3:o:vendor:product:1.0:*:*:*:*:*:*:*",
                    "vendor-os",
                    new[]
                    {
                        new AffectedVersionRange(
                            rangeKind: "cpe",
                            introducedVersion: null,
                            fixedVersion: null,
                            lastAffectedVersion: null,
                            rangeExpression: "<=1.0",
                            provenance: provenance)
                    },
                    Array.Empty<AffectedPackageStatus>(),
                    new[] { provenance })
            },
            cvssMetrics: new[]
            {
                new CvssMetric("3.1", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 9.8, "critical", provenance)
            },
            provenance: new[] { provenance });
    }

    private static Advisory CreateVendorOverride()
    {
        var provenance = new AdvisoryProvenance("vendor", "psirt", "VSA-2025-1000", DateTimeOffset.Parse("2025-02-11T00:00:00Z"));
        return new Advisory(
            "CVE-2025-1000",
            "Vendor Security Advisory",
            "Critical impact on supported platforms.",
            "en",
            DateTimeOffset.Parse("2025-02-06T00:00:00Z"),
            DateTimeOffset.Parse("2025-02-11T06:00:00Z"),
            "critical",
            exploitKnown: false,
            aliases: new[] { "CVE-2025-1000", "VSA-2025-1000" },
            references: new[]
            {
                new AdvisoryReference("https://vendor.example/advisories/VSA-2025-1000", "advisory", "vendor", "Vendor advisory", provenance),
            },
            affectedPackages: new[]
            {
                new AffectedPackage(
                    AffectedPackageTypes.Cpe,
                    "cpe:2.3:o:vendor:product:1.0:*:*:*:*:*:*:*",
                    "vendor-os",
                    Array.Empty<AffectedVersionRange>(),
                    new[]
                    {
                        new AffectedPackageStatus("known_affected", provenance)
                    },
                    new[] { provenance })
            },
            cvssMetrics: new[]
            {
                new CvssMetric("3.1", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", 10.0, "critical", provenance)
            },
            provenance: new[] { provenance });
    }

    private static Advisory CreateKevSignal()
    {
        var provenance = new AdvisoryProvenance("kev", "catalog", "CVE-2025-1000", DateTimeOffset.Parse("2025-02-12T00:00:00Z"));
        return new Advisory(
            "CVE-2025-1000",
            "Known Exploited Vulnerability",
            null,
            null,
            published: null,
            modified: null,
            severity: null,
            exploitKnown: true,
            aliases: new[] { "KEV-CVE-2025-1000" },
            references: Array.Empty<AdvisoryReference>(),
            affectedPackages: Array.Empty<AffectedPackage>(),
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { provenance });
    }
}
