using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Scanner.Worker.Diagnostics;
using StellaOps.Scanner.Worker.Hosting;
using StellaOps.Scanner.Worker.Options;
using StellaOps.Scanner.Worker.Processing;
using Xunit;

namespace StellaOps.Scanner.Worker.Tests;

public sealed class WorkerBasicScanScenarioTests
{
    [Fact]
    public async Task DelayAsync_CompletesAfterTimeAdvance()
    {
        var scheduler = new ControlledDelayScheduler();
        var delayTask = scheduler.DelayAsync(TimeSpan.FromSeconds(5), CancellationToken.None);
        scheduler.AdvanceBy(TimeSpan.FromSeconds(5));
        await delayTask.WaitAsync(TimeSpan.FromSeconds(1));
    }

    [Fact]
    public async Task Worker_CompletesJob_RecordsTelemetry_And_Heartbeats()
    {
        var fakeTime = new FakeTimeProvider();
        fakeTime.SetUtcNow(DateTimeOffset.UtcNow);

        var options = new ScannerWorkerOptions
        {
            MaxConcurrentJobs = 1,
        };
        options.Telemetry.EnableTelemetry = false;
        options.Telemetry.EnableMetrics = true;

        var optionsMonitor = new StaticOptionsMonitor<ScannerWorkerOptions>(options);
        var testLoggerProvider = new TestLoggerProvider();
        var lease = new TestJobLease(fakeTime);
        var jobSource = new TestJobSource(lease);
        var scheduler = new ControlledDelayScheduler();
        var analyzer = new TestAnalyzerDispatcher(scheduler);

        using var listener = new WorkerMetricsListener();
        listener.Start();

        using var services = new ServiceCollection()
            .AddLogging(builder =>
            {
                builder.ClearProviders();
                builder.AddProvider(testLoggerProvider);
                builder.SetMinimumLevel(LogLevel.Debug);
            })
            .AddSingleton(fakeTime)
            .AddSingleton<TimeProvider>(fakeTime)
            .AddSingleton<IOptionsMonitor<ScannerWorkerOptions>>(optionsMonitor)
            .AddSingleton<ScannerWorkerMetrics>()
            .AddSingleton<ScanProgressReporter>()
            .AddSingleton<ScanJobProcessor>()
            .AddSingleton<LeaseHeartbeatService>()
            .AddSingleton<IDelayScheduler>(scheduler)
            .AddSingleton<IScanJobSource>(_ => jobSource)
            .AddSingleton<IScanAnalyzerDispatcher>(analyzer)
            .AddSingleton<IScanStageExecutor, AnalyzerStageExecutor>()
            .AddSingleton<ScannerWorkerHostedService>()
            .BuildServiceProvider();

        var worker = services.GetRequiredService<ScannerWorkerHostedService>();

        await worker.StartAsync(CancellationToken.None);

        await jobSource.LeaseIssued.Task.WaitAsync(TimeSpan.FromSeconds(5));

        scheduler.AdvanceBy(TimeSpan.FromSeconds(30));
        scheduler.AdvanceBy(TimeSpan.FromSeconds(30));

        try
        {
            await lease.Completed.Task.WaitAsync(TimeSpan.FromSeconds(30));
        }
        catch (TimeoutException ex)
        {
            var stageLogs = string.Join(Environment.NewLine, testLoggerProvider
                .GetEntriesForCategory(typeof(ScanProgressReporter).FullName!)
                .Select(entry => entry.ToFormattedString()));

            throw new TimeoutException($"Worker did not complete within timeout. Logs:{Environment.NewLine}{stageLogs}", ex);
        }

        await worker.StopAsync(CancellationToken.None);

        Assert.True(lease.Completed.Task.IsCompletedSuccessfully, "Job should complete successfully.");
        Assert.Single(analyzer.Executions);
        Assert.True(lease.RenewalCount >= 1, "Lease should have been renewed at least once.");

        var stageOrder = testLoggerProvider
            .GetEntriesForCategory(typeof(ScanProgressReporter).FullName!)
            .Where(entry => entry.EventId.Id == 1000)
            .Select(entry => entry.GetScopeProperty<string>("Stage"))
            .Where(stage => stage is not null)
            .Cast<string>()
            .ToArray();

        Assert.Equal(ScanStageNames.Ordered, stageOrder);

        var queueLatency = listener.Measurements.Where(m => m.InstrumentName == "scanner_worker_queue_latency_ms").ToArray();
        Assert.Single(queueLatency);
        Assert.True(queueLatency[0].Value > 0, "Queue latency should be positive.");

        var jobDuration = listener.Measurements.Where(m => m.InstrumentName == "scanner_worker_job_duration_ms").ToArray();
        Assert.Single(jobDuration);
        Assert.True(jobDuration[0].Value > 0, "Job duration should be positive.");

        var stageDurations = listener.Measurements.Where(m => m.InstrumentName == "scanner_worker_stage_duration_ms").ToArray();
        Assert.Contains(stageDurations, m => m.Tags.TryGetValue("stage", out var stage) && Equals(stage, ScanStageNames.ExecuteAnalyzers));
    }

