using System.Collections.Generic;
using System.Globalization;
using System.IO;
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.Models;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Ics.Kaspersky;
using StellaOps.Feedser.Source.Ics.Kaspersky.Configuration;
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;

namespace StellaOps.Feedser.Source.Ics.Kaspersky.Tests;

[Collection("mongo-fixture")]
public sealed class KasperskyConnectorTests : IAsyncLifetime
{
    private readonly MongoIntegrationFixture _fixture;
    private readonly FakeTimeProvider _timeProvider;
    private readonly CannedHttpMessageHandler _handler;
    private ServiceProvider? _serviceProvider;

    public KasperskyConnectorTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
        _timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 10, 20, 0, 0, 0, TimeSpan.Zero));
        _handler = new CannedHttpMessageHandler();
    }

    [Fact]
    public async Task FetchParseMap_CreatesSnapshot()
    {
        var options = new KasperskyOptions
        {
            FeedUri = new Uri("https://ics-cert.example/feed-advisories/", UriKind.Absolute),
            WindowSize = TimeSpan.FromDays(30),
            WindowOverlap = TimeSpan.FromDays(1),
            MaxPagesPerFetch = 1,
            RequestDelay = TimeSpan.Zero,
        };

        await EnsureServiceProviderAsync(options);
        var provider = _serviceProvider!;

        _handler.Clear();

        _handler.AddTextResponse(options.FeedUri, ReadFixture("feed-page1.xml"), "application/rss+xml");
        var detailUri = new Uri("https://ics-cert.example/advisories/acme-controller-2024/");
        _handler.AddTextResponse(detailUri, ReadFixture("detail-acme-controller-2024.html"), "text/html");

        var connector = new KasperskyConnectorPlugin().Create(provider);

        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(5, CancellationToken.None);
        Assert.Single(advisories);
        var canonical = SnapshotSerializer.ToSnapshot(advisories.Single());
        var expected = ReadFixture("expected-advisory.json");
        var normalizedExpected = NormalizeLineEndings(expected);
        var normalizedActual = NormalizeLineEndings(canonical);
        if (!string.Equals(normalizedExpected, normalizedActual, StringComparison.Ordinal))
        {
            var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "Ics", "Kaspersky", "Fixtures", "expected-advisory.actual.json");
            Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
            File.WriteAllText(actualPath, canonical);
        }

        Assert.Equal(normalizedExpected, normalizedActual);

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var document = await documentStore.FindBySourceAndUriAsync(KasperskyConnectorPlugin.SourceName, detailUri.ToString(), CancellationToken.None);
        Assert.NotNull(document);
        Assert.Equal(DocumentStatuses.Mapped, document!.Status);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(KasperskyConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pending)
            ? pending.AsBsonArray
            : new BsonArray();
        Assert.Empty(pendingDocuments);
    }

    [Fact]
    public async Task FetchFailure_RecordsBackoff()
    {
        var options = new KasperskyOptions
        {
            FeedUri = new Uri("https://ics-cert.example/feed-advisories/", UriKind.Absolute),
            WindowSize = TimeSpan.FromDays(30),
            WindowOverlap = TimeSpan.FromDays(1),
            MaxPagesPerFetch = 1,
            RequestDelay = TimeSpan.Zero,
        };

        await EnsureServiceProviderAsync(options);
        var provider = _serviceProvider!;
        _handler.Clear();
        _handler.AddResponse(options.FeedUri, () => new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent("feed error", Encoding.UTF8, "text/plain"),
        });

        var connector = new KasperskyConnectorPlugin().Create(provider);

        await Assert.ThrowsAsync<HttpRequestException>(() => connector.FetchAsync(provider, CancellationToken.None));

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(KasperskyConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.Equal(1, state!.FailCount);
        Assert.NotNull(state.LastFailureReason);
        Assert.Contains("500", state.LastFailureReason, StringComparison.Ordinal);
        Assert.True(state.BackoffUntil.HasValue);
        Assert.True(state.BackoffUntil!.Value > _timeProvider.GetUtcNow());
    }

    [Fact]
    public async Task Fetch_NotModifiedMaintainsDocumentState()
    {
        var options = new KasperskyOptions
        {
            FeedUri = new Uri("https://ics-cert.example/feed-advisories/", UriKind.Absolute),
            WindowSize = TimeSpan.FromDays(30),
            WindowOverlap = TimeSpan.FromDays(1),
            MaxPagesPerFetch = 1,
            RequestDelay = TimeSpan.Zero,
        };

        await EnsureServiceProviderAsync(options);
        var provider = _serviceProvider!;
        _handler.Clear();

        var feedXml = ReadFixture("feed-page1.xml");
        var detailUri = new Uri("https://ics-cert.example/advisories/acme-controller-2024/");
        var detailHtml = ReadFixture("detail-acme-controller-2024.html");
        var etag = new EntityTagHeaderValue("\"ics-2024-acme\"");
        var lastModified = new DateTimeOffset(2024, 10, 15, 10, 0, 0, TimeSpan.Zero);

        _handler.AddTextResponse(options.FeedUri, feedXml, "application/rss+xml");
        _handler.AddResponse(detailUri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(detailHtml, Encoding.UTF8, "text/html"),
            };
            response.Headers.ETag = etag;
            response.Content.Headers.LastModified = lastModified;
            return response;
        });

        var connector = new KasperskyConnectorPlugin().Create(provider);

        await connector.FetchAsync(provider, CancellationToken.None);
        _timeProvider.Advance(TimeSpan.FromMinutes(1));
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var document = await documentStore.FindBySourceAndUriAsync(KasperskyConnectorPlugin.SourceName, detailUri.ToString(), CancellationToken.None);
        Assert.NotNull(document);
        Assert.Equal(DocumentStatuses.Mapped, document!.Status);

        _handler.AddTextResponse(options.FeedUri, feedXml, "application/rss+xml");
        _handler.AddResponse(detailUri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.NotModified);
            response.Headers.ETag = etag;
            return response;
        });

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

        document = await documentStore.FindBySourceAndUriAsync(KasperskyConnectorPlugin.SourceName, detailUri.ToString(), CancellationToken.None);
        Assert.NotNull(document);
        Assert.Equal(DocumentStatuses.Mapped, document!.Status);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(KasperskyConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs));
        Assert.Equal(0, pendingDocs.AsBsonArray.Count);
        Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings));
        Assert.Equal(0, pendingMappings.AsBsonArray.Count);
    }

    [Fact]
    public async Task Fetch_DuplicateContentSkipsRequeue()
    {
        var options = new KasperskyOptions
        {
            FeedUri = new Uri("https://ics-cert.example/feed-advisories/", UriKind.Absolute),
            WindowSize = TimeSpan.FromDays(30),
            WindowOverlap = TimeSpan.FromDays(1),
            MaxPagesPerFetch = 1,
            RequestDelay = TimeSpan.Zero,
        };

        await EnsureServiceProviderAsync(options);
        var provider = _serviceProvider!;
        _handler.Clear();

        var feedXml = ReadFixture("feed-page1.xml");
        var detailUri = new Uri("https://ics-cert.example/advisories/acme-controller-2024/");
        var detailHtml = ReadFixture("detail-acme-controller-2024.html");

        _handler.AddTextResponse(options.FeedUri, feedXml, "application/rss+xml");
        _handler.AddTextResponse(detailUri, detailHtml, "text/html");

        var connector = new KasperskyConnectorPlugin().Create(provider);

        await connector.FetchAsync(provider, CancellationToken.None);
        _timeProvider.Advance(TimeSpan.FromMinutes(1));
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var document = await documentStore.FindBySourceAndUriAsync(KasperskyConnectorPlugin.SourceName, detailUri.ToString(), CancellationToken.None);
        Assert.NotNull(document);
        Assert.Equal(DocumentStatuses.Mapped, document!.Status);

        _handler.AddTextResponse(options.FeedUri, feedXml, "application/rss+xml");
        _handler.AddTextResponse(detailUri, detailHtml, "text/html");

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

        document = await documentStore.FindBySourceAndUriAsync(KasperskyConnectorPlugin.SourceName, detailUri.ToString(), CancellationToken.None);
        Assert.NotNull(document);
        Assert.Equal(DocumentStatuses.Mapped, document!.Status);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(KasperskyConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        var pendingDocs = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
            ? pendingDocsValue.AsBsonArray
            : new BsonArray();
        Assert.Empty(pendingDocs);
        var pendingMappings = state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue)
            ? pendingMappingsValue.AsBsonArray
            : new BsonArray();
        Assert.Empty(pendingMappings);
    }

    private async Task EnsureServiceProviderAsync(KasperskyOptions template)
    {
        if (_serviceProvider is not null)
        {
            await ResetDatabaseAsync();
            return;
        }

        await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);

        var services = new ServiceCollection();
        services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
        services.AddSingleton<TimeProvider>(_timeProvider);
        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.AddKasperskyIcsConnector(opts =>
        {
            opts.FeedUri = template.FeedUri;
            opts.WindowSize = template.WindowSize;
            opts.WindowOverlap = template.WindowOverlap;
            opts.MaxPagesPerFetch = template.MaxPagesPerFetch;
            opts.RequestDelay = template.RequestDelay;
        });

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

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

    private Task ResetDatabaseAsync()
        => _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);

    private static string ReadFixture(string filename)
    {
        var baseDirectory = AppContext.BaseDirectory;
        var primary = Path.Combine(baseDirectory, "Source", "Ics", "Kaspersky", "Fixtures", filename);
        if (File.Exists(primary))
        {
            return File.ReadAllText(primary);
        }

        var fallback = Path.Combine(baseDirectory, "Kaspersky", "Fixtures", filename);
        return File.ReadAllText(fallback);
    }

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

    public Task InitializeAsync() => Task.CompletedTask;

    public async Task DisposeAsync()
    {
        if (_serviceProvider is IAsyncDisposable asyncDisposable)
        {
            await asyncDisposable.DisposeAsync();
        }
        else
        {
            _serviceProvider?.Dispose();
        }
    }
}
