// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.DataProtection.Abstractions;
using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.DataProtection;

public class DataProtectionCommonExtensionsTests
{
    [Theory]
    [InlineData(new object[] { new string[0] })]
    [InlineData(new object[] { new string[] { null } })]
    [InlineData(new object[] { new string[] { "the next value is bad", null } })]
    public void CreateProtector_ChainedAsIEnumerable_FailureCases(string[] purposes)
    {
        // Arrange
        var mockProtector = new Mock<IDataProtector>();
        mockProtector.Setup(o => o.CreateProtector(It.IsAny<string>())).Returns(mockProtector.Object);
        var provider = mockProtector.Object;

        // Act & assert
        ExceptionAssert.ThrowsArgument(
            testCode: () => provider.CreateProtector((IEnumerable<string>)purposes),
            paramName: "purposes",
            exceptionMessage: Resources.DataProtectionExtensions_NullPurposesCollection);
    }

    [Theory]
    [InlineData(new object[] { new string[] { null } })]
    [InlineData(new object[] { new string[] { "the next value is bad", null } })]
    public void CreateProtector_ChainedAsParams_FailureCases(string[] subPurposes)
    {
        // Arrange
        var mockProtector = new Mock<IDataProtector>();
        mockProtector.Setup(o => o.CreateProtector(It.IsAny<string>())).Returns(mockProtector.Object);
        var provider = mockProtector.Object;

        // Act & assert
        ExceptionAssert.ThrowsArgument(
            testCode: () => provider.CreateProtector("primary-purpose", subPurposes),
            paramName: "purposes",
            exceptionMessage: Resources.DataProtectionExtensions_NullPurposesCollection);
    }

    [Fact]
    public void CreateProtector_ChainedAsIEnumerable_SuccessCase()
    {
        // Arrange
        var finalExpectedProtector = new Mock<IDataProtector>().Object;

        var thirdMock = new Mock<IDataProtector>();
        thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
        var secondMock = new Mock<IDataProtector>();
        secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
        var firstMock = new Mock<IDataProtector>();
        firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);

        // Act
        var retVal = firstMock.Object.CreateProtector((IEnumerable<string>)new string[] { "first", "second", "third" });

