using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using StellaOps.Feedser.Source.Common.Http;

namespace StellaOps.Feedser.Source.Common.Tests;

public sealed class SourceHttpClientBuilderTests
{
    [Fact]
    public void AddSourceHttpClient_ConfiguresVersionAndHandler()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());

        bool configureInvoked = false;
        bool? observedEnableMultiple = null;
        SocketsHttpHandler? capturedHandler = null;

        services.AddSourceHttpClient("source.test", (_, options) =>
        {
            options.AllowedHosts.Add("example.test");
            options.RequestVersion = HttpVersion.Version20;
            options.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
            options.EnableMultipleHttp2Connections = false;
            options.ConfigureHandler = handler =>
            {
                capturedHandler = handler;
                observedEnableMultiple = handler.EnableMultipleHttp2Connections;
                configureInvoked = true;
            };
        });

        using var provider = services.BuildServiceProvider();
        var factory = provider.GetRequiredService<IHttpClientFactory>();

        var client = factory.CreateClient("source.test");

        Assert.Equal(HttpVersion.Version20, client.DefaultRequestVersion);
        Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, client.DefaultVersionPolicy);
        Assert.True(configureInvoked);
        Assert.False(observedEnableMultiple);
        Assert.NotNull(capturedHandler);
    }

    [Fact]
    public void AddSourceHttpClient_LoadsProxyConfiguration()
    {
        var services = new ServiceCollection();
        services.AddLogging();

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"feedser:httpClients:source.icscisa:{ProxySection}:{ProxyAddressKey}"] = "http://proxy.local:8080",
                [$"feedser:httpClients:source.icscisa:{ProxySection}:{ProxyBypassOnLocalKey}"] = "false",
                [$"feedser:httpClients:source.icscisa:{ProxySection}:{ProxyBypassListKey}:0"] = "localhost",
                [$"feedser:httpClients:source.icscisa:{ProxySection}:{ProxyBypassListKey}:1"] = "127.0.0.1",
                [$"feedser:httpClients:source.icscisa:{ProxySection}:{ProxyUseDefaultCredentialsKey}"] = "false",
                [$"feedser:httpClients:source.icscisa:{ProxySection}:{ProxyUsernameKey}"] = "svc-feedser",
                [$"feedser:httpClients:source.icscisa:{ProxySection}:{ProxyPasswordKey}"] = "s3cr3t!",
            })
            .Build();

        services.AddSingleton<IConfiguration>(configuration);

        services.AddSourceHttpClient("source.icscisa", (_, options) =>
        {
            options.AllowedHosts.Add("content.govdelivery.com");
            options.ProxyAddress = new Uri("http://configure.local:9000");
        });

        using var provider = services.BuildServiceProvider();
        _ = provider.GetRequiredService<IHttpClientFactory>().CreateClient("source.icscisa");

        var resolvedConfiguration = provider.GetRequiredService<IConfiguration>();
        var proxySection = resolvedConfiguration
            .GetSection("feedser")
            .GetSection("httpClients")
            .GetSection("source.icscisa")
            .GetSection("proxy");
        Assert.True(proxySection.Exists());
        Assert.Equal("http://proxy.local:8080", proxySection[ProxyAddressKey]);

        var configuredOptions = provider.GetRequiredService<IOptionsMonitor<SourceHttpClientOptions>>().Get("source.icscisa");
        Assert.NotNull(configuredOptions.ProxyAddress);
        Assert.Equal(new Uri("http://proxy.local:8080"), configuredOptions.ProxyAddress);
        Assert.False(configuredOptions.ProxyBypassOnLocal);
        Assert.Contains("localhost", configuredOptions.ProxyBypassList, StringComparer.OrdinalIgnoreCase);
        Assert.Contains("127.0.0.1", configuredOptions.ProxyBypassList);
        Assert.False(configuredOptions.ProxyUseDefaultCredentials);
        Assert.Equal("svc-feedser", configuredOptions.ProxyUsername);
        Assert.Equal("s3cr3t!", configuredOptions.ProxyPassword);
    }

    [Fact]
    public void AddSourceHttpClient_UsesConfigurationToBypassValidation()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        using var trustedRoot = CreateSelfSignedCertificate();
        var pemPath = Path.Combine(Path.GetTempPath(), $"stellaops-trust-{Guid.NewGuid():N}.pem");
        WriteCertificatePem(trustedRoot, pemPath);

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"feedser:httpClients:source.acsc:{AllowInvalidKey}"] = "true",
                [$"feedser:httpClients:source.acsc:{TrustedRootPathsKey}:0"] = pemPath,
            })
            .Build();

        services.AddSingleton<IConfiguration>(configuration);

        bool configureInvoked = false;
        SocketsHttpHandler? capturedHandler = null;

        services.AddSourceHttpClient("source.acsc", (_, options) =>
        {
            options.AllowedHosts.Add("example.test");
            options.ConfigureHandler = handler =>
            {
                capturedHandler = handler;
                configureInvoked = true;
            };
        });

        using var provider = services.BuildServiceProvider();
        var factory = provider.GetRequiredService<IHttpClientFactory>();

        var client = factory.CreateClient("source.acsc");
        var optionsMonitor = provider.GetRequiredService<IOptionsMonitor<SourceHttpClientOptions>>();
        var configuredOptions = optionsMonitor.Get("source.acsc");

        Assert.True(configureInvoked);
        Assert.NotNull(capturedHandler);
        Assert.True(configuredOptions.AllowInvalidServerCertificates);
        Assert.NotNull(capturedHandler!.SslOptions.RemoteCertificateValidationCallback);

        var callback = capturedHandler.SslOptions.RemoteCertificateValidationCallback!;
