using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Http;
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.Acsc;
using StellaOps.Feedser.Source.Acsc.Configuration;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Testing;
using Xunit;

namespace StellaOps.Feedser.Source.Acsc.Tests.Acsc;

[Collection("mongo-fixture")]
public sealed class AcscConnectorFetchTests : IAsyncLifetime
{
    private static readonly Uri BaseEndpoint = new("https://origin.example/");
    private static readonly Uri RelayEndpoint = new("https://relay.example/");
    private static readonly Uri AlertsDirectUri = new(BaseEndpoint, "/feeds/alerts/rss");
    private static readonly Uri AlertsRelayUri = new(RelayEndpoint, "/feeds/alerts/rss");

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

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

    [Fact]
    public async Task FetchAsync_DirectSuccessAdvancesCursor()
    {
        await using var provider = await BuildProviderAsync(preferRelay: false);

        var connector = provider.GetRequiredService<AcscConnector>();
        SeedRssResponse(AlertsDirectUri, "direct", DateTimeOffset.Parse("2025-10-10T02:15:00Z"), DateTimeOffset.Parse("2025-10-11T05:30:00Z"));

        await connector.FetchAsync(provider, CancellationToken.None);
        _handler.AssertNoPendingResponses();

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.Equal("Direct", state!.Cursor.GetValue("preferredEndpoint").AsString);

        var feeds = state.Cursor.GetValue("feeds").AsBsonDocument;
        Assert.True(feeds.TryGetValue("alerts", out var published));
        Assert.Equal(DateTime.Parse("2025-10-11T05:30:00Z").ToUniversalTime(), published.ToUniversalTime());

        var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray;
        Assert.Single(pendingDocuments);

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var documentId = Guid.Parse(pendingDocuments[0]!.AsString);
        var document = await documentStore.FindAsync(documentId, CancellationToken.None);
        Assert.NotNull(document);
        Assert.Equal(DocumentStatuses.PendingParse, document!.Status);
        var directMetadata = document.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
        Assert.True(directMetadata.TryGetValue("acsc.fetch.mode", out var mode));
        Assert.Equal("direct", mode);
    }

    [Fact]
    public async Task FetchAsync_DirectFailureFallsBackToRelay()
    {
        await using var provider = await BuildProviderAsync(preferRelay: false);

        var connector = provider.GetRequiredService<AcscConnector>();
        _handler.AddException(HttpMethod.Get, AlertsDirectUri, new HttpRequestException("HTTP/2 reset"));
        SeedRssResponse(AlertsRelayUri, "relay", DateTimeOffset.Parse("2025-10-09T10:00:00Z"), DateTimeOffset.Parse("2025-10-11T00:00:00Z"));

        await connector.FetchAsync(provider, CancellationToken.None);
        _handler.AssertNoPendingResponses();

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.Equal("Relay", state!.Cursor.GetValue("preferredEndpoint").AsString);

        var feeds = state.Cursor.GetValue("feeds").AsBsonDocument;
        Assert.True(feeds.TryGetValue("alerts", out var published));
        Assert.Equal(DateTime.Parse("2025-10-11T00:00:00Z").ToUniversalTime(), published.ToUniversalTime());

        var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray;
        Assert.Single(pendingDocuments);

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var documentId = Guid.Parse(pendingDocuments[0]!.AsString);
        var document = await documentStore.FindAsync(documentId, CancellationToken.None);
        Assert.NotNull(document);
        Assert.Equal(DocumentStatuses.PendingParse, document!.Status);
        var metadata = document.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
        Assert.True(metadata.TryGetValue("acsc.fetch.mode", out var mode));
        Assert.Equal("relay", mode);

        Assert.Collection(_handler.Requests,
            request =>
            {
                Assert.Equal(HttpMethod.Get, request.Method);
                Assert.Equal(AlertsDirectUri, request.Uri);
            },
            request =>
            {
                Assert.Equal(HttpMethod.Get, request.Method);
                Assert.Equal(AlertsRelayUri, request.Uri);
            });
    }

    public async Task InitializeAsync() => await Task.CompletedTask;

    public Task DisposeAsync() => Task.CompletedTask;

    private async Task<ServiceProvider> BuildProviderAsync(bool preferRelay)
    {
        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.AddAcscConnector(options =>
        {
            options.BaseEndpoint = BaseEndpoint;
            options.RelayEndpoint = RelayEndpoint;
            options.EnableRelayFallback = true;
            options.PreferRelayByDefault = preferRelay;
            options.ForceRelay = false;
            options.RequestTimeout = TimeSpan.FromSeconds(10);
            options.Feeds.Clear();
            options.Feeds.Add(new AcscFeedOptions
            {
                Slug = "alerts",
                RelativePath = "/feeds/alerts/rss",
                Enabled = true,
            });
        });

        services.Configure<HttpClientFactoryOptions>(AcscOptions.HttpClientName, builderOptions =>
        {
            builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler);
        });
        services.Configure<SourceHttpClientOptions>(AcscOptions.HttpClientName, options =>
        {
            options.MaxAttempts = 1;
            options.BaseDelay = TimeSpan.Zero;
        });

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

    private void SeedRssResponse(Uri uri, string mode, DateTimeOffset first, DateTimeOffset second)
    {
        var payload = CreateRssPayload(first, second);
        _handler.AddResponse(HttpMethod.Get, uri, _ =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"),
            };

            response.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue($"\"{mode}-etag\"");
            response.Content.Headers.LastModified = second;
            return response;
        });
    }

    private static string CreateRssPayload(DateTimeOffset first, DateTimeOffset second)
    {
        return $$"""
                 <?xml version="1.0" encoding="UTF-8"?>
                 <rss version="2.0">
                   <channel>
                     <title>Alerts</title>
                     <link>https://origin.example/feeds/alerts</link>
                     <item>
                       <title>First</title>
                       <link>https://origin.example/alerts/first</link>
                       <pubDate>{{first.ToString("r", CultureInfo.InvariantCulture)}}</pubDate>
                     </item>
                     <item>
                       <title>Second</title>
                       <link>https://origin.example/alerts/second</link>
                       <pubDate>{{second.ToString("r", CultureInfo.InvariantCulture)}}</pubDate>
                     </item>
                   </channel>
                 </rss>
                 """;
    }
}
