using System;
using System.Collections.Immutable;
using System.IO;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Vexer.Core;
using StellaOps.Vexer.Export;
using StellaOps.Vexer.Policy;
using StellaOps.Vexer.Storage.Mongo;
using Xunit;

namespace StellaOps.Vexer.Export.Tests;

public sealed class ExportEngineTests
{
    [Fact]
    public async Task ExportAsync_GeneratesAndCachesManifest()
    {
        var store = new InMemoryExportStore();
        var evaluator = new StaticPolicyEvaluator("baseline/v1");
        var dataSource = new InMemoryExportDataSource();
        var exporter = new DummyExporter(VexExportFormat.Json);
        var engine = new VexExportEngine(store, evaluator, dataSource, new[] { exporter }, NullLogger<VexExportEngine>.Instance);

        var query = VexQuery.Create(new[] { new VexQueryFilter("vulnId", "CVE-2025-0001") });
        var context = new VexExportRequestContext(query, VexExportFormat.Json, DateTimeOffset.UtcNow, ForceRefresh: false);

        var manifest = await engine.ExportAsync(context, CancellationToken.None);

        Assert.False(manifest.FromCache);
        Assert.Equal(VexExportFormat.Json, manifest.Format);
        Assert.Equal("baseline/v1", manifest.ConsensusRevision);
        Assert.Equal(1, manifest.ClaimCount);

        // second call hits cache
        var cached = await engine.ExportAsync(context, CancellationToken.None);
        Assert.True(cached.FromCache);
        Assert.Equal(manifest.ExportId, cached.ExportId);
    }

    [Fact]
    public async Task ExportAsync_ForceRefreshInvalidatesCacheEntry()
    {
        var store = new InMemoryExportStore();
        var evaluator = new StaticPolicyEvaluator("baseline/v1");
        var dataSource = new InMemoryExportDataSource();
        var exporter = new DummyExporter(VexExportFormat.Json);
        var cacheIndex = new RecordingCacheIndex();
        var engine = new VexExportEngine(store, evaluator, dataSource, new[] { exporter }, NullLogger<VexExportEngine>.Instance, cacheIndex);

        var query = VexQuery.Create(new[] { new VexQueryFilter("vulnId", "CVE-2025-0001") });
        var initialContext = new VexExportRequestContext(query, VexExportFormat.Json, DateTimeOffset.UtcNow);
        _ = await engine.ExportAsync(initialContext, CancellationToken.None);

        var refreshContext = new VexExportRequestContext(query, VexExportFormat.Json, DateTimeOffset.UtcNow.AddMinutes(1), ForceRefresh: true);
        var refreshed = await engine.ExportAsync(refreshContext, CancellationToken.None);

        Assert.False(refreshed.FromCache);
        var signature = VexQuerySignature.FromQuery(refreshContext.Query);
        Assert.True(cacheIndex.RemoveCalls.TryGetValue((signature.Value, refreshContext.Format), out var removed));
        Assert.True(removed);
    }

    [Fact]
    public async Task ExportAsync_WritesArtifactsToAllStores()
    {
        var store = new InMemoryExportStore();
        var evaluator = new StaticPolicyEvaluator("baseline/v1");
        var dataSource = new InMemoryExportDataSource();
        var exporter = new DummyExporter(VexExportFormat.Json);
        var recorder1 = new RecordingArtifactStore();
        var recorder2 = new RecordingArtifactStore();
        var engine = new VexExportEngine(
            store,
            evaluator,
            dataSource,
            new[] { exporter },
            NullLogger<VexExportEngine>.Instance,
            cacheIndex: null,
            artifactStores: new[] { recorder1, recorder2 });

        var query = VexQuery.Create(new[] { new VexQueryFilter("vulnId", "CVE-2025-0001") });
        var context = new VexExportRequestContext(query, VexExportFormat.Json, DateTimeOffset.UtcNow);

        await engine.ExportAsync(context, CancellationToken.None);

        Assert.Equal(1, recorder1.SaveCount);
        Assert.Equal(1, recorder2.SaveCount);
    }

    private sealed class InMemoryExportStore : IVexExportStore
    {
        private readonly Dictionary<string, VexExportManifest> _store = new(StringComparer.Ordinal);

