using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Feedser.Merge.Options;
using StellaOps.Feedser.Merge.Services;
using StellaOps.Feedser.Models;

namespace StellaOps.Feedser.Merge.Tests;

public sealed class AdvisoryPrecedenceMergerTests
{
    [Fact]
    public void Merge_PrefersVendorPrecedenceOverNvd()
    {
        var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
        var merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
        using var metrics = new MetricCollector("StellaOps.Feedser.Merge");

        var (redHat, nvd) = CreateVendorAndRegistryAdvisories();
        var expectedMergeTimestamp = timeProvider.GetUtcNow();

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

        Assert.Equal("CVE-2025-1000", merged.AdvisoryKey);
        Assert.Equal("Red Hat Security Advisory", merged.Title);
        Assert.Equal("Vendor-confirmed impact on RHEL 9.", merged.Summary);
        Assert.Equal("high", merged.Severity);
        Assert.Equal(redHat.Published, merged.Published);
        Assert.Equal(redHat.Modified, merged.Modified);
        Assert.Contains("RHSA-2025:0001", merged.Aliases);
        Assert.Contains("CVE-2025-1000", merged.Aliases);

        var package = Assert.Single(merged.AffectedPackages);
        Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*", package.Identifier);
        Assert.Empty(package.VersionRanges); // NVD range suppressed by vendor precedence
        Assert.Contains(package.Statuses, status => status.Status == "known_affected");
        Assert.Contains(package.Provenance, provenance => provenance.Source == "redhat");
        Assert.Contains(package.Provenance, provenance => provenance.Source == "nvd");

        Assert.Contains(merged.CvssMetrics, metric => metric.Provenance.Source == "redhat");
        Assert.Contains(merged.CvssMetrics, metric => metric.Provenance.Source == "nvd");

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

        var rangeMeasurement = Assert.Single(metrics.Measurements, measurement => measurement.Name == "feedser.merge.range_overrides");
        Assert.Equal(1, rangeMeasurement.Value);
        Assert.Contains(rangeMeasurement.Tags, tag => string.Equals(tag.Key, "suppressed_source", StringComparison.Ordinal) && tag.Value?.ToString()?.Contains("nvd", StringComparison.OrdinalIgnoreCase) == true);

        var severityConflict = Assert.Single(metrics.Measurements, measurement => measurement.Name == "feedser.merge.conflicts");
        Assert.Equal(1, severityConflict.Value);
        Assert.Contains(severityConflict.Tags, tag => string.Equals(tag.Key, "type", StringComparison.Ordinal) && string.Equals(tag.Value?.ToString(), "severity", StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public void Merge_KevOnlyTogglesExploitKnown()
    {
        var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 2, 1, 0, 0, 0, TimeSpan.Zero));
        var merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);

        var nvdProvenance = new AdvisoryProvenance("nvd", "document", "https://nvd", timeProvider.GetUtcNow());
        var baseAdvisory = new Advisory(
            "CVE-2025-2000",
                "CVE-2025-2000",
                "Base registry summary",
                "en",
                new DateTimeOffset(2025, 1, 5, 0, 0, 0, TimeSpan.Zero),
                new DateTimeOffset(2025, 1, 6, 0, 0, 0, TimeSpan.Zero),
                "medium",
                exploitKnown: false,
                aliases: new[] { "CVE-2025-2000" },
                references: Array.Empty<AdvisoryReference>(),
                affectedPackages: new[]
                {
                    new AffectedPackage(
                        AffectedPackageTypes.Cpe,
                        "cpe:2.3:a:example:product:2.0:*:*:*:*:*:*:*",
                        null,
                        new[]
                        {
                            new AffectedVersionRange(
                                "semver",
                                "2.0.0",
                                "2.0.5",
                                null,
                                "<2.0.5",
                                new AdvisoryProvenance("nvd", "cpe_match", "product", timeProvider.GetUtcNow()))
                        },
                        Array.Empty<AffectedPackageStatus>(),
                        new[] { nvdProvenance })
                },
                cvssMetrics: Array.Empty<CvssMetric>(),
                provenance: new[] { nvdProvenance });

        var kevProvenance = new AdvisoryProvenance("kev", "catalog", "CVE-2025-2000", timeProvider.GetUtcNow());
        var kevAdvisory = new Advisory(
            "CVE-2025-2000",
            "Known Exploited Vulnerability",
            summary: null,
            language: null,
            published: null,
            modified: null,
            severity: null,
            exploitKnown: true,
            aliases: new[] { "KEV-CVE-2025-2000" },
            references: Array.Empty<AdvisoryReference>(),
            affectedPackages: Array.Empty<AffectedPackage>(),
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { kevProvenance });

        var merged = merger.Merge(new[] { baseAdvisory, kevAdvisory });

        Assert.True(merged.ExploitKnown);
        Assert.Equal("medium", merged.Severity); // KEV must not override severity
        Assert.Equal("Base registry summary", merged.Summary);
        Assert.Contains("CVE-2025-2000", merged.Aliases);
        Assert.Contains("KEV-CVE-2025-2000", merged.Aliases);
        Assert.Contains(merged.Provenance, provenance => provenance.Source == "kev");
        Assert.Contains(merged.Provenance, provenance => provenance.Source == "merge");
    }

