using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Signing;
using StellaOps.Configuration;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using Xunit;

namespace StellaOps.Authority.Tests.Signing;

public sealed class AuthoritySigningKeyManagerTests
{
    [Fact]
    public void Rotate_ReplacesActiveKeyAndRetiresPreviousKey()
    {
        var tempDir = Directory.CreateTempSubdirectory("authority-signing-tests").FullName;
        var key1Relative = "key-1.pem";
        var key2Relative = "key-2.pem";

        try
        {
            CreateEcPrivateKey(Path.Combine(tempDir, key1Relative));

            var options = new StellaOpsAuthorityOptions
            {
                Issuer = new Uri("https://authority.test"),
                Storage = { ConnectionString = "mongodb://localhost/test" },
                Signing =
                {
                    Enabled = true,
                    ActiveKeyId = "key-1",
                    KeyPath = key1Relative,
                    Algorithm = SignatureAlgorithms.Es256,
                    KeySource = "file",
                    Provider = "default"
                }
            };

            using var provider = BuildProvider(tempDir, options);
            var manager = provider.GetRequiredService<AuthoritySigningKeyManager>();
            var jwksService = provider.GetRequiredService<AuthorityJwksService>();

            var initial = jwksService.Build();
            var initialKey = Assert.Single(initial.Keys);
            Assert.Equal("key-1", initialKey.Kid);
            Assert.Equal(AuthoritySigningKeyStatus.Active, initialKey.Status);

            CreateEcPrivateKey(Path.Combine(tempDir, key2Relative));

            var result = manager.Rotate(new SigningRotationRequest
            {
                KeyId = "key-2",
                Location = key2Relative
            });

            Assert.Equal("key-2", result.ActiveKeyId);
            Assert.Equal("key-1", result.PreviousKeyId);
            Assert.Contains("key-1", result.RetiredKeyIds);

            Assert.Equal("key-2", options.Signing.ActiveKeyId);
            var additional = Assert.Single(options.Signing.AdditionalKeys);
            Assert.Equal("key-1", additional.KeyId);
            Assert.Equal(key1Relative, additional.Path);
            Assert.Equal("file", additional.Source);

            var afterRotation = jwksService.Build();
            Assert.Equal(2, afterRotation.Keys.Count);

            var activeEntry = Assert.Single(afterRotation.Keys, key => key.Status == AuthoritySigningKeyStatus.Active);
            Assert.Equal("key-2", activeEntry.Kid);

            var retiredEntry = Assert.Single(afterRotation.Keys, key => key.Status == AuthoritySigningKeyStatus.Retired);
            Assert.Equal("key-1", retiredEntry.Kid);
        }
        finally
        {
            try
            {
                Directory.Delete(tempDir, recursive: true);
            }
            catch
            {
                // ignore cleanup failures
            }
        }
    }

    private static ServiceProvider BuildProvider(string basePath, StellaOpsAuthorityOptions options)
    {
        var services = new ServiceCollection();
        services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug));
        services.AddSingleton<IHostEnvironment>(new TestHostEnvironment(basePath));
        services.AddSingleton(options);
        services.AddSingleton<IOptions<StellaOpsAuthorityOptions>>(Options.Create(options));
        services.AddStellaOpsCrypto();
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthoritySigningKeySource, FileAuthoritySigningKeySource>());
        services.AddSingleton<AuthoritySigningKeyManager>();
        services.AddSingleton<AuthorityJwksService>();

        return services.BuildServiceProvider();
    }

    private static void CreateEcPrivateKey(string path)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
        var pem = ecdsa.ExportECPrivateKeyPem();
        File.WriteAllText(path, pem);
    }

    private sealed class TestHostEnvironment : IHostEnvironment
    {
        public TestHostEnvironment(string contentRoot)
        {
            ContentRootPath = contentRoot;
            ContentRootFileProvider = new PhysicalFileProvider(contentRoot);
            EnvironmentName = Environments.Development;
            ApplicationName = "StellaOps.Authority.Tests";
        }

        public string EnvironmentName { get; set; }

        public string ApplicationName { get; set; }

        public string ContentRootPath { get; set; }

        public IFileProvider ContentRootFileProvider { get; set; }
    }
}
