using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Kisa.Configuration;
using StellaOps.Feedser.Source.Kisa.Internal;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;
using StellaOps.Feedser.Testing;
using Xunit;
using System.Linq;

namespace StellaOps.Feedser.Source.Kisa.Tests;

[Collection("mongo-fixture")]
public sealed class KisaConnectorTests : IAsyncLifetime
{
    private static readonly Uri FeedUri = new("https://test.local/rss/securityInfo.do");
    private static readonly Uri DetailApiUri = new("https://test.local/rssDetailData.do?IDX=5868");
    private static readonly Uri DetailPageUri = new("https://test.local/detailDos.do?IDX=5868");

    private readonly MongoIntegrationFixture _fixture;
    private readonly CannedHttpMessageHandler _handler;

    public KisaConnectorTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
        _handler = new CannedHttpMessageHandler();
    }

    [Fact]
    public async Task FetchParseMap_ProducesCanonicalAdvisory()
    {
        await using var provider = await BuildServiceProviderAsync();
        SeedResponses();

        var connector = provider.GetRequiredService<KisaConnector>();
        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None);
        advisories.Should().HaveCount(1);

        var advisory = advisories[0];
        advisory.AdvisoryKey.Should().Be("5868");
        advisory.Language.Should().Be("ko");
        advisory.Aliases.Should().Contain("CVE-2025-29866");
        advisory.AffectedPackages.Should().Contain(package => package.Identifier.Contains("태그프리"));
        advisory.References.Should().Contain(reference => reference.Url == DetailPageUri.ToString());

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(KisaConnectorPlugin.SourceName, CancellationToken.None);
        state.Should().NotBeNull();
        state!.Cursor.Should().NotBeNull();
        state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
        pendingDocs!.AsBsonArray.Should().BeEmpty();
        state.Cursor.TryGetValue("pendingMappings", out var pendingMappings).Should().BeTrue();
        pendingMappings!.AsBsonArray.Should().BeEmpty();
    }

    [Fact]
    public async Task Telemetry_RecordsMetrics()
    {
        await using var provider = await BuildServiceProviderAsync();
        SeedResponses();

        using var metrics = new KisaMetricCollector();

        var connector = provider.GetRequiredService<KisaConnector>();
        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        Sum(metrics.Measurements, "kisa.feed.success").Should().Be(1);
        Sum(metrics.Measurements, "kisa.feed.items").Should().BeGreaterThan(0);
        Sum(metrics.Measurements, "kisa.detail.success").Should().Be(1);
        Sum(metrics.Measurements, "kisa.detail.failures").Should().Be(0);
        Sum(metrics.Measurements, "kisa.parse.success").Should().Be(1);
        Sum(metrics.Measurements, "kisa.parse.failures").Should().Be(0);
        Sum(metrics.Measurements, "kisa.map.success").Should().Be(1);
        Sum(metrics.Measurements, "kisa.map.failures").Should().Be(0);
    }

    private async Task<ServiceProvider> BuildServiceProviderAsync()
    {
        await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
        _handler.Clear();

        var services = new ServiceCollection();
        services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
        services.AddSingleton(_handler);

        services.AddMongoStorage(options =>
        {
            options.ConnectionString = _fixture.Runner.ConnectionString;
            options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
            options.CommandTimeout = TimeSpan.FromSeconds(5);
        });

        services.AddSourceCommon();
        services.AddKisaConnector(options =>
        {
            options.FeedUri = FeedUri;
            options.DetailApiUri = new Uri("https://test.local/rssDetailData.do");
            options.DetailPageUri = new Uri("https://test.local/detailDos.do");
            options.RequestDelay = TimeSpan.Zero;
            options.MaxAdvisoriesPerFetch = 10;
            options.MaxKnownAdvisories = 32;
        });

        services.Configure<HttpClientFactoryOptions>(KisaOptions.HttpClientName, builderOptions =>
        {
            builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
            {
                builder.PrimaryHandler = _handler;
            });
        });

        var provider = services.BuildServiceProvider();
        var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
        await bootstrapper.InitializeAsync(CancellationToken.None);
        return provider;
    }

    private void SeedResponses()
    {
        AddXmlResponse(FeedUri, ReadFixture("kisa-feed.xml"));
        AddJsonResponse(DetailApiUri, ReadFixture("kisa-detail.json"));
    }

    private void AddXmlResponse(Uri uri, string xml)
    {
        _handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
        {
            Content = new StringContent(xml, Encoding.UTF8, "application/rss+xml"),
        });
    }

    private void AddJsonResponse(Uri uri, string json)
    {
        _handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
        {
            Content = new StringContent(json, Encoding.UTF8, "application/json"),
        });
    }

    private static string ReadFixture(string fileName)
        => System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName));

    private static long Sum(IEnumerable<KisaMetricCollector.MetricMeasurement> measurements, string name)
        => measurements.Where(m => m.Name == name).Sum(m => m.Value);

    private sealed class KisaMetricCollector : IDisposable
    {
        private readonly MeterListener _listener;
        private readonly ConcurrentBag<MetricMeasurement> _measurements = new();

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

            _listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
            {
                var tagList = new List<KeyValuePair<string, object?>>(tags.Length);
                foreach (var tag in tags)
                {
                    tagList.Add(tag);
                }

                _measurements.Add(new MetricMeasurement(instrument.Name, measurement, tagList));
            });

            _listener.Start();
        }

        public IReadOnlyCollection<MetricMeasurement> Measurements => _measurements;

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

        internal sealed record MetricMeasurement(string Name, long Value, IReadOnlyList<KeyValuePair<string, object?>> Tags);
    }

    public Task InitializeAsync() => Task.CompletedTask;

    public Task DisposeAsync() => Task.CompletedTask;
}