    [Fact]
    public void Merge_RespectsConfiguredPrecedenceOverrides()
    {
        var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero));
        var options = new AdvisoryPrecedenceOptions
        {
            Ranks = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
            {
                ["nvd"] = 0,
                ["redhat"] = 5,
            }
        };

        var logger = new TestLogger<AdvisoryPrecedenceMerger>();
        using var metrics = new MetricCollector("StellaOps.Feedser.Merge");

        var merger = new AdvisoryPrecedenceMerger(
            new AffectedPackagePrecedenceResolver(),
            options,
            timeProvider,
            logger);

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

        Assert.Equal("CVE-2025-1000", merged.AdvisoryKey);
        Assert.Equal("CVE-2025-1000", merged.Title); // NVD preferred
        Assert.Equal("NVD summary", merged.Summary);
        Assert.Equal("medium", merged.Severity);

        var package = Assert.Single(merged.AffectedPackages);
        Assert.NotEmpty(package.VersionRanges); // Vendor range no longer overrides
        Assert.Contains(package.Provenance, provenance => provenance.Source == "nvd");
        Assert.Contains(package.Provenance, provenance => provenance.Source == "redhat");

        var overrideMeasurement = Assert.Single(metrics.Measurements, m => m.Name == "feedser.merge.overrides");
        Assert.Equal(1, overrideMeasurement.Value);
        Assert.Contains(overrideMeasurement.Tags, tag => tag.Key == "primary_source" && string.Equals(tag.Value?.ToString(), "nvd", StringComparison.OrdinalIgnoreCase));
        Assert.Contains(overrideMeasurement.Tags, tag => tag.Key == "suppressed_source" && tag.Value?.ToString()?.Contains("redhat", StringComparison.OrdinalIgnoreCase) == true);

        Assert.DoesNotContain(metrics.Measurements, measurement => measurement.Name == "feedser.merge.range_overrides");

        var conflictMeasurement = Assert.Single(metrics.Measurements, measurement => measurement.Name == "feedser.merge.conflicts");
        Assert.Equal(1, conflictMeasurement.Value);
        Assert.Contains(conflictMeasurement.Tags, tag => tag.Key == "type" && string.Equals(tag.Value?.ToString(), "severity", StringComparison.OrdinalIgnoreCase));
        Assert.Contains(conflictMeasurement.Tags, tag => tag.Key == "reason" && string.Equals(tag.Value?.ToString(), "mismatch", StringComparison.OrdinalIgnoreCase));

        var logEntry = Assert.Single(logger.Entries, entry => entry.EventId.Name == "AdvisoryOverride");
        Assert.Equal(LogLevel.Information, logEntry.Level);
        Assert.NotNull(logEntry.StructuredState);
        Assert.Contains(logEntry.StructuredState!, kvp =>
            (string.Equals(kvp.Key, "Override", StringComparison.Ordinal) ||
             string.Equals(kvp.Key, "@Override", StringComparison.Ordinal)) &&
            kvp.Value is not null);
    }

    private static (Advisory Vendor, Advisory Registry) CreateVendorAndRegistryAdvisories()
    {
        var redHatPublished = new DateTimeOffset(2025, 1, 10, 0, 0, 0, TimeSpan.Zero);
        var redHatModified = redHatPublished.AddDays(1);
        var redHatProvenance = new AdvisoryProvenance("redhat", "advisory", "RHSA-2025:0001", redHatModified);
        var redHatPackage = new AffectedPackage(
            AffectedPackageTypes.Cpe,
            "cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*",
            "rhel-9",
            Array.Empty<AffectedVersionRange>(),
            new[] { new AffectedPackageStatus("known_affected", redHatProvenance) },
            new[] { redHatProvenance });
        var redHat = new Advisory(
            "CVE-2025-1000",
            "Red Hat Security Advisory",
            "Vendor-confirmed impact on RHEL 9.",
            "en",
            redHatPublished,
            redHatModified,
            "high",
            exploitKnown: false,
            aliases: new[] { "CVE-2025-1000", "RHSA-2025:0001" },
            references: new[]
            {
                new AdvisoryReference(
                    "https://access.redhat.com/errata/RHSA-2025:0001",
                    "advisory",
                    "redhat",
                    "Red Hat errata",
                    redHatProvenance)
            },
            affectedPackages: new[] { redHatPackage },
            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",
                    new AdvisoryProvenance("redhat", "cvss", "RHSA-2025:0001", redHatModified))
            },
            provenance: new[] { redHatProvenance });

        var nvdPublished = new DateTimeOffset(2025, 1, 5, 0, 0, 0, TimeSpan.Zero);
        var nvdModified = nvdPublished.AddDays(2);
        var nvdProvenance = new AdvisoryProvenance("nvd", "document", "https://nvd.nist.gov/vuln/detail/CVE-2025-1000", nvdModified);
        var nvdPackage = new AffectedPackage(
            AffectedPackageTypes.Cpe,
            "cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*",
            "rhel-9",
            new[]
            {
                new AffectedVersionRange(
                    "cpe",
                    null,
                    null,
                    null,
                    "<=9.0",
                    new AdvisoryProvenance("nvd", "cpe_match", "RHEL", nvdModified))
            },
            Array.Empty<AffectedPackageStatus>(),
            new[] { nvdProvenance });
        var nvd = new Advisory(
            "CVE-2025-1000",
            "CVE-2025-1000",
            "NVD summary",
            "en",
            nvdPublished,
            nvdModified,
            "medium",
            exploitKnown: false,
            aliases: new[] { "CVE-2025-1000" },
            references: new[]
            {
                new AdvisoryReference(
                    "https://nvd.nist.gov/vuln/detail/CVE-2025-1000",
                    "advisory",
                    "nvd",
                    "NVD advisory",
                    nvdProvenance)
            },
            affectedPackages: new[] { nvdPackage },
            cvssMetrics: new[]
            {
                new CvssMetric(
                    "3.1",
                    "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N",
                    6.8,
                    "medium",
                    new AdvisoryProvenance("nvd", "cvss", "CVE-2025-1000", nvdModified))
            },
            provenance: new[] { nvdProvenance });

        return (redHat, nvd);
    }
}