        // Assert
        Assert.Same(finalExpectedProtector, retVal);
    }

    [Fact]
    public void CreateProtector_ChainedAsParams_NonEmptyParams_SuccessCase()
    {
        // Arrange
        var finalExpectedProtector = new Mock<IDataProtector>().Object;

        var thirdMock = new Mock<IDataProtector>();
        thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
        var secondMock = new Mock<IDataProtector>();
        secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
        var firstMock = new Mock<IDataProtector>();
        firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);

        // Act
        var retVal = firstMock.Object.CreateProtector("first", "second", "third");

        // Assert
        Assert.Same(finalExpectedProtector, retVal);
    }

    [Theory]
    [InlineData(new object[] { null })]
    [InlineData(new object[] { new string[0] })]
    public void CreateProtector_ChainedAsParams_EmptyParams_SuccessCases(string[] subPurposes)
    {
        // Arrange
        var finalExpectedProtector = new Mock<IDataProtector>().Object;
        var firstMock = new Mock<IDataProtector>();
        firstMock.Setup(o => o.CreateProtector("first")).Returns(finalExpectedProtector);

        // Act
        var retVal = firstMock.Object.CreateProtector("first", subPurposes);

        // Assert
        Assert.Same(finalExpectedProtector, retVal);
    }

    [Fact]
    public void GetDataProtectionProvider_NoServiceFound_Throws()
    {
        // Arrange
        var services = new Mock<IServiceProvider>().Object;

        // Act & assert
        var ex = Assert.Throws<InvalidOperationException>(() => services.GetDataProtectionProvider());
        Assert.Equal(Resources.FormatDataProtectionExtensions_NoService(typeof(IDataProtectionProvider).FullName), ex.Message);
    }

    [Fact]
    public void GetDataProtectionProvider_ServiceFound_ReturnsService()
    {
        // Arrange
        var expected = new Mock<IDataProtectionProvider>().Object;
        var mockServices = new Mock<IServiceProvider>();
        mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(expected);
        var services = mockServices.Object;

        // Act
        var actual = services.GetDataProtectionProvider();

        // Assert
        Assert.Same(expected, actual);
    }

    [Theory]
    [InlineData(new object[] { new string[0] })]
    [InlineData(new object[] { new string[] { null } })]
    [InlineData(new object[] { new string[] { "the next value is bad", null } })]
    public void GetDataProtector_ChainedAsIEnumerable_FailureCases(string[] purposes)
    {
        // Arrange
        var mockProtector = new Mock<IDataProtector>();
        mockProtector.Setup(o => o.CreateProtector(It.IsAny<string>())).Returns(mockProtector.Object);
        var mockServices = new Mock<IServiceProvider>();
        mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(mockProtector.Object);
        var services = mockServices.Object;

        // Act & assert
        ExceptionAssert.ThrowsArgument(
            testCode: () => services.GetDataProtector((IEnumerable<string>)purposes),
            paramName: "purposes",
            exceptionMessage: Resources.DataProtectionExtensions_NullPurposesCollection);
    }

    [Theory]
    [InlineData(new object[] { new string[] { null } })]
    [InlineData(new object[] { new string[] { "the next value is bad", null } })]
    public void GetDataProtector_ChainedAsParams_FailureCases(string[] subPurposes)
    {
        // Arrange
        var mockProtector = new Mock<IDataProtector>();
        mockProtector.Setup(o => o.CreateProtector(It.IsAny<string>())).Returns(mockProtector.Object);
        var mockServices = new Mock<IServiceProvider>();
        mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(mockProtector.Object);
        var services = mockServices.Object;

        // Act & assert
        ExceptionAssert.ThrowsArgument(
            testCode: () => services.GetDataProtector("primary-purpose", subPurposes),
            paramName: "purposes",
            exceptionMessage: Resources.DataProtectionExtensions_NullPurposesCollection);
    }

    [Fact]
    public void GetDataProtector_ChainedAsIEnumerable_SuccessCase()
    {
        // Arrange
        var finalExpectedProtector = new Mock<IDataProtector>().Object;

        var thirdMock = new Mock<IDataProtector>();
        thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
        var secondMock = new Mock<IDataProtector>();
        secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
        var firstMock = new Mock<IDataProtector>();
        firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);

        var mockServices = new Mock<IServiceProvider>();
        mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(firstMock.Object);
        var services = mockServices.Object;

        // Act
        var retVal = services.GetDataProtector((IEnumerable<string>)new string[] { "first", "second", "third" });

        // Assert
        Assert.Same(finalExpectedProtector, retVal);
    }

    [Fact]
    public void GetDataProtector_ChainedAsParams_NonEmptyParams_SuccessCase()
    {
        // Arrange
        var finalExpectedProtector = new Mock<IDataProtector>().Object;

        var thirdMock = new Mock<IDataProtector>();
        thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
        var secondMock = new Mock<IDataProtector>();
        secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
        var firstMock = new Mock<IDataProtector>();
        firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);

        var mockServices = new Mock<IServiceProvider>();
        mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(firstMock.Object);
        var services = mockServices.Object;

        // Act
        var retVal = services.GetDataProtector("first", "second", "third");

        // Assert
        Assert.Same(finalExpectedProtector, retVal);
    }

    [Theory]
    [InlineData(new object[] { null })]
    [InlineData(new object[] { new string[0] })]
    public void GetDataProtector_ChainedAsParams_EmptyParams_SuccessCases(string[] subPurposes)
    {
        // Arrange
        var finalExpectedProtector = new Mock<IDataProtector>().Object;
        var firstMock = new Mock<IDataProtector>();
        firstMock.Setup(o => o.CreateProtector("first")).Returns(finalExpectedProtector);
        var mockServices = new Mock<IServiceProvider>();
        mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(firstMock.Object);
        var services = mockServices.Object;

        // Act
        var retVal = services.GetDataProtector("first", subPurposes);

        // Assert
        Assert.Same(finalExpectedProtector, retVal);
    }

    [Fact]
    public void Protect_InvalidUtf8_Failure()
    {
        // Arrange
        Mock<IDataProtector> mockProtector = new Mock<IDataProtector>();

        // Act & assert
        var ex = Assert.Throws<CryptographicException>(() =>
        {
            mockProtector.Object.Protect("Hello\ud800");
        });
        Assert.IsAssignableFrom<EncoderFallbackException>(ex.InnerException);
    }

    [Fact]
    public void Protect_Success()
    {
        // Arrange
        Mock<IDataProtector> mockProtector = new Mock<IDataProtector>();
        mockProtector.Setup(p => p.Protect(new byte[] { 0x48, 0x65, 0x6c, 0x6c, 0x6f })).Returns(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 });

        // Act
        string retVal = mockProtector.Object.Protect("Hello");

        // Assert
        Assert.Equal("AQIDBAU", retVal);
    }

    [Fact]
    public void Unprotect_InvalidBase64BeforeDecryption_Failure()
    {
        // Arrange
        Mock<IDataProtector> mockProtector = new Mock<IDataProtector>();

        // Act & assert
        var ex = Assert.Throws<CryptographicException>(() =>
        {
            mockProtector.Object.Unprotect("A");
        });
    }

    [Fact]
    public void Unprotect_InvalidUtf8AfterDecryption_Failure()
    {
        // Arrange
        Mock<IDataProtector> mockProtector = new Mock<IDataProtector>();
        mockProtector.Setup(p => p.Unprotect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0xff });

        // Act & assert
        var ex = Assert.Throws<CryptographicException>(() =>
        {
            mockProtector.Object.Unprotect("AQIDBAU");
        });
        Assert.IsAssignableFrom<DecoderFallbackException>(ex.InnerException);
    }

    [Fact]
    public void Unprotect_Success()
    {
        // Arrange
        Mock<IDataProtector> mockProtector = new Mock<IDataProtector>();
        mockProtector.Setup(p => p.Unprotect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0x48, 0x65, 0x6c, 0x6c, 0x6f });

        // Act
        string retVal = DataProtectionCommonExtensions.Unprotect(mockProtector.Object, "AQIDBAU");

        // Assert
        Assert.Equal("Hello", retVal);
    }
}
