using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using StellaOps.Feedser.Core;
using StellaOps.Feedser.Exporter.Json;
using StellaOps.Feedser.Models;
using Xunit;

namespace StellaOps.Feedser.Source.Nvd.Tests.Nvd;

public sealed class NvdMergeExportParityTests
{
    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);

    [Fact]
    public async Task CanonicalMerge_PreservesCreditsAndReferences_ExporterMaintainsParity()
    {
        var ghsa = LoadFixture("credit-parity.ghsa.json");
        var osv = LoadFixture("credit-parity.osv.json");
        var nvd = LoadFixture("credit-parity.nvd.json");

        var merger = new CanonicalMerger();
        var result = merger.Merge("CVE-2025-5555", ghsa, nvd, osv);
        var merged = result.Advisory;

        Assert.NotNull(merged);
        var creditKeys = merged!.Credits
            .Select(static credit => $"{credit.Role}|{credit.DisplayName}|{string.Join("|", credit.Contacts.OrderBy(static c => c, StringComparer.Ordinal))}")
            .ToHashSet(StringComparer.Ordinal);

        Assert.Equal(2, creditKeys.Count);
        Assert.Contains("reporter|Alice Researcher|mailto:alice.researcher@example.com", creditKeys);
        Assert.Contains("remediation_developer|Bob Maintainer|https://github.com/acme/bob-maintainer", creditKeys);

        var referenceUrls = merged.References.Select(static reference => reference.Url).ToHashSet(StringComparer.OrdinalIgnoreCase);
        Assert.Equal(5, referenceUrls.Count);
        Assert.Contains($"https://github.com/advisories/GHSA-credit-parity", referenceUrls);
        Assert.Contains("https://example.com/ghsa/patch", referenceUrls);
        Assert.Contains($"https://osv.dev/vulnerability/GHSA-credit-parity", referenceUrls);
        Assert.Contains($"https://services.nvd.nist.gov/vuln/detail/CVE-2025-5555", referenceUrls);
        Assert.Contains("https://example.com/nvd/reference", referenceUrls);

        using var tempDirectory = new TempDirectory();
        var options = new JsonExportOptions { OutputRoot = tempDirectory.Path };
        var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
        var exportResult = await builder.WriteAsync(new[] { merged }, new DateTimeOffset(2025, 10, 11, 0, 0, 0, TimeSpan.Zero));

        Assert.Single(exportResult.Files);
        var exportFile = exportResult.Files[0];
        var exportPath = Path.Combine(exportResult.ExportDirectory, exportFile.RelativePath.Replace('/', Path.DirectorySeparatorChar));
        Assert.True(File.Exists(exportPath));

        var exported = JsonSerializer.Deserialize<Advisory>(await File.ReadAllTextAsync(exportPath), SerializerOptions);
        Assert.NotNull(exported);

        var exportedCredits = exported!.Credits
            .Select(static credit => $"{credit.Role}|{credit.DisplayName}|{string.Join("|", credit.Contacts.OrderBy(static c => c, StringComparer.Ordinal))}")
            .ToHashSet(StringComparer.Ordinal);
        Assert.Equal(creditKeys, exportedCredits);

        var exportedReferences = exported.References.Select(static reference => reference.Url).ToHashSet(StringComparer.OrdinalIgnoreCase);
        Assert.Equal(referenceUrls, exportedReferences);
    }

    private static Advisory LoadFixture(string fileName)
    {
        var path = Path.Combine(AppContext.BaseDirectory, "Nvd", "Fixtures", fileName);
        return JsonSerializer.Deserialize<Advisory>(File.ReadAllText(path), SerializerOptions)
            ?? throw new InvalidOperationException($"Failed to deserialize fixture '{fileName}'.");
    }

    private sealed class TempDirectory : IDisposable
    {
        public TempDirectory()
        {
            Path = Directory.CreateTempSubdirectory("nvd-merge-export").FullName;
        }

        public string Path { get; }

        public void Dispose()
        {
            try
            {
                if (Directory.Exists(Path))
                {
                    Directory.Delete(Path, recursive: true);
                }
            }
            catch
            {
                // best effort cleanup
            }
        }
    }
}
