// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.DataProtection.Internal;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Win32;
using Xunit;

namespace Microsoft.AspNetCore.DataProtection
{
    public class RegistryPolicyResolverTests
    {
        [ConditionalFact]
        [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
        public void ResolvePolicy_NoEntries_ResultsInNoPolicies()
        {
            // Arrange
            var registryEntries = new Dictionary<string, object>();

            // Act
            var context = RunTestWithRegValues(registryEntries);

            // Assert
            Assert.Null(context.EncryptorConfiguration);
            Assert.Null(context.DefaultKeyLifetime);
            Assert.Empty(context.KeyEscrowSinks);
        }

        [ConditionalFact]
        [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
        public void ResolvePolicy_KeyEscrowSinks()
        {
            // Arrange
            var registryEntries = new Dictionary<string, object>()
            {
                ["KeyEscrowSinks"] = String.Join(" ;; ; ", new Type[] { typeof(MyKeyEscrowSink1), typeof(MyKeyEscrowSink2) }.Select(t => t.AssemblyQualifiedName))
            };

            // Act
            var context = RunTestWithRegValues(registryEntries);

            // Assert
            var actualKeyEscrowSinks = context.KeyEscrowSinks.ToArray();
            Assert.Equal(2, actualKeyEscrowSinks.Length);
            Assert.IsType<MyKeyEscrowSink1>(actualKeyEscrowSinks[0]);
            Assert.IsType<MyKeyEscrowSink2>(actualKeyEscrowSinks[1]);
        }

        [ConditionalFact]
        [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
        public void ResolvePolicy_DefaultKeyLifetime()
        {
            // Arrange
            var registryEntries = new Dictionary<string, object>()
            {
                ["DefaultKeyLifetime"] = 1024 // days
            };

            // Act
            var context = RunTestWithRegValues(registryEntries);

            // Assert
            Assert.Equal(1024, context.DefaultKeyLifetime);
        }

        [ConditionalFact]
        [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
        public void ResolvePolicy_CngCbcEncryption_WithoutExplicitSettings()
        {
            // Arrange
            var registryEntries = new Dictionary<string, object>()
            {
                ["EncryptionType"] = "cng-cbc"
            };
            var expectedConfiguration = new CngCbcAuthenticatedEncryptorConfiguration();

            // Act
            var context = RunTestWithRegValues(registryEntries);

            // Assert
            var actualConfiguration = (CngCbcAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;

            Assert.Equal(expectedConfiguration.EncryptionAlgorithm, actualConfiguration.EncryptionAlgorithm);
            Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
            Assert.Equal(expectedConfiguration.EncryptionAlgorithmProvider, actualConfiguration.EncryptionAlgorithmProvider);
            Assert.Equal(expectedConfiguration.HashAlgorithm, actualConfiguration.HashAlgorithm);
            Assert.Equal(expectedConfiguration.HashAlgorithmProvider, actualConfiguration.HashAlgorithmProvider);
        }

        [ConditionalFact]
        [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
        public void ResolvePolicy_CngCbcEncryption_WithExplicitSettings()
        {
            // Arrange
            var registryEntries = new Dictionary<string, object>()
            {
                ["EncryptionType"] = "cng-cbc",
                ["EncryptionAlgorithm"] = "enc-alg",
                ["EncryptionAlgorithmKeySize"] = 2048,
                ["EncryptionAlgorithmProvider"] = "my-enc-alg-provider",
                ["HashAlgorithm"] = "hash-alg",
                ["HashAlgorithmProvider"] = "my-hash-alg-provider"
            };
            var expectedConfiguration = new CngCbcAuthenticatedEncryptorConfiguration()
            {
                EncryptionAlgorithm = "enc-alg",
                EncryptionAlgorithmKeySize = 2048,
                EncryptionAlgorithmProvider = "my-enc-alg-provider",
                HashAlgorithm = "hash-alg",
                HashAlgorithmProvider = "my-hash-alg-provider"
            };

            // Act
            var context = RunTestWithRegValues(registryEntries);

            // Assert
            var actualConfiguration = (CngCbcAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;

            Assert.Equal(expectedConfiguration.EncryptionAlgorithm, actualConfiguration.EncryptionAlgorithm);
            Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
            Assert.Equal(expectedConfiguration.EncryptionAlgorithmProvider, actualConfiguration.EncryptionAlgorithmProvider);
            Assert.Equal(expectedConfiguration.HashAlgorithm, actualConfiguration.HashAlgorithm);
            Assert.Equal(expectedConfiguration.HashAlgorithmProvider, actualConfiguration.HashAlgorithmProvider);
        }

        [ConditionalFact]
        [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
        public void ResolvePolicy_CngGcmEncryption_WithoutExplicitSettings()
        {
            // Arrange
            var registryEntries = new Dictionary<string, object>()
            {
                ["EncryptionType"] = "cng-gcm"
            };
            var expectedConfiguration = new CngGcmAuthenticatedEncryptorConfiguration();

            // Act
            var context = RunTestWithRegValues(registryEntries);

            // Assert
            var actualConfiguration = (CngGcmAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;

            Assert.Equal(expectedConfiguration.EncryptionAlgorithm, actualConfiguration.EncryptionAlgorithm);
            Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
            Assert.Equal(expectedConfiguration.EncryptionAlgorithmProvider, actualConfiguration.EncryptionAlgorithmProvider);
        }

        [ConditionalFact]
        [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
        public void ResolvePolicy_CngGcmEncryption_WithExplicitSettings()
        {
            // Arrange
            var registryEntries = new Dictionary<string, object>()
            {
                ["EncryptionType"] = "cng-gcm",
                ["EncryptionAlgorithm"] = "enc-alg",
                ["EncryptionAlgorithmKeySize"] = 2048,
                ["EncryptionAlgorithmProvider"] = "my-enc-alg-provider"
            };
            var expectedConfiguration = new CngGcmAuthenticatedEncryptorConfiguration()
            {
                EncryptionAlgorithm = "enc-alg",
                EncryptionAlgorithmKeySize = 2048,
                EncryptionAlgorithmProvider = "my-enc-alg-provider"
            };

            // Act
            var context = RunTestWithRegValues(registryEntries);

            // Assert
            var actualConfiguration = (CngGcmAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;

            Assert.Equal(expectedConfiguration.EncryptionAlgorithm, actualConfiguration.EncryptionAlgorithm);
            Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
            Assert.Equal(expectedConfiguration.EncryptionAlgorithmProvider, actualConfiguration.EncryptionAlgorithmProvider);
        }

        [ConditionalFact]
        [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
        public void ResolvePolicy_ManagedEncryption_WithoutExplicitSettings()
        {
            // Arrange
            var registryEntries = new Dictionary<string, object>()
            {
                ["EncryptionType"] = "managed"
            };
            var expectedConfiguration = new ManagedAuthenticatedEncryptorConfiguration();

            // Act
            var context = RunTestWithRegValues(registryEntries);

            // Assert
            var actualConfiguration = (ManagedAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;

            Assert.Equal(expectedConfiguration.EncryptionAlgorithmType, actualConfiguration.EncryptionAlgorithmType);
            Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
            Assert.Equal(expectedConfiguration.ValidationAlgorithmType, actualConfiguration.ValidationAlgorithmType);
        }

        [ConditionalFact]
        [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
        public void ResolvePolicy_ManagedEncryption_WithExplicitSettings()
        {
            // Arrange
            var registryEntries = new Dictionary<string, object>()
            {
                ["EncryptionType"] = "managed",
                ["EncryptionAlgorithmType"] = typeof(TripleDES).AssemblyQualifiedName,
                ["EncryptionAlgorithmKeySize"] = 2048,
                ["ValidationAlgorithmType"] = typeof(HMACSHA1).AssemblyQualifiedName
            };
            var expectedConfiguration = new ManagedAuthenticatedEncryptorConfiguration()
            {
                EncryptionAlgorithmType = typeof(TripleDES),
                EncryptionAlgorithmKeySize = 2048,
                ValidationAlgorithmType = typeof(HMACSHA1)
            };

            // Act
            var context = RunTestWithRegValues(registryEntries);

            // Assert
            var actualConfiguration = (ManagedAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;

            Assert.Equal(expectedConfiguration.EncryptionAlgorithmType, actualConfiguration.EncryptionAlgorithmType);
            Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
            Assert.Equal(expectedConfiguration.ValidationAlgorithmType, actualConfiguration.ValidationAlgorithmType);
        }

        private static RegistryPolicy RunTestWithRegValues(Dictionary<string, object> regValues)
        {
            return WithUniqueTempRegKey(registryKey =>
            {
                foreach (var entry in regValues)
                {
                    registryKey.SetValue(entry.Key, entry.Value);
                }

                var policyResolver = new RegistryPolicyResolver(
                    registryKey,
                    activator: SimpleActivator.DefaultWithoutServices);

                return policyResolver.ResolvePolicy();
            });
        }

        /// <summary>
        /// Runs a test and cleans up the registry key afterward.
        /// </summary>
        private static RegistryPolicy WithUniqueTempRegKey(Func<RegistryKey, RegistryPolicy> testCode)
        {
            string uniqueName = Guid.NewGuid().ToString();
            var uniqueSubkey = LazyHkcuTempKey.Value.CreateSubKey(uniqueName);
            try
            {
                return testCode(uniqueSubkey);
            }
            finally
            {
                // clean up when test is done
                LazyHkcuTempKey.Value.DeleteSubKeyTree(uniqueName, throwOnMissingSubKey: false);
            }
        }

        private static readonly Lazy<RegistryKey> LazyHkcuTempKey = new Lazy<RegistryKey>(() =>
        {
            try
            {
                return Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\ASP.NET\temp");
            }
            catch
            {
                // swallow all failures
                return null;
            }
        });

        private class ConditionalRunTestOnlyIfHkcuRegistryAvailable : Attribute, ITestCondition
        {
            public bool IsMet => (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && LazyHkcuTempKey.Value != null);

            public string SkipReason { get; } = "HKCU registry couldn't be opened.";
        }

        private class MyKeyEscrowSink1 : IKeyEscrowSink
        {
            public void Store(Guid keyId, XElement element)
            {
                throw new NotImplementedException();
            }
        }

        private class MyKeyEscrowSink2 : IKeyEscrowSink
        {
            public void Store(Guid keyId, XElement element)
            {
                throw new NotImplementedException();
            }
        }
    }
}
