using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Feedser.Exporter.Json;
using StellaOps.Feedser.Models;

namespace StellaOps.Feedser.Exporter.Json.Tests;

public sealed class JsonExportSnapshotBuilderTests : IDisposable
{
    private readonly string _root;

    public JsonExportSnapshotBuilderTests()
    {
        _root = Directory.CreateTempSubdirectory("feedser-json-export-tests").FullName;
    }

    [Fact]
    public async Task WritesDeterministicTree()
    {
        var options = new JsonExportOptions { OutputRoot = _root };
        var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
        var exportedAt = DateTimeOffset.Parse("2024-07-15T12:00:00Z", CultureInfo.InvariantCulture);

        var advisories = new[]
        {
            CreateAdvisory(
                advisoryKey: "CVE-2024-9999",
                aliases: new[] { "GHSA-zzzz-yyyy-xxxx", "CVE-2024-9999" },
                title: "Deterministic Snapshot",
                severity: "critical"),
            CreateAdvisory(
                advisoryKey: "VENDOR-2024-42",
                aliases: new[] { "ALIAS-1", "ALIAS-2" },
                title: "Vendor Advisory",
                severity: "medium"),
        };

        var result = await builder.WriteAsync(advisories, exportedAt, cancellationToken: CancellationToken.None);

        Assert.Equal(advisories.Length, result.AdvisoryCount);
        Assert.Equal(exportedAt, result.ExportedAt);

        var expectedFiles = result.FilePaths.OrderBy(x => x, StringComparer.Ordinal).ToArray();
        Assert.Contains("nvd/2024/CVE-2024-9999.json", expectedFiles);
        Assert.Contains("misc/VENDOR-2024-42.json", expectedFiles);

        var cvePath = ResolvePath(result.ExportDirectory, "nvd/2024/CVE-2024-9999.json");
        Assert.True(File.Exists(cvePath));
        var actualJson = await File.ReadAllTextAsync(cvePath, CancellationToken.None);
        Assert.Equal(SnapshotSerializer.ToSnapshot(advisories[0]), actualJson);
    }

    [Fact]
    public async Task ProducesIdenticalBytesAcrossRuns()
    {
        var options = new JsonExportOptions { OutputRoot = _root };
        var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
        var exportedAt = DateTimeOffset.Parse("2024-05-01T00:00:00Z", CultureInfo.InvariantCulture);
        var advisories = new[]
        {
            CreateAdvisory("CVE-2024-1000", new[] { "CVE-2024-1000", "GHSA-aaaa-bbbb-cccc" }, "Snapshot Stable", "high"),
        };

        var first = await builder.WriteAsync(advisories, exportedAt, exportName: "20240501T000000Z", CancellationToken.None);
        var firstDigest = ComputeDigest(first);

        var second = await builder.WriteAsync(advisories, exportedAt, exportName: "20240501T000000Z", CancellationToken.None);
        var secondDigest = ComputeDigest(second);

        Assert.Equal(Convert.ToHexString(firstDigest), Convert.ToHexString(secondDigest));
    }

    [Fact]
    public async Task WriteAsync_NormalizesInputOrdering()
    {
        var options = new JsonExportOptions { OutputRoot = _root };
        var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
        var exportedAt = DateTimeOffset.Parse("2024-06-01T00:00:00Z", CultureInfo.InvariantCulture);

        var advisoryA = CreateAdvisory("CVE-2024-1000", new[] { "CVE-2024-1000" }, "Alpha", "high");
        var advisoryB = CreateAdvisory("VENDOR-0001", new[] { "VENDOR-0001" }, "Vendor Advisory", "medium");

        var result = await builder.WriteAsync(new[] { advisoryB, advisoryA }, exportedAt, cancellationToken: CancellationToken.None);

        var expectedOrder = result.FilePaths.OrderBy(path => path, StringComparer.Ordinal).ToArray();
        Assert.Equal(expectedOrder, result.FilePaths.ToArray());
    }