    private sealed class TestJobSource : IScanJobSource
    {
        private readonly TestJobLease _lease;
        private int _delivered;

        public TestJobSource(TestJobLease lease)
        {
            _lease = lease;
        }

        public TaskCompletionSource LeaseIssued { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);

        public Task<IScanJobLease?> TryAcquireAsync(CancellationToken cancellationToken)
        {
            if (Interlocked.Exchange(ref _delivered, 1) == 0)
            {
                LeaseIssued.TrySetResult();
                return Task.FromResult<IScanJobLease?>(_lease);
            }

            return Task.FromResult<IScanJobLease?>(null);
        }
    }

    private sealed class TestJobLease : IScanJobLease
    {
        private readonly FakeTimeProvider _timeProvider;
        private readonly Dictionary<string, string> _metadata = new()
        {
            { "queue", "tests" },
            { "job.kind", "basic" },
        };

        public TestJobLease(FakeTimeProvider timeProvider)
        {
            _timeProvider = timeProvider;
            EnqueuedAtUtc = _timeProvider.GetUtcNow() - TimeSpan.FromSeconds(5);
            LeasedAtUtc = _timeProvider.GetUtcNow();
        }

        public string JobId { get; } = Guid.NewGuid().ToString("n");

        public string ScanId { get; } = $"scan-{Guid.NewGuid():n}";

        public int Attempt { get; } = 1;

        public DateTimeOffset EnqueuedAtUtc { get; }

        public DateTimeOffset LeasedAtUtc { get; }

        public TimeSpan LeaseDuration { get; } = TimeSpan.FromSeconds(90);

        public IReadOnlyDictionary<string, string> Metadata => _metadata;

        public TaskCompletionSource Completed { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);

        public int RenewalCount => _renewalCount;

        public ValueTask RenewAsync(CancellationToken cancellationToken)
        {
            Interlocked.Increment(ref _renewalCount);
            return ValueTask.CompletedTask;
        }

        public ValueTask CompleteAsync(CancellationToken cancellationToken)
        {
            Completed.TrySetResult();
            return ValueTask.CompletedTask;
        }

        public ValueTask AbandonAsync(string reason, CancellationToken cancellationToken)
        {
            Completed.TrySetException(new InvalidOperationException($"Abandoned: {reason}"));
            return ValueTask.CompletedTask;
        }

        public ValueTask PoisonAsync(string reason, CancellationToken cancellationToken)
        {
            Completed.TrySetException(new InvalidOperationException($"Poisoned: {reason}"));
            return ValueTask.CompletedTask;
        }

        public ValueTask DisposeAsync() => ValueTask.CompletedTask;

        private int _renewalCount;
    }

    private sealed class TestAnalyzerDispatcher : IScanAnalyzerDispatcher
    {
        private readonly IDelayScheduler _scheduler;

        public TestAnalyzerDispatcher(IDelayScheduler scheduler)
        {
            _scheduler = scheduler;
        }

        public List<string> Executions { get; } = new();

        public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
        {
            Executions.Add(context.JobId);
            await _scheduler.DelayAsync(TimeSpan.FromSeconds(45), cancellationToken);
        }
    }

    private sealed class ControlledDelayScheduler : IDelayScheduler
    {
        private readonly object _lock = new();
        private readonly SortedDictionary<double, List<ScheduledDelay>> _scheduled = new();
        private double _currentMilliseconds;

        public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
        {
            if (delay <= TimeSpan.Zero)
            {
                return Task.CompletedTask;
            }

            var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
            var scheduled = new ScheduledDelay(tcs, cancellationToken);
            lock (_lock)
            {
                var due = _currentMilliseconds + delay.TotalMilliseconds;
                if (!_scheduled.TryGetValue(due, out var list))
                {
                    list = new List<ScheduledDelay>();
                    _scheduled.Add(due, list);
                }

                list.Add(scheduled);
            }

            return scheduled.Task;
        }

        public void AdvanceBy(TimeSpan delta)
        {
            lock (_lock)
            {
                _currentMilliseconds += delta.TotalMilliseconds;
                var dueKeys = _scheduled.Keys.Where(key => key <= _currentMilliseconds).ToList();
                foreach (var due in dueKeys)
                {
                    foreach (var scheduled in _scheduled[due])
                    {
                        scheduled.Complete();
                    }

                    _scheduled.Remove(due);
                }
            }
        }

