using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Feedser.Models;
using Xunit;

namespace StellaOps.Feedser.Models.Tests;

public sealed class ProvenanceDiagnosticsTests
{
    [Fact]
    public void RecordMissing_AddsExpectedTagsAndDeduplicates()
    {
        ResetState();

        var measurements = new List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)>();
        using var listener = CreateListener(measurements);

        var baseline = DateTimeOffset.UtcNow;
        ProvenanceDiagnostics.RecordMissing("source-A", "range:pkg", baseline, new[] { ProvenanceFieldMasks.VersionRanges });
        ProvenanceDiagnostics.RecordMissing("source-A", "range:pkg", baseline.AddMinutes(5), new[] { ProvenanceFieldMasks.VersionRanges });
        ProvenanceDiagnostics.RecordMissing("source-A", "reference:https://example", baseline.AddMinutes(10), new[] { ProvenanceFieldMasks.References });

        listener.Dispose();

        Assert.Equal(2, measurements.Count);

        var first = measurements[0];
        Assert.Equal(1, first.Value);
        Assert.Equal("feedser.provenance.missing", first.Instrument);
        Assert.Equal("source-A", first.Tags["source"]);
        Assert.Equal("range:pkg", first.Tags["component"]);
        Assert.Equal("range", first.Tags["category"]);
        Assert.Equal("high", first.Tags["severity"]);
        Assert.Equal(ProvenanceFieldMasks.VersionRanges, first.Tags["fieldMask"]);

        var second = measurements[1];
        Assert.Equal("feedser.provenance.missing", second.Instrument);
        Assert.Equal("reference", second.Tags["category"]);
        Assert.Equal("low", second.Tags["severity"]);
        Assert.Equal(ProvenanceFieldMasks.References, second.Tags["fieldMask"]);
    }

    [Fact]
    public void ReportResumeWindow_ClearsTrackedEntries_WhenWindowBackfills()
    {
        ResetState();

        var timestamp = DateTimeOffset.UtcNow;
        ProvenanceDiagnostics.RecordMissing("source-B", "package:lib", timestamp);

        var (recorded, earliest, syncRoot) = GetInternalState();
        lock (syncRoot)
        {
            Assert.True(earliest.ContainsKey("source-B"));
            Assert.Contains(recorded, entry => entry.StartsWith("source-B|", StringComparison.OrdinalIgnoreCase));
        }

        ProvenanceDiagnostics.ReportResumeWindow("source-B", timestamp.AddMinutes(-5), NullLogger.Instance);

        lock (syncRoot)
        {
            Assert.False(earliest.ContainsKey("source-B"));
            Assert.DoesNotContain(recorded, entry => entry.StartsWith("source-B|", StringComparison.OrdinalIgnoreCase));
        }
    }

    [Fact]
    public void ReportResumeWindow_RetainsEntries_WhenWindowTooRecent()
    {
        ResetState();

        var timestamp = DateTimeOffset.UtcNow;
        ProvenanceDiagnostics.RecordMissing("source-C", "range:pkg", timestamp);

        ProvenanceDiagnostics.ReportResumeWindow("source-C", timestamp.AddMinutes(1), NullLogger.Instance);

        var (recorded, earliest, syncRoot) = GetInternalState();
        lock (syncRoot)
        {
            Assert.True(earliest.ContainsKey("source-C"));
            Assert.Contains(recorded, entry => entry.StartsWith("source-C|", StringComparison.OrdinalIgnoreCase));
        }
    }

    [Fact]
    public void RecordRangePrimitive_EmitsCoverageMetric()
    {
        var range = new AffectedVersionRange(
            rangeKind: "evr",
            introducedVersion: "1:1.1.1n-0+deb11u2",
            fixedVersion: null,
            lastAffectedVersion: null,
            rangeExpression: null,
            provenance: new AdvisoryProvenance("source-D", "range", "pkg", DateTimeOffset.UtcNow),
            primitives: new RangePrimitives(
                SemVer: null,
                Nevra: null,
                Evr: new EvrPrimitive(
                    new EvrComponent(1, "1.1.1n", "0+deb11u2"),
                    null,
                    null),
                VendorExtensions: new Dictionary<string, string> { ["debian.release"] = "bullseye" }));

        var measurements = new List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)>();
        using var listener = CreateListener(measurements, "feedser.range.primitives");

        ProvenanceDiagnostics.RecordRangePrimitive("source-D", range);

        listener.Dispose();

        var record = Assert.Single(measurements);
        Assert.Equal("feedser.range.primitives", record.Instrument);
        Assert.Equal(1, record.Value);
        Assert.Equal("source-D", record.Tags["source"]);
        Assert.Equal("evr", record.Tags["rangeKind"]);
        Assert.Equal("evr", record.Tags["primitiveKinds"]);
        Assert.Equal("true", record.Tags["hasVendorExtensions"]);
    }

    private static MeterListener CreateListener(
        List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)> measurements,
        params string[] instrumentNames)
    {
        var allowed = instrumentNames is { Length: > 0 } ? instrumentNames : new[] { "feedser.provenance.missing" };
        var allowedSet = new HashSet<string>(allowed, StringComparer.OrdinalIgnoreCase);

        var listener = new MeterListener
        {
            InstrumentPublished = (instrument, l) =>
            {
                if (instrument.Meter.Name == "StellaOps.Feedser.Models.Provenance" && allowedSet.Contains(instrument.Name))
                {
                    l.EnableMeasurementEvents(instrument);
                }
            }
        };

        listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
        {
            var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
            foreach (var tag in tags)
            {
                dict[tag.Key] = tag.Value;
            }

            measurements.Add((instrument.Name, measurement, dict));
        });

        listener.Start();
        return listener;
    }

    private static void ResetState()
    {
        var (_, _, syncRoot) = GetInternalState();
        lock (syncRoot)
        {
            var (recorded, earliest, _) = GetInternalState();
            recorded.Clear();
            earliest.Clear();
        }
    }

    private static (HashSet<string> Recorded, Dictionary<string, DateTimeOffset> Earliest, object SyncRoot) GetInternalState()
    {
        var type = typeof(ProvenanceDiagnostics);
        var recordedField = type.GetField("RecordedComponents", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("RecordedComponents not found");
        var earliestField = type.GetField("EarliestMissing", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("EarliestMissing not found");
        var syncField = type.GetField("SyncRoot", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("SyncRoot not found");

        var recorded = (HashSet<string>)recordedField.GetValue(null)!;
        var earliest = (Dictionary<string, DateTimeOffset>)earliestField.GetValue(null)!;
        var sync = syncField.GetValue(null)!;
        return (recorded, earliest, sync);
    }
}