    [Fact]
    public async Task WriteAsync_EnumeratesStreamOnlyOnce()
    {
        var options = new JsonExportOptions { OutputRoot = _root };
        var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
        var exportedAt = DateTimeOffset.Parse("2024-08-01T00:00:00Z", CultureInfo.InvariantCulture);

        var advisories = new[]
        {
            CreateAdvisory("CVE-2024-2000", new[] { "CVE-2024-2000" }, "Streaming One", "medium"),
            CreateAdvisory("CVE-2024-2001", new[] { "CVE-2024-2001" }, "Streaming Two", "low"),
        };

        var sequence = new SingleEnumerationAsyncSequence(advisories);
        var result = await builder.WriteAsync(sequence, exportedAt, cancellationToken: CancellationToken.None);

        Assert.Equal(advisories.Length, result.AdvisoryCount);
    }

    private static Advisory CreateAdvisory(string advisoryKey, string[] aliases, string title, string severity)
    {
        return new Advisory(
            advisoryKey: advisoryKey,
            title: title,
            summary: null,
            language: "EN",
            published: DateTimeOffset.Parse("2024-01-01T00:00:00Z", CultureInfo.InvariantCulture),
            modified: DateTimeOffset.Parse("2024-01-02T00:00:00Z", CultureInfo.InvariantCulture),
            severity: severity,
            exploitKnown: false,
            aliases: aliases,
            references: new[]
            {
                new AdvisoryReference("https://example.com/advisory", "advisory", null, null, AdvisoryProvenance.Empty),
            },
            affectedPackages: new[]
            {
                new AffectedPackage(
                    AffectedPackageTypes.SemVer,
                    "sample/package",
                    platform: null,
                    versionRanges: Array.Empty<AffectedVersionRange>(),
                    statuses: Array.Empty<AffectedPackageStatus>(),
                    provenance: Array.Empty<AdvisoryProvenance>()),
            },
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[]
            {
                new AdvisoryProvenance("feedser", "normalized", "canonical", DateTimeOffset.Parse("2024-01-02T00:00:00Z", CultureInfo.InvariantCulture)),
            });
    }

    private static byte[] ComputeDigest(JsonExportResult result)
    {
        using var sha256 = SHA256.Create();
        foreach (var relative in result.FilePaths.OrderBy(x => x, StringComparer.Ordinal))
        {
            var fullPath = ResolvePath(result.ExportDirectory, relative);
            var bytes = File.ReadAllBytes(fullPath);
            sha256.TransformBlock(bytes, 0, bytes.Length, null, 0);
        }

        sha256.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
        return sha256.Hash ?? Array.Empty<byte>();
    }

    private static string ResolvePath(string root, string relative)
    {
        var segments = relative.Split('/', StringSplitOptions.RemoveEmptyEntries);
        return Path.Combine(new[] { root }.Concat(segments).ToArray());
    }

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

    private sealed class SingleEnumerationAsyncSequence : IAsyncEnumerable<Advisory>
    {
        private readonly IReadOnlyList<Advisory> _advisories;
        private int _enumerated;

        public SingleEnumerationAsyncSequence(IReadOnlyList<Advisory> advisories)
        {
            _advisories = advisories ?? throw new ArgumentNullException(nameof(advisories));
        }

        public IAsyncEnumerator<Advisory> GetAsyncEnumerator(CancellationToken cancellationToken = default)
        {
            if (Interlocked.Exchange(ref _enumerated, 1) == 1)
            {
                throw new InvalidOperationException("Sequence was enumerated more than once.");
            }

            return Enumerate(cancellationToken);

            async IAsyncEnumerator<Advisory> Enumerate([EnumeratorCancellation] CancellationToken ct)
            {
                foreach (var advisory in _advisories)
                {
                    ct.ThrowIfCancellationRequested();
                    yield return advisory;
                    await Task.Yield();
                }
            }
        }
    }
}
