using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Mongo2Go;
using MongoDB.Driver;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Plugin.Standard.Bootstrap;
using StellaOps.Authority.Plugin.Standard.Storage;
using StellaOps.Authority.Storage.Mongo.Documents;
using StellaOps.Authority.Storage.Mongo.Stores;

namespace StellaOps.Authority.Plugin.Standard.Tests;

public class StandardPluginRegistrarTests
{
    [Fact]
    public async Task Register_ConfiguresIdentityProviderAndSeedsBootstrapUser()
    {
        using var runner = MongoDbRunner.Start(singleNodeReplSet: true);
        var client = new MongoClient(runner.ConnectionString);
        var database = client.GetDatabase("registrar-tests");

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["passwordPolicy:minimumLength"] = "8",
                ["passwordPolicy:requireDigit"] = "false",
                ["passwordPolicy:requireSymbol"] = "false",
                ["lockout:enabled"] = "false",
                ["bootstrapUser:username"] = "bootstrap",
                ["bootstrapUser:password"] = "Bootstrap1!",
                ["bootstrapUser:requirePasswordReset"] = "true"
            })
            .Build();

        var manifest = new AuthorityPluginManifest(
            "standard",
            "standard",
            true,
            typeof(StandardPluginRegistrar).Assembly.GetName().Name,
            typeof(StandardPluginRegistrar).Assembly.Location,
            new[] { AuthorityPluginCapabilities.Password, AuthorityPluginCapabilities.Bootstrap, AuthorityPluginCapabilities.ClientProvisioning },
            new Dictionary<string, string?>(),
            "standard.yaml");

        var pluginContext = new AuthorityPluginContext(manifest, configuration);
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddSingleton<IMongoDatabase>(database);
        services.AddSingleton<IAuthorityClientStore>(new InMemoryClientStore());

        var registrar = new StandardPluginRegistrar();
        registrar.Register(new AuthorityPluginRegistrationContext(services, pluginContext, configuration));

        var provider = services.BuildServiceProvider();
        var hostedServices = provider.GetServices<IHostedService>();
        foreach (var hosted in hostedServices)
        {
            if (hosted is StandardPluginBootstrapper bootstrapper)
            {
                await bootstrapper.StartAsync(CancellationToken.None);
            }
        }

        var plugin = provider.GetRequiredService<IIdentityProviderPlugin>();
        Assert.Equal("standard", plugin.Type);
        Assert.True(plugin.Capabilities.SupportsPassword);
        Assert.False(plugin.Capabilities.SupportsMfa);
        Assert.True(plugin.Capabilities.SupportsClientProvisioning);

        var verification = await plugin.Credentials.VerifyPasswordAsync("bootstrap", "Bootstrap1!", CancellationToken.None);
        Assert.True(verification.Succeeded);
        Assert.True(verification.User?.RequiresPasswordReset);
    }

    [Fact]
    public void Register_ForcesPasswordCapability_WhenManifestMissing()
    {
        using var runner = MongoDbRunner.Start(singleNodeReplSet: true);
        var client = new MongoClient(runner.ConnectionString);
        var database = client.GetDatabase("registrar-capabilities");

        var configuration = new ConfigurationBuilder().Build();
        var manifest = new AuthorityPluginManifest(
            "standard",
            "standard",
            true,
            typeof(StandardPluginRegistrar).Assembly.GetName().Name,
            typeof(StandardPluginRegistrar).Assembly.Location,
            Array.Empty<string>(),
            new Dictionary<string, string?>(),
            "standard.yaml");

        var pluginContext = new AuthorityPluginContext(manifest, configuration);
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddSingleton<IMongoDatabase>(database);
        services.AddSingleton<IAuthorityClientStore>(new InMemoryClientStore());

        var registrar = new StandardPluginRegistrar();
        registrar.Register(new AuthorityPluginRegistrationContext(services, pluginContext, configuration));

        using var provider = services.BuildServiceProvider();
        var plugin = provider.GetRequiredService<IIdentityProviderPlugin>();

        Assert.True(plugin.Capabilities.SupportsPassword);
    }

    [Fact]
    public void Register_Throws_WhenBootstrapConfigurationIncomplete()
    {
        using var runner = MongoDbRunner.Start(singleNodeReplSet: true);
        var client = new MongoClient(runner.ConnectionString);
        var database = client.GetDatabase("registrar-bootstrap-validation");

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["bootstrapUser:username"] = "bootstrap"
            })
            .Build();

        var manifest = new AuthorityPluginManifest(
            "standard",
            "standard",
            true,
            typeof(StandardPluginRegistrar).Assembly.GetName().Name,
            typeof(StandardPluginRegistrar).Assembly.Location,
            new[] { AuthorityPluginCapabilities.Password },
            new Dictionary<string, string?>(),
            "standard.yaml");

        var pluginContext = new AuthorityPluginContext(manifest, configuration);
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddSingleton<IMongoDatabase>(database);
        services.AddSingleton<IAuthorityClientStore>(new InMemoryClientStore());

        var registrar = new StandardPluginRegistrar();
        registrar.Register(new AuthorityPluginRegistrationContext(services, pluginContext, configuration));

        using var provider = services.BuildServiceProvider();
        Assert.Throws<InvalidOperationException>(() => provider.GetRequiredService<IIdentityProviderPlugin>());
    }
}

internal sealed class InMemoryClientStore : IAuthorityClientStore
{
    private readonly Dictionary<string, AuthorityClientDocument> clients = new(StringComparer.OrdinalIgnoreCase);

    public ValueTask<AuthorityClientDocument?> FindByClientIdAsync(string clientId, CancellationToken cancellationToken)
    {
        clients.TryGetValue(clientId, out var document);
        return ValueTask.FromResult(document);
    }

    public ValueTask UpsertAsync(AuthorityClientDocument document, CancellationToken cancellationToken)
    {
        clients[document.ClientId] = document;
        return ValueTask.CompletedTask;
    }

    public ValueTask<bool> DeleteByClientIdAsync(string clientId, CancellationToken cancellationToken)
        => ValueTask.FromResult(clients.Remove(clientId));
}
