using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Feedser.Source.Common.Http;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Testing;

namespace StellaOps.Feedser.Testing;

/// <summary>
/// Provides a reusable container for connector integration tests with canned HTTP responses and Mongo isolation.
/// </summary>
public sealed class ConnectorTestHarness : IAsyncDisposable
{
    private readonly MongoIntegrationFixture _fixture;
    private readonly DateTimeOffset _initialTime;
    private readonly string[] _httpClientNames;
    private ServiceProvider? _serviceProvider;

    public ConnectorTestHarness(MongoIntegrationFixture fixture, DateTimeOffset initialTime, params string[] httpClientNames)
    {
        _fixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
        _initialTime = initialTime;
        _httpClientNames = httpClientNames.Length == 0
            ? Array.Empty<string>()
            : httpClientNames.Distinct(StringComparer.Ordinal).ToArray();

        TimeProvider = CreateTimeProvider(initialTime);
        Handler = new CannedHttpMessageHandler();
    }

    public FakeTimeProvider TimeProvider { get; private set; }

    public CannedHttpMessageHandler Handler { get; }

    public ServiceProvider ServiceProvider => _serviceProvider ?? throw new InvalidOperationException("Call EnsureServiceProviderAsync first.");

    public async Task<ServiceProvider> EnsureServiceProviderAsync(Action<IServiceCollection> configureServices)
    {
        ArgumentNullException.ThrowIfNull(configureServices);

        if (_serviceProvider is not null)
        {
            return _serviceProvider;
        }

        var services = new ServiceCollection();
        services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
        services.AddSingleton<TimeProvider>(TimeProvider);
        services.AddSingleton(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();

        configureServices(services);

        foreach (var clientName in _httpClientNames)
        {
            services.Configure<HttpClientFactoryOptions>(clientName, options =>
            {
                options.HttpMessageHandlerBuilderActions.Add(builder =>
                {
                    builder.PrimaryHandler = Handler;
                });
            });
        }

        var provider = services.BuildServiceProvider();
        _serviceProvider = provider;

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

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

            _serviceProvider = null;
        }

        await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
        Handler.Clear();
        TimeProvider = CreateTimeProvider(_initialTime);
    }

    public async ValueTask DisposeAsync()
    {
        await ResetAsync();
    }

    private static FakeTimeProvider CreateTimeProvider(DateTimeOffset now)
        => new(now)
        {
            AutoAdvanceAmount = TimeSpan.Zero,
        };
}
