using System.Collections.Concurrent;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Feedser.Core;
using StellaOps.Feedser.Merge.Services;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.Aliases;
using StellaOps.Feedser.Storage.Mongo.MergeEvents;

namespace StellaOps.Feedser.Merge.Tests;

public sealed class AdvisoryMergeServiceTests
{
    [Fact]
    public async Task MergeAsync_AppliesCanonicalRulesAndPersistsDecisions()
    {
        var aliasStore = new FakeAliasStore();
        aliasStore.Register("GHSA-aaaa-bbbb-cccc",
            (AliasSchemes.Ghsa, "GHSA-aaaa-bbbb-cccc"),
            (AliasSchemes.Cve, "CVE-2025-4242"));
        aliasStore.Register("CVE-2025-4242",
            (AliasSchemes.Cve, "CVE-2025-4242"));
        aliasStore.Register("OSV-2025-xyz",
            (AliasSchemes.OsV, "OSV-2025-xyz"),
            (AliasSchemes.Cve, "CVE-2025-4242"));

        var advisoryStore = new FakeAdvisoryStore();
        advisoryStore.Seed(CreateGhsaAdvisory(), CreateNvdAdvisory(), CreateOsvAdvisory());

        var mergeEventStore = new InMemoryMergeEventStore();
        var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 4, 1, 0, 0, 0, TimeSpan.Zero));
        var writer = new MergeEventWriter(mergeEventStore, new CanonicalHashCalculator(), timeProvider, NullLogger<MergeEventWriter>.Instance);
        var precedenceMerger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
        var aliasResolver = new AliasGraphResolver(aliasStore);
        var canonicalMerger = new CanonicalMerger(timeProvider);
        var service = new AdvisoryMergeService(aliasResolver, advisoryStore, precedenceMerger, writer, canonicalMerger, NullLogger<AdvisoryMergeService>.Instance);

        var result = await service.MergeAsync("GHSA-aaaa-bbbb-cccc", CancellationToken.None);

        Assert.NotNull(result.Merged);
        Assert.Equal("OSV summary overrides", result.Merged!.Summary);

        var upserted = advisoryStore.LastUpserted;
        Assert.NotNull(upserted);
        Assert.Equal("CVE-2025-4242", upserted!.AdvisoryKey);
        Assert.Equal("OSV summary overrides", upserted.Summary);

        var mergeRecord = mergeEventStore.LastRecord;
        Assert.NotNull(mergeRecord);
        var summaryDecision = Assert.Single(mergeRecord!.FieldDecisions, decision => decision.Field == "summary");
        Assert.Equal("osv", summaryDecision.SelectedSource);
        Assert.Equal("freshness_override", summaryDecision.DecisionReason);
    }

    private static Advisory CreateGhsaAdvisory()
    {
        var recorded = DateTimeOffset.Parse("2025-03-01T00:00:00Z");
        var provenance = new AdvisoryProvenance("ghsa", "map", "GHSA-aaaa-bbbb-cccc", recorded, new[] { ProvenanceFieldMasks.Advisory });
        return new Advisory(
            "GHSA-aaaa-bbbb-cccc",
            "Container escape",
            "Initial GHSA summary.",
            "en",
            recorded,
            recorded,
            "medium",
            exploitKnown: false,
            aliases: new[] { "CVE-2025-4242", "GHSA-aaaa-bbbb-cccc" },
            references: Array.Empty<AdvisoryReference>(),
            affectedPackages: Array.Empty<AffectedPackage>(),
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { provenance });
    }

    private static Advisory CreateNvdAdvisory()
    {
        var recorded = DateTimeOffset.Parse("2025-03-02T00:00:00Z");
        var provenance = new AdvisoryProvenance("nvd", "map", "CVE-2025-4242", recorded, new[] { ProvenanceFieldMasks.Advisory });
        return new Advisory(
            "CVE-2025-4242",
            "CVE-2025-4242",
            "Baseline NVD summary.",
            "en",
            recorded,
            recorded,
            "high",
            exploitKnown: false,
            aliases: new[] { "CVE-2025-4242" },
            references: Array.Empty<AdvisoryReference>(),
            affectedPackages: Array.Empty<AffectedPackage>(),
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { provenance });
    }

    private static Advisory CreateOsvAdvisory()
    {
        var recorded = DateTimeOffset.Parse("2025-03-05T12:00:00Z");
        var provenance = new AdvisoryProvenance("osv", "map", "OSV-2025-xyz", recorded, new[] { ProvenanceFieldMasks.Advisory });
        return new Advisory(
            "OSV-2025-xyz",
            "Container escape",
            "OSV summary overrides",
            "en",
            recorded,
            recorded,
            "critical",
            exploitKnown: false,
            aliases: new[] { "OSV-2025-xyz", "CVE-2025-4242" },
            references: Array.Empty<AdvisoryReference>(),
            affectedPackages: Array.Empty<AffectedPackage>(),
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { provenance });
    }

    private sealed class FakeAliasStore : IAliasStore
    {
        private readonly ConcurrentDictionary<string, List<AliasRecord>> _records = new(StringComparer.OrdinalIgnoreCase);

        public void Register(string advisoryKey, params (string Scheme, string Value)[] aliases)
        {
            var list = new List<AliasRecord>();
            foreach (var (scheme, value) in aliases)
            {
                list.Add(new AliasRecord(advisoryKey, scheme, value, DateTimeOffset.UtcNow));
            }

            _records[advisoryKey] = list;
        }

        public Task<AliasUpsertResult> ReplaceAsync(string advisoryKey, IEnumerable<AliasEntry> aliases, DateTimeOffset updatedAt, CancellationToken cancellationToken)
            => Task.FromResult(new AliasUpsertResult(advisoryKey, Array.Empty<AliasCollision>()));

        public Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken)
        {
            var matches = _records.Values
                .SelectMany(static records => records)
                .Where(record => string.Equals(record.Scheme, scheme, StringComparison.OrdinalIgnoreCase) && string.Equals(record.Value, value, StringComparison.OrdinalIgnoreCase))
                .ToList();

            return Task.FromResult<IReadOnlyList<AliasRecord>>(matches);
        }

        public Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
        {
            if (_records.TryGetValue(advisoryKey, out var records))
            {
                return Task.FromResult<IReadOnlyList<AliasRecord>>(records);
            }

            return Task.FromResult<IReadOnlyList<AliasRecord>>(Array.Empty<AliasRecord>());
        }
    }

    private sealed class FakeAdvisoryStore : IAdvisoryStore
    {
        private readonly ConcurrentDictionary<string, Advisory> _advisories = new(StringComparer.OrdinalIgnoreCase);

        public Advisory? LastUpserted { get; private set; }

        public void Seed(params Advisory[] advisories)
        {
            foreach (var advisory in advisories)
            {
                _advisories[advisory.AdvisoryKey] = advisory;
            }
        }

        public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
        {
            _advisories.TryGetValue(advisoryKey, out var advisory);
            return Task.FromResult(advisory);
        }

        public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
            => Task.FromResult<IReadOnlyList<Advisory>>(Array.Empty<Advisory>());

        public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
        {
            _advisories[advisory.AdvisoryKey] = advisory;
            LastUpserted = advisory;
            return Task.CompletedTask;
        }

        public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken) => AsyncEnumerable.Empty<Advisory>();
    }

    private sealed class InMemoryMergeEventStore : IMergeEventStore
    {
        public MergeEventRecord? LastRecord { get; private set; }

        public Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken)
        {
            LastRecord = record;
            return Task.CompletedTask;
        }

        public Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
            => Task.FromResult<IReadOnlyList<MergeEventRecord>>(Array.Empty<MergeEventRecord>());
    }
}
