using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Reflection;
using MongoDB.Bson;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Osv;
using StellaOps.Feedser.Source.Osv.Internal;
using StellaOps.Feedser.Normalization.Identifiers;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;
using Xunit;

namespace StellaOps.Feedser.Source.Osv.Tests;

public sealed class OsvMapperTests
{
    [Fact]
    public void Map_NormalizesAliasesReferencesAndRanges()
    {
        var published = DateTimeOffset.UtcNow.AddDays(-2);
        var modified = DateTimeOffset.UtcNow.AddDays(-1);

        using var databaseSpecificJson = JsonDocument.Parse("{}");
        using var ecosystemSpecificJson = JsonDocument.Parse("{}");

        var dto = new OsvVulnerabilityDto
        {
            Id = "OSV-2025-TEST",
            Summary = "Test summary",
            Details = "Longer details for the advisory.",
            Published = published,
            Modified = modified,
            Aliases = new[] { "CVE-2025-0001", "CVE-2025-0001", "GHSA-xxxx" },
            Related = new[] { "CVE-2025-0002" },
            References = new[]
            {
                new OsvReferenceDto { Url = "https://example.com/advisory", Type = "ADVISORY" },
                new OsvReferenceDto { Url = "https://example.com/advisory", Type = "ADVISORY" },
                new OsvReferenceDto { Url = "https://example.com/patch", Type = "PATCH" },
            },
            DatabaseSpecific = databaseSpecificJson.RootElement,
            Severity = new[]
            {
                new OsvSeverityDto { Type = "CVSS_V3", Score = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" },
            },
            Affected = new[]
            {
                new OsvAffectedPackageDto
                {
                    Package = new OsvPackageDto
                    {
                        Ecosystem = "PyPI",
                        Name = "example",
                        Purl = "pkg:pypi/example",
                    },
                    Ranges = new[]
                    {
                        new OsvRangeDto
                        {
                            Type = "SEMVER",
                            Events = new[]
                            {
                                new OsvEventDto { Introduced = "0" },
                                new OsvEventDto { Fixed = "1.0.1" },
                            }
                        }
                    },
                    EcosystemSpecific = ecosystemSpecificJson.RootElement,
                }
            }
        };

        var document = new DocumentRecord(
            Guid.NewGuid(),
            OsvConnectorPlugin.SourceName,
            "https://osv.dev/vulnerability/OSV-2025-TEST",
            DateTimeOffset.UtcNow,
            "sha256",
            DocumentStatuses.PendingParse,
            "application/json",
            null,
            new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["osv.ecosystem"] = "PyPI",
            },
            null,
            modified,
            null,
            null);

        var payload = BsonDocument.Parse(JsonSerializer.Serialize(dto, new JsonSerializerOptions(JsonSerializerDefaults.Web)
        {
            DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
        }));
        var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, DateTimeOffset.UtcNow);

        var advisory = OsvMapper.Map(dto, document, dtoRecord, "PyPI");

        Assert.Equal(dto.Id, advisory.AdvisoryKey);
        Assert.Contains("CVE-2025-0002", advisory.Aliases);
        Assert.Equal(4, advisory.Aliases.Length);

        Assert.Equal(2, advisory.References.Length);
        Assert.Equal("https://example.com/advisory", advisory.References[0].Url);
        Assert.Equal("https://example.com/patch", advisory.References[1].Url);

        Assert.Single(advisory.AffectedPackages);
        var affected = advisory.AffectedPackages[0];
        Assert.Equal(AffectedPackageTypes.SemVer, affected.Type);
        Assert.Single(affected.VersionRanges);
        Assert.Equal("0", affected.VersionRanges[0].IntroducedVersion);
        Assert.Equal("1.0.1", affected.VersionRanges[0].FixedVersion);
        var semver = affected.VersionRanges[0].Primitives?.SemVer;
        Assert.NotNull(semver);
        Assert.Equal("0", semver!.Introduced);
        Assert.True(semver.IntroducedInclusive);
        Assert.Equal("1.0.1", semver.Fixed);
        Assert.False(semver.FixedInclusive);