        private sealed class ScheduledDelay
        {
            private readonly TaskCompletionSource<object?> _tcs;
            private readonly CancellationTokenRegistration _registration;

            public ScheduledDelay(TaskCompletionSource<object?> tcs, CancellationToken cancellationToken)
            {
                _tcs = tcs;
                if (cancellationToken.CanBeCanceled)
                {
                    _registration = cancellationToken.Register(state =>
                    {
                        var source = (TaskCompletionSource<object?>)state!;
                        source.TrySetCanceled(cancellationToken);
                    }, tcs);
                }
            }

            public Task Task => _tcs.Task;

            public void Complete()
            {
                _registration.Dispose();
                _tcs.TrySetResult(null);
            }
        }
    }

    private sealed class StaticOptionsMonitor<TOptions> : IOptionsMonitor<TOptions>
        where TOptions : class
    {
        private readonly TOptions _value;

        public StaticOptionsMonitor(TOptions value)
        {
            _value = value;
        }

        public TOptions CurrentValue => _value;

        public TOptions Get(string? name) => _value;

        public IDisposable OnChange(Action<TOptions, string?> listener) => NullDisposable.Instance;

        private sealed class NullDisposable : IDisposable
        {
            public static readonly NullDisposable Instance = new();
            public void Dispose()
            {
            }
        }
    }

    private sealed class WorkerMetricsListener : IDisposable
    {
        private readonly MeterListener _listener;
        public ConcurrentBag<Measurement> Measurements { get; } = new();

        public WorkerMetricsListener()
        {
            _listener = new MeterListener
            {
                InstrumentPublished = (instrument, listener) =>
                {
                    if (instrument.Meter.Name == ScannerWorkerInstrumentation.MeterName)
                    {
                        listener.EnableMeasurementEvents(instrument);
                    }
                }
            };

            _listener.SetMeasurementEventCallback<double>((instrument, measurement, tags, state) =>
            {
                var tagDictionary = new Dictionary<string, object?>(tags.Length, StringComparer.Ordinal);
                foreach (var tag in tags)
                {
                    tagDictionary[tag.Key] = tag.Value;
                }

                Measurements.Add(new Measurement(instrument.Name, measurement, tagDictionary));
            });
        }

        public void Start() => _listener.Start();

        public void Dispose() => _listener.Dispose();
    }

    public sealed record Measurement(string InstrumentName, double Value, IReadOnlyDictionary<string, object?> Tags)
    {
        public object? this[string name] => Tags.TryGetValue(name, out var value) ? value : null;
    }

    private sealed class TestLoggerProvider : ILoggerProvider
    {
        private readonly ConcurrentQueue<TestLogEntry> _entries = new();

        public ILogger CreateLogger(string categoryName) => new TestLogger(categoryName, _entries);

        public void Dispose()
        {
        }

        public IEnumerable<TestLogEntry> GetEntriesForCategory(string categoryName)
            => _entries.Where(entry => entry.Category == categoryName);

        private sealed class TestLogger : ILogger
        {
            private readonly string _category;
            private readonly ConcurrentQueue<TestLogEntry> _entries;

            public TestLogger(string category, ConcurrentQueue<TestLogEntry> entries)
            {
                _category = category;
                _entries = entries;
            }

            public IDisposable? BeginScope<TState>(TState state) where TState : notnull => NullDisposable.Instance;

            public bool IsEnabled(LogLevel logLevel) => true;

            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
            {
                _entries.Enqueue(new TestLogEntry(_category, logLevel, eventId, state, exception));
            }
        }

        private sealed class NullDisposable : IDisposable
        {
            public static readonly NullDisposable Instance = new();
            public void Dispose()
            {
            }
        }
    }

    public sealed record TestLogEntry(string Category, LogLevel Level, EventId EventId, object? State, Exception? Exception)
    {
        public T? GetScopeProperty<T>(string name)
        {
            if (State is not IEnumerable<KeyValuePair<string, object?>> state)
            {
                return default;
            }

            foreach (var kvp in state)
            {
                if (string.Equals(kvp.Key, name, StringComparison.OrdinalIgnoreCase) && kvp.Value is T value)
                {
                    return value;
                }
            }

            return default;
        }

        public string ToFormattedString()
        {
            var properties = State is IEnumerable<KeyValuePair<string, object?>> kvps
                ? string.Join(", ", kvps.Select(kvp => $"{kvp.Key}={kvp.Value}"))
                : State?.ToString() ?? string.Empty;

            var exceptionPart = Exception is null ? string.Empty : $" Exception={Exception.GetType().Name}: {Exception.Message}";
            return $"[{Level}] {Category} ({EventId.Id}) {properties}{exceptionPart}";
        }
    }
}