        public ValueTask<VexExportManifest?> FindAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken)
        {
            var key = CreateKey(signature.Value, format);
            _store.TryGetValue(key, out var manifest);
            return ValueTask.FromResult<VexExportManifest?>(manifest);
        }

        public ValueTask SaveAsync(VexExportManifest manifest, CancellationToken cancellationToken)
        {
            var key = CreateKey(manifest.QuerySignature.Value, manifest.Format);
            _store[key] = manifest;
            return ValueTask.CompletedTask;
        }

        private static string CreateKey(string signature, VexExportFormat format)
            => FormattableString.Invariant($"{signature}|{format}");
    }

    private sealed class RecordingCacheIndex : IVexCacheIndex
    {
        public Dictionary<(string Signature, VexExportFormat Format), bool> RemoveCalls { get; } = new();

        public ValueTask<VexCacheEntry?> FindAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken)
            => ValueTask.FromResult<VexCacheEntry?>(null);

        public ValueTask SaveAsync(VexCacheEntry entry, CancellationToken cancellationToken)
            => ValueTask.CompletedTask;

        public ValueTask RemoveAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken)
        {
            RemoveCalls[(signature.Value, format)] = true;
            return ValueTask.CompletedTask;
        }
    }

    private sealed class RecordingArtifactStore : IVexArtifactStore
    {
        public int SaveCount { get; private set; }

        public ValueTask<VexStoredArtifact> SaveAsync(VexExportArtifact artifact, CancellationToken cancellationToken)
        {
            SaveCount++;
            return ValueTask.FromResult(new VexStoredArtifact(artifact.ContentAddress, "memory", artifact.Content.Length, artifact.Metadata));
        }

        public ValueTask DeleteAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
            => ValueTask.CompletedTask;

        public ValueTask<Stream?> OpenReadAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
            => ValueTask.FromResult<Stream?>(null);
    }

    private sealed class StaticPolicyEvaluator : IVexPolicyEvaluator
    {
        public StaticPolicyEvaluator(string version)
        {
            Version = version;
        }

        public string Version { get; }

        public VexPolicySnapshot Snapshot => VexPolicySnapshot.Default;

        public double GetProviderWeight(VexProvider provider) => 1.0;

        public bool IsClaimEligible(VexClaim claim, VexProvider provider, out string? rejectionReason)
        {
            rejectionReason = null;
            return true;
        }
    }

    private sealed class InMemoryExportDataSource : IVexExportDataSource
    {
        public ValueTask<VexExportDataSet> FetchAsync(VexQuery query, CancellationToken cancellationToken)
        {
            var claim = new VexClaim(
                "CVE-2025-0001",
                "vendor",
                new VexProduct("pkg:demo/app", "Demo"),
                VexClaimStatus.Affected,
                new VexClaimDocument(VexDocumentFormat.Csaf, "sha256:demo", new Uri("https://example.org/demo")),
                DateTimeOffset.UtcNow,
                DateTimeOffset.UtcNow);

            var consensus = new VexConsensus(
                "CVE-2025-0001",
                claim.Product,
                VexConsensusStatus.Affected,
                DateTimeOffset.UtcNow,
                new[] { new VexConsensusSource("vendor", VexClaimStatus.Affected, "sha256:demo", 1.0) },
                conflicts: null,
                policyVersion: "baseline/v1",
                summary: "affected");

            return ValueTask.FromResult(new VexExportDataSet(
                ImmutableArray.Create(consensus),
                ImmutableArray.Create(claim),
                ImmutableArray.Create("vendor")));
        }
    }

    private sealed class DummyExporter : IVexExporter
    {
        public DummyExporter(VexExportFormat format)
        {
            Format = format;
        }

        public VexExportFormat Format { get; }

        public VexContentAddress Digest(VexExportRequest request)
            => new("sha256", "deadbeef");

        public ValueTask<VexExportResult> SerializeAsync(VexExportRequest request, Stream output, CancellationToken cancellationToken)
        {
            var bytes = System.Text.Encoding.UTF8.GetBytes("{}");
            output.Write(bytes);
            return ValueTask.FromResult(new VexExportResult(new VexContentAddress("sha256", "deadbeef"), bytes.Length, ImmutableDictionary<string, string>.Empty));
        }
    }
}