#pragma warning disable SYSLIB0057
        using var serverCertificate = new X509Certificate2(trustedRoot.Export(X509ContentType.Cert));
#pragma warning restore SYSLIB0057
        var result = callback(new object(), serverCertificate, null, SslPolicyErrors.RemoteCertificateChainErrors);
        Assert.True(result);

        File.Delete(pemPath);
    }

    [Fact]
    public void AddSourceHttpClient_LoadsTrustedRootsFromOfflineRoot()
    {
        var services = new ServiceCollection();
        services.AddLogging();

        using var trustedRoot = CreateSelfSignedCertificate();
        var offlineRoot = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), $"stellaops-offline-{Guid.NewGuid():N}"));
        var relativePath = Path.Combine("trust", "root.pem");
        var certificatePath = Path.Combine(offlineRoot.FullName, relativePath);
        Directory.CreateDirectory(Path.GetDirectoryName(certificatePath)!);
        WriteCertificatePem(trustedRoot, certificatePath);

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"feedser:{OfflineRootKey}"] = offlineRoot.FullName,
                [$"feedser:httpClients:source.nkcki:{TrustedRootPathsKey}:0"] = relativePath,
            })
            .Build();

        services.AddSingleton<IConfiguration>(configuration);

        SocketsHttpHandler? capturedHandler = null;
        services.AddSourceHttpClient("source.nkcki", (_, options) =>
        {
            options.AllowedHosts.Add("example.test");
            options.ConfigureHandler = handler => capturedHandler = handler;
        });

        using var provider = services.BuildServiceProvider();
        var factory = provider.GetRequiredService<IHttpClientFactory>();
        _ = factory.CreateClient("source.nkcki");

        var monitor = provider.GetRequiredService<IOptionsMonitor<SourceHttpClientOptions>>();
        var configuredOptions = monitor.Get("source.nkcki");

        Assert.False(configuredOptions.AllowInvalidServerCertificates);
        Assert.Contains(
            configuredOptions.TrustedRootCertificates,
            certificate => string.Equals(certificate.Thumbprint, trustedRoot.Thumbprint, StringComparison.OrdinalIgnoreCase));

        Assert.NotNull(capturedHandler);
        var callback = capturedHandler!.SslOptions.RemoteCertificateValidationCallback;
        Assert.NotNull(callback);
