// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.DataProtection.KeyManagement
{
    public class KeyRingTests
    {
        [Fact]
        public void DefaultAuthenticatedEncryptor_Prop_InstantiationIsDeferred()
        {
            // Arrange
            var expectedEncryptorInstance = new Mock<IAuthenticatedEncryptor>().Object;

            var key1 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance);
            var key2 = new MyKey();

            // Act
            var keyRing = new KeyRing(key1, new[] { key1, key2 });

            // Assert
            Assert.Equal(0, key1.NumTimesCreateEncryptorInstanceCalled);
            Assert.Same(expectedEncryptorInstance, keyRing.DefaultAuthenticatedEncryptor);
            Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled);
            Assert.Same(expectedEncryptorInstance, keyRing.DefaultAuthenticatedEncryptor);
            Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled); // should've been cached
        }

        [Fact]
        public void DefaultKeyId_Prop()
        {
            // Arrange
            var key1 = new MyKey();
            var key2 = new MyKey();

            // Act
            var keyRing = new KeyRing(key2, new[] { key1, key2 });

            // Assert
            Assert.Equal(key2.KeyId, keyRing.DefaultKeyId);
        }

        [Fact]
        public void DefaultKeyIdAndEncryptor_IfDefaultKeyNotPresentInAllKeys()
        {
            // Arrange
            var key1 = new MyKey();
            var key2 = new MyKey();
            var key3 = new MyKey(expectedEncryptorInstance: new Mock<IAuthenticatedEncryptor>().Object);

            // Act
            var keyRing = new KeyRing(key3, new[] { key1, key2 });

            // Assert
            Assert.Equal(key3.KeyId, keyRing.DefaultKeyId);
            Assert.Equal(key3.CreateEncryptor(), keyRing.GetAuthenticatedEncryptorByKeyId(key3.KeyId, out var _));
        }

        [Fact]
        public void GetAuthenticatedEncryptorByKeyId_DefersInstantiation_AndReturnsRevocationInfo()
        {
            // Arrange
            var expectedEncryptorInstance1 = new Mock<IAuthenticatedEncryptor>().Object;
            var expectedEncryptorInstance2 = new Mock<IAuthenticatedEncryptor>().Object;

            var key1 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance1, isRevoked: true);
            var key2 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance2);


            // Act
            var keyRing = new KeyRing(key2, new[] { key1, key2 });

            // Assert
            Assert.Equal(0, key1.NumTimesCreateEncryptorInstanceCalled);
            Assert.Same(expectedEncryptorInstance1, keyRing.GetAuthenticatedEncryptorByKeyId(key1.KeyId, out var isRevoked));
            Assert.True(isRevoked);
            Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled);
            Assert.Same(expectedEncryptorInstance1, keyRing.GetAuthenticatedEncryptorByKeyId(key1.KeyId, out isRevoked));
            Assert.True(isRevoked);
            Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled);
            Assert.Equal(0, key2.NumTimesCreateEncryptorInstanceCalled);
            Assert.Same(expectedEncryptorInstance2, keyRing.GetAuthenticatedEncryptorByKeyId(key2.KeyId, out isRevoked));
            Assert.False(isRevoked);
            Assert.Equal(1, key2.NumTimesCreateEncryptorInstanceCalled);
            Assert.Same(expectedEncryptorInstance2, keyRing.GetAuthenticatedEncryptorByKeyId(key2.KeyId, out isRevoked));
            Assert.False(isRevoked);
            Assert.Equal(1, key2.NumTimesCreateEncryptorInstanceCalled);
            Assert.Same(expectedEncryptorInstance2, keyRing.DefaultAuthenticatedEncryptor);
            Assert.Equal(1, key2.NumTimesCreateEncryptorInstanceCalled);
        }

        private sealed class MyKey : IKey
        {
            public int NumTimesCreateEncryptorInstanceCalled;
            private readonly Func<IAuthenticatedEncryptor> _encryptorFactory;

            public MyKey(bool isRevoked = false, IAuthenticatedEncryptor expectedEncryptorInstance = null)
            {
                CreationDate = DateTimeOffset.Now;
                ActivationDate = CreationDate + TimeSpan.FromHours(1);
                ExpirationDate = CreationDate + TimeSpan.FromDays(30);
                IsRevoked = isRevoked;
                KeyId = Guid.NewGuid();
                _encryptorFactory = () => expectedEncryptorInstance ?? new Mock<IAuthenticatedEncryptor>().Object;
            }

            public DateTimeOffset ActivationDate { get; }
            public DateTimeOffset CreationDate { get; }
            public DateTimeOffset ExpirationDate { get; }
            public bool IsRevoked { get; }
            public Guid KeyId { get; }
            public IAuthenticatedEncryptorDescriptor Descriptor => throw new NotImplementedException();

            public IAuthenticatedEncryptor CreateEncryptor()
            {
                NumTimesCreateEncryptorInstanceCalled++;
                return _encryptorFactory();
            }
        }
    }
}
