using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using StellaOps.Feedser.Models;

namespace StellaOps.Feedser.Models.Tests;

public sealed class CanonicalJsonSerializerTests
{
    [Fact]
    public void SerializesPropertiesInDeterministicOrder()
    {
        var advisory = new Advisory(
            advisoryKey: "TEST-321",
            title: "Ordering",
            summary: null,
            language: null,
            published: null,
            modified: null,
            severity: null,
            exploitKnown: false,
            aliases: new[] { "b", "a" },
            references: Array.Empty<AdvisoryReference>(),
            affectedPackages: Array.Empty<AffectedPackage>(),
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: Array.Empty<AdvisoryProvenance>());

        var json = CanonicalJsonSerializer.Serialize(advisory);
        using var document = JsonDocument.Parse(json);
        var propertyNames = document.RootElement.EnumerateObject().Select(p => p.Name).ToArray();

        var sorted = propertyNames.OrderBy(name => name, StringComparer.Ordinal).ToArray();
        Assert.Equal(sorted, propertyNames);
    }

    [Fact]
    public void SnapshotSerializerProducesStableOutput()
    {
        var advisory = new Advisory(
            advisoryKey: "TEST-999",
            title: "Snapshot",
            summary: "Example",
            language: "EN",
            published: DateTimeOffset.Parse("2024-06-01T00:00:00Z"),
            modified: DateTimeOffset.Parse("2024-06-01T01:00:00Z"),
            severity: "high",
            exploitKnown: false,
            aliases: new[] { "ALIAS-1" },
            references: new[]
            {
                new AdvisoryReference("https://example.com/a", "advisory", null, null, AdvisoryProvenance.Empty),
            },
            affectedPackages: Array.Empty<AffectedPackage>(),
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: Array.Empty<AdvisoryProvenance>());

        var snap1 = SnapshotSerializer.ToSnapshot(advisory);
        var snap2 = SnapshotSerializer.ToSnapshot(advisory);

        Assert.Equal(snap1, snap2);
        var normalized1 = snap1.Replace("\r\n", "\n");
        var normalized2 = snap2.Replace("\r\n", "\n");
        Assert.Equal(normalized1, normalized2);
    }

    [Fact]
    public void SerializesRangePrimitivesPayload()
    {
        var recordedAt = new DateTimeOffset(2025, 2, 1, 0, 0, 0, TimeSpan.Zero);
        var provenance = new AdvisoryProvenance("connector-x", "map", "segment-1", recordedAt);
        var primitives = new RangePrimitives(
            new SemVerPrimitive(
                Introduced: "2.0.0",
                IntroducedInclusive: true,
                Fixed: "2.3.4",
                FixedInclusive: false,
                LastAffected: "2.3.3",
                LastAffectedInclusive: true,
                ConstraintExpression: ">=2.0.0 <2.3.4"),
            new NevraPrimitive(
                Introduced: new NevraComponent("pkg", 0, "2.0.0", "1", "x86_64"),
                Fixed: null,
                LastAffected: new NevraComponent("pkg", 0, "2.3.3", "3", "x86_64")),
            new EvrPrimitive(
                Introduced: new EvrComponent(1, "2.0.0", "1"),
                Fixed: new EvrComponent(1, "2.3.4", null),
                LastAffected: null),
            new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["channel"] = "stable",
            });

        var range = new AffectedVersionRange(
            rangeKind: "semver",
            introducedVersion: "2.0.0",
            fixedVersion: "2.3.4",
            lastAffectedVersion: "2.3.3",
            rangeExpression: ">=2.0.0 <2.3.4",
            provenance,
            primitives);

        var package = new AffectedPackage(
            type: "semver",
            identifier: "pkg@2.x",
            platform: "linux",
            versionRanges: new[] { range },
            statuses: Array.Empty<AffectedPackageStatus>(),
            provenance: new[] { provenance });

        var advisory = new Advisory(
            advisoryKey: "TEST-PRIM",
            title: "Range primitive serialization",
            summary: null,
            language: null,
            published: recordedAt,
            modified: recordedAt,
            severity: null,
            exploitKnown: false,
            aliases: Array.Empty<string>(),
            references: Array.Empty<AdvisoryReference>(),
            affectedPackages: new[] { package },
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { provenance });

        var json = CanonicalJsonSerializer.Serialize(advisory);
        using var document = JsonDocument.Parse(json);
        var rangeElement = document.RootElement
            .GetProperty("affectedPackages")[0]
            .GetProperty("versionRanges")[0];

        Assert.True(rangeElement.TryGetProperty("primitives", out var primitivesElement));

        var semver = primitivesElement.GetProperty("semVer");
        Assert.Equal("2.0.0", semver.GetProperty("introduced").GetString());
        Assert.True(semver.GetProperty("introducedInclusive").GetBoolean());
        Assert.Equal("2.3.4", semver.GetProperty("fixed").GetString());
        Assert.False(semver.GetProperty("fixedInclusive").GetBoolean());
        Assert.Equal("2.3.3", semver.GetProperty("lastAffected").GetString());

        var nevra = primitivesElement.GetProperty("nevra");
        Assert.Equal("pkg", nevra.GetProperty("introduced").GetProperty("name").GetString());
        Assert.Equal(0, nevra.GetProperty("introduced").GetProperty("epoch").GetInt32());

        var evr = primitivesElement.GetProperty("evr");
        Assert.Equal(1, evr.GetProperty("introduced").GetProperty("epoch").GetInt32());
        Assert.Equal("2.3.4", evr.GetProperty("fixed").GetProperty("upstreamVersion").GetString());

        var extensions = primitivesElement.GetProperty("vendorExtensions");
        Assert.Equal("stable", extensions.GetProperty("channel").GetString());
    }
}
