using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Bson;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Ru.Nkcki;
using StellaOps.Feedser.Source.Ru.Nkcki.Configuration;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Testing;
using StellaOps.Feedser.Models;
using MongoDB.Driver;
using Xunit;

namespace StellaOps.Feedser.Source.Ru.Nkcki.Tests;

[Collection("mongo-fixture")]
public sealed class RuNkckiConnectorTests : IAsyncLifetime
{
    private static readonly Uri ListingUri = new("https://cert.gov.ru/materialy/uyazvimosti/");
    private static readonly Uri ListingPage2Uri = new("https://cert.gov.ru/materialy/uyazvimosti/?PAGEN_1=2");
    private static readonly Uri BulletinUri = new("https://cert.gov.ru/materialy/uyazvimosti/bulletin-sample.json.zip");
    private static readonly Uri LegacyBulletinUri = new("https://cert.gov.ru/materialy/uyazvimosti/bulletin-legacy.json.zip");

    private readonly MongoIntegrationFixture _fixture;
    private readonly FakeTimeProvider _timeProvider;
    private readonly CannedHttpMessageHandler _handler;

    public RuNkckiConnectorTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
        _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero));
        _handler = new CannedHttpMessageHandler();
    }

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

        var connector = provider.GetRequiredService<RuNkckiConnector>();
        await connector.FetchAsync(provider, CancellationToken.None);
        _timeProvider.Advance(TimeSpan.FromMinutes(1));
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
        Assert.Equal(2, advisories.Count);

        var snapshot = SnapshotSerializer.ToSnapshot(advisories);
        WriteOrAssertSnapshot(snapshot, "nkcki-advisories.snapshot.json");

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var document = await documentStore.FindBySourceAndUriAsync(RuNkckiConnectorPlugin.SourceName, "https://cert.gov.ru/materialy/uyazvimosti/2025-01001", CancellationToken.None);
        Assert.NotNull(document);
        Assert.Equal(DocumentStatuses.Mapped, document!.Status);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(RuNkckiConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.True(IsEmptyArray(state!.Cursor, "pendingDocuments"));
        Assert.True(IsEmptyArray(state.Cursor, "pendingMappings"));
    }

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

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

        _handler.Clear();
        for (var i = 0; i < 3; i++)
        {
            _handler.AddResponse(ListingUri, () => new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent("error", Encoding.UTF8, "text/plain"),
            });
        }

        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var before = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
        Assert.Equal(2, before.Count);

        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var after = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
        Assert.Equal(before.Select(advisory => advisory.AdvisoryKey).OrderBy(static key => key), after.Select(advisory => advisory.AdvisoryKey).OrderBy(static key => key));

        _handler.AssertNoPendingResponses();
    }

    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<TimeProvider>(_timeProvider);

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

        services.AddSourceCommon();
        services.AddRuNkckiConnector(options =>
        {
            options.BaseAddress = new Uri("https://cert.gov.ru/");
            options.ListingPath = "/materialy/uyazvimosti/";
            options.MaxBulletinsPerFetch = 2;
            options.MaxListingPagesPerFetch = 2;
            options.MaxVulnerabilitiesPerFetch = 50;
            options.ListingCacheDuration = TimeSpan.Zero;
            var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.Database.DatabaseNamespace.DatabaseName);
            Directory.CreateDirectory(cacheRoot);
            options.CacheDirectory = Path.Combine(cacheRoot, "ru-nkcki");
            options.RequestDelay = TimeSpan.Zero;
        });

        services.Configure<HttpClientFactoryOptions>(RuNkckiOptions.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 SeedListingAndBulletin()
    {
        var listingHtml = ReadFixture("listing.html");
        _handler.AddTextResponse(ListingUri, listingHtml, "text/html");

        var listingPage2Html = ReadFixture("listing-page2.html");
        _handler.AddTextResponse(ListingPage2Uri, listingPage2Html, "text/html");

        var bulletinBytes = ReadBulletinFixture("bulletin-sample.json.zip");
        _handler.AddResponse(BulletinUri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new ByteArrayContent(bulletinBytes),
            };
            response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
            response.Content.Headers.LastModified = new DateTimeOffset(2025, 9, 22, 0, 0, 0, TimeSpan.Zero);
            return response;
        });

        var legacyBytes = ReadBulletinFixture("bulletin-legacy.json.zip");
        _handler.AddResponse(LegacyBulletinUri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new ByteArrayContent(legacyBytes),
            };
            response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
            response.Content.Headers.LastModified = new DateTimeOffset(2024, 8, 2, 0, 0, 0, TimeSpan.Zero);
            return response;
        });
    }

    private static bool IsEmptyArray(BsonDocument document, string field)
    {
        if (!document.TryGetValue(field, out var value) || value is not BsonArray array)
        {
            return false;
        }

        return array.Count == 0;
    }

    private static string ReadFixture(string filename)
    {
        var path = Path.Combine("Fixtures", filename);
        var resolved = ResolveFixturePath(path);
        return File.ReadAllText(resolved);
    }

    private static byte[] ReadBulletinFixture(string filename)
    {
        var path = Path.Combine("Fixtures", filename);
        var resolved = ResolveFixturePath(path);
        return File.ReadAllBytes(resolved);
    }

    private static string ResolveFixturePath(string relativePath)
    {
        var projectRoot = GetProjectRoot();
        var projectPath = Path.Combine(projectRoot, relativePath);
        if (File.Exists(projectPath))
        {
            return projectPath;
        }

        var binaryPath = Path.Combine(AppContext.BaseDirectory, relativePath);
        if (File.Exists(binaryPath))
        {
            return Path.GetFullPath(binaryPath);
        }

        throw new FileNotFoundException($"Fixture not found: {relativePath}");
    }

    private static void WriteOrAssertSnapshot(string snapshot, string filename)
    {
        if (ShouldUpdateFixtures())
        {
            var path = GetWritableFixturePath(filename);
            Directory.CreateDirectory(Path.GetDirectoryName(path)!);
            File.WriteAllText(path, snapshot);
            return;
        }

        var expectedPath = ResolveFixturePath(Path.Combine("Fixtures", filename));
        if (!File.Exists(expectedPath))
        {
            throw new FileNotFoundException($"Expected snapshot missing: {expectedPath}. Set UPDATE_NKCKI_FIXTURES=1 to generate.");
        }

        var expected = File.ReadAllText(expectedPath);
        Assert.Equal(Normalize(expected), Normalize(snapshot));
    }

    private static string GetWritableFixturePath(string filename)
    {
        var projectRoot = GetProjectRoot();
        return Path.Combine(projectRoot, "Fixtures", filename);
    }

    private static bool ShouldUpdateFixtures()
    {
        var value = Environment.GetEnvironmentVariable("UPDATE_NKCKI_FIXTURES");
        return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
            || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
    }

    private static string Normalize(string text)
        => text.Replace("\r\n", "\n", StringComparison.Ordinal);

    private static string GetProjectRoot()
    {
        var current = AppContext.BaseDirectory;
        while (!string.IsNullOrEmpty(current))
        {
            var candidate = Path.Combine(current, "StellaOps.Feedser.Source.Ru.Nkcki.Tests.csproj");
            if (File.Exists(candidate))
            {
                return current;
            }

            current = Path.GetDirectoryName(current);
        }

        throw new InvalidOperationException("Unable to locate project root for Ru.Nkcki tests.");
    }

    public Task InitializeAsync() => Task.CompletedTask;

    public async Task DisposeAsync()
        => await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
}