        Assert.Single(advisory.CvssMetrics);
        Assert.Equal("3.1", advisory.CvssMetrics[0].Version);
    }

    [Fact]
    public void Map_AssignsSeverityFallbackWhenCvssVectorUnsupported()
    {
        using var databaseSpecificJson = JsonDocument.Parse("""
        {
            "severity": "MODERATE",
            "cwe_ids": ["CWE-290"]
        }
        """);

        var dto = new OsvVulnerabilityDto
        {
            Id = "OSV-CVSS4",
            Summary = "Severity-only advisory",
            Details = "OSV entry that lacks a parsable CVSS vector.",
            Published = DateTimeOffset.UtcNow.AddDays(-10),
            Modified = DateTimeOffset.UtcNow.AddDays(-5),
            DatabaseSpecific = databaseSpecificJson.RootElement,
            Severity = new[]
            {
                new OsvSeverityDto
                {
                    Type = "CVSS_V4",
                    Score = "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N"
                }
            }
        };

        var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, "PyPI");
        var advisory = OsvMapper.Map(dto, document, dtoRecord, "PyPI");

        Assert.True(advisory.CvssMetrics.IsEmpty);
        Assert.Equal("medium", advisory.Severity);
        Assert.Equal("osv:severity/medium", advisory.CanonicalMetricId);

        var weakness = Assert.Single(advisory.Cwes);
        var provenance = Assert.Single(weakness.Provenance);
        Assert.Equal("database_specific.cwe_ids", provenance.DecisionReason);
    }

    [Theory]
    [InlineData("Go", "github.com/example/project", "pkg:golang/github.com/example/project")]
    [InlineData("PyPI", "social_auth_app_django", "pkg:pypi/social-auth-app-django")]
    [InlineData("npm", "@Scope/Package", "pkg:npm/%40scope/package")]
    [InlineData("Maven", "org.example:library", "pkg:maven/org.example/library")]
    [InlineData("crates", "serde", "pkg:cargo/serde")]
    public void Map_InfersCanonicalPackageUrlWhenPurlMissing(string ecosystem, string packageName, string expectedIdentifier)
    {
        var dto = new OsvVulnerabilityDto
        {
            Id = $"OSV-{ecosystem}-PURL",
            Summary = "Test advisory",
            Details = "Details",
            Published = DateTimeOffset.UtcNow.AddDays(-1),
            Modified = DateTimeOffset.UtcNow,
            Affected = new[]
            {
                new OsvAffectedPackageDto
                {
                    Package = new OsvPackageDto
                    {
                        Ecosystem = ecosystem,
                        Name = packageName,
                        Purl = null,
                    },
                    Ranges = null,
                }
            }
        };

        if (string.Equals(ecosystem, "npm", StringComparison.OrdinalIgnoreCase))
        {
            Assert.True(IdentifierNormalizer.TryNormalizePackageUrl("pkg:npm/%40scope/package", out var canonical));
            Assert.Equal(expectedIdentifier, canonical);
        }

        var method = typeof(OsvMapper).GetMethod("DetermineIdentifier", BindingFlags.NonPublic | BindingFlags.Static);
        Assert.NotNull(method);
        var directIdentifier = method!.Invoke(null, new object?[] { dto.Affected![0].Package!, ecosystem }) as string;
        Assert.Equal(expectedIdentifier, directIdentifier);

        var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, ecosystem);
        var advisory = OsvMapper.Map(dto, document, dtoRecord, ecosystem);

        var affected = Assert.Single(advisory.AffectedPackages);
        Assert.Equal(expectedIdentifier, affected.Identifier);
    }

    private static (DocumentRecord Document, DtoRecord DtoRecord) CreateDocumentAndDtoRecord(OsvVulnerabilityDto dto, string ecosystem)
    {
        var recordedAt = DateTimeOffset.UtcNow;
        var document = new DocumentRecord(
            Guid.NewGuid(),
            OsvConnectorPlugin.SourceName,
            $"https://osv.dev/vulnerability/{dto.Id}",
            recordedAt,
            "sha256",
            DocumentStatuses.PendingParse,
            "application/json",
            null,
            new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["osv.ecosystem"] = ecosystem,
            },
            null,
            dto.Modified,
            null,
            null);

        var payload = new BsonDocument("id", dto.Id);
        var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, recordedAt);
        return (document, dtoRecord);
    }
}