#pragma warning disable SYSLIB0057
        using var serverCertificate = new X509Certificate2(trustedRoot.Export(X509ContentType.Cert));
#pragma warning restore SYSLIB0057
        var validationResult = callback!(new object(), serverCertificate, null, SslPolicyErrors.RemoteCertificateChainErrors);
        Assert.True(validationResult);

        Directory.Delete(offlineRoot.FullName, recursive: true);
    }

    [Fact]
    public void AddSourceHttpClient_LoadsConfigurationFromSourceHttpSection()
    {
        var services = new ServiceCollection();
        services.AddLogging();

        using var trustedRoot = CreateSelfSignedCertificate();
        var offlineRoot = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), $"stellaops-offline-{Guid.NewGuid():N}"));
        var relativePath = Path.Combine("certs", "root.pem");
        var certificatePath = Path.Combine(offlineRoot.FullName, relativePath);
        Directory.CreateDirectory(Path.GetDirectoryName(certificatePath)!);
        WriteCertificatePem(trustedRoot, certificatePath);

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"feedser:{OfflineRootKey}"] = offlineRoot.FullName,
                [$"feedser:sources:nkcki:http:{TrustedRootPathsKey}:0"] = relativePath,
            })
            .Build();

        services.AddSingleton<IConfiguration>(configuration);

        SocketsHttpHandler? capturedHandler = null;
        services.AddSourceHttpClient("source.nkcki", (_, options) =>
        {
            options.AllowedHosts.Add("example.test");
            options.ConfigureHandler = handler => capturedHandler = handler;
        });

        using var provider = services.BuildServiceProvider();
        _ = provider.GetRequiredService<IHttpClientFactory>().CreateClient("source.nkcki");

        var configuredOptions = provider.GetRequiredService<IOptionsMonitor<SourceHttpClientOptions>>().Get("source.nkcki");
        Assert.False(configuredOptions.AllowInvalidServerCertificates);
        Assert.Contains(
            configuredOptions.TrustedRootCertificates,
            certificate => string.Equals(certificate.Thumbprint, trustedRoot.Thumbprint, StringComparison.OrdinalIgnoreCase));

        Assert.NotNull(capturedHandler);
        var callback = capturedHandler!.SslOptions.RemoteCertificateValidationCallback;
        Assert.NotNull(callback);
#pragma warning disable SYSLIB0057
        using var serverCertificate = new X509Certificate2(trustedRoot.Export(X509ContentType.Cert));
#pragma warning restore SYSLIB0057
        var validationResult = callback!(new object(), serverCertificate, null, SslPolicyErrors.RemoteCertificateChainErrors);
        Assert.True(validationResult);

        Directory.Delete(offlineRoot.FullName, recursive: true);
    }

    private static X509Certificate2 CreateSelfSignedCertificate()
    {
        using var rsa = RSA.Create(2048);
        var request = new CertificateRequest("CN=StellaOps Test Root", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
        request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));
        request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));

        return request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(5));
    }

    private static void WriteCertificatePem(X509Certificate2 certificate, string path)
    {
        var builder = new StringBuilder();
        builder.AppendLine("-----BEGIN CERTIFICATE-----");
        builder.AppendLine(Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks));
        builder.AppendLine("-----END CERTIFICATE-----");
        File.WriteAllText(path, builder.ToString(), Encoding.ASCII);
    }

    private const string AllowInvalidKey = "allowInvalidCertificates";
    private const string TrustedRootPathsKey = "trustedRootPaths";
    private const string OfflineRootKey = "offlineRoot";
    private const string ProxySection = "proxy";
    private const string ProxyAddressKey = "address";
    private const string ProxyBypassOnLocalKey = "bypassOnLocal";
    private const string ProxyBypassListKey = "bypassList";
    private const string ProxyUseDefaultCredentialsKey = "useDefaultCredentials";
    private const string ProxyUsernameKey = "username";
    private const string ProxyPasswordKey = "password";
}
