Various C# Xunit test assertion helpers.

// 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.Reflection;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.AspNetCore.Testing
{
    // TODO: eventually want: public partial class Assert : Xunit.Assert
    public static class ExceptionAssert
    {
        /// <summary>
        /// Verifies that an exception of the given type (or optionally a derived type) is thrown.
        /// </summary>
        /// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static TException Throws<TException>(Action testCode)
            where TException : Exception
        {
            return VerifyException<TException>(RecordException(testCode));
        }

        /// <summary>
        /// Verifies that an exception of the given type is thrown.
        /// Also verifies that the exception message matches.
        /// </summary>
        /// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="exceptionMessage">The exception message to verify</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static TException Throws<TException>(Action testCode, string exceptionMessage)
            where TException : Exception
        {
            var ex = Throws<TException>(testCode);
            VerifyExceptionMessage(ex, exceptionMessage);
            return ex;
        }

        /// <summary>
        /// Verifies that an exception of the given type is thrown.
        /// Also verifies that the exception message matches.
        /// </summary>
        /// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="exceptionMessage">The exception message to verify</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static async Task<TException> ThrowsAsync<TException>(Func<Task> testCode, string exceptionMessage)
            where TException : Exception
        {
            // The 'testCode' Task might execute asynchronously in a different thread making it hard to enforce the thread culture.
            // The correct way to verify exception messages in such a scenario would be to run the task synchronously inside of a
            // culture enforced block.
            var ex = await Assert.ThrowsAsync<TException>(testCode);
            VerifyExceptionMessage(ex, exceptionMessage);
            return ex;
        }

        /// <summary>
        /// Verifies that an exception of the given type is thrown.
        /// Also verified that the exception message matches.
        /// </summary>
        /// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="exceptionMessage">The exception message to verify</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static TException Throws<TException>(Func<object> testCode, string exceptionMessage)
            where TException : Exception
        {
            return Throws<TException>(() => { testCode(); }, exceptionMessage);
        }

        /// <summary>
        /// Verifies that the code throws an <see cref="ArgumentException"/>
        /// </summary>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="paramName">The name of the parameter that should throw the exception</param>
        /// <param name="exceptionMessage">The exception message to verify</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static ArgumentException ThrowsArgument(Action testCode, string paramName, string exceptionMessage)
        {
            return ThrowsArgumentInternal<ArgumentException>(testCode, paramName, exceptionMessage);
        }

        private static TException ThrowsArgumentInternal<TException>(
            Action testCode,
            string paramName,
            string exceptionMessage)
            where TException : ArgumentException
        {
            var ex = Throws<TException>(testCode);
            if (paramName != null)
            {
                Assert.Equal(paramName, ex.ParamName);
            }
            VerifyExceptionMessage(ex, exceptionMessage, partialMatch: true);
            return ex;
        }

        /// <summary>
        /// Verifies that the code throws an <see cref="ArgumentException"/>.
        /// </summary>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="paramName">The name of the parameter that should throw the exception</param>
        /// <param name="exceptionMessage">The exception message to verify</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static Task<ArgumentException> ThrowsArgumentAsync(Func<Task> testCode, string paramName, string exceptionMessage)
        {
            return ThrowsArgumentAsyncInternal<ArgumentException>(testCode, paramName, exceptionMessage);
        }

        private static async Task<TException> ThrowsArgumentAsyncInternal<TException>(
            Func<Task> testCode,
            string paramName,
            string exceptionMessage)
            where TException : ArgumentException
        {
            var ex = await Assert.ThrowsAsync<TException>(testCode);
            if (paramName != null)
            {
                Assert.Equal(paramName, ex.ParamName);
            }
            VerifyExceptionMessage(ex, exceptionMessage, partialMatch: true);
            return ex;
        }

        /// <summary>
        /// Verifies that the code throws an <see cref="ArgumentNullException"/>.
        /// </summary>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="paramName">The name of the parameter that should throw the exception</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static ArgumentNullException ThrowsArgumentNull(Action testCode, string paramName)
        {
            var ex = Throws<ArgumentNullException>(testCode);
            if (paramName != null)
            {
                Assert.Equal(paramName, ex.ParamName);
            }
            return ex;
        }

        /// <summary>
        /// Verifies that the code throws an ArgumentException with the expected message that indicates that the value cannot
        /// be null or empty.
        /// </summary>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="paramName">The name of the parameter that should throw the exception</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static ArgumentException ThrowsArgumentNullOrEmpty(Action testCode, string paramName)
        {
            return ThrowsArgumentInternal<ArgumentException>(testCode, paramName, "Value cannot be null or empty.");
        }

        /// <summary>
        /// Verifies that the code throws an ArgumentException with the expected message that indicates that the value cannot
        /// be null or empty.
        /// </summary>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="paramName">The name of the parameter that should throw the exception</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static Task<ArgumentException> ThrowsArgumentNullOrEmptyAsync(Func<Task> testCode, string paramName)
        {
            return ThrowsArgumentAsyncInternal<ArgumentException>(testCode, paramName, "Value cannot be null or empty.");
        }

        /// <summary>
        /// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot
        /// be null or empty string.
        /// </summary>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="paramName">The name of the parameter that should throw the exception</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static ArgumentException ThrowsArgumentNullOrEmptyString(Action testCode, string paramName)
        {
            return ThrowsArgumentInternal<ArgumentException>(testCode, paramName, "Value cannot be null or an empty string.");
        }

        /// <summary>
        /// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot
        /// be null or empty string.
        /// </summary>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="paramName">The name of the parameter that should throw the exception</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static Task<ArgumentException> ThrowsArgumentNullOrEmptyStringAsync(Func<Task> testCode, string paramName)
        {
            return ThrowsArgumentAsyncInternal<ArgumentException>(testCode, paramName, "Value cannot be null or an empty string.");
        }

        /// <summary>
        /// Verifies that the code throws an ArgumentOutOfRangeException (or optionally any exception which derives from it).
        /// </summary>
        /// <param name="testCode">A delegate to the code to be tested</param>
        /// <param name="paramName">The name of the parameter that should throw the exception</param>
        /// <param name="exceptionMessage">The exception message to verify</param>
        /// <param name="actualValue">The actual value provided</param>
        /// <returns>The exception that was thrown, when successful</returns>
        public static ArgumentOutOfRangeException ThrowsArgumentOutOfRange(Action testCode, string paramName, string exceptionMessage, object actualValue = null)
        {
            var ex = ThrowsArgumentInternal<ArgumentOutOfRangeException>(testCode, paramName, exceptionMessage);

            if (paramName != null)
            {
                Assert.Equal(paramName, ex.ParamName);
            }

            if (actualValue != null)
            {
                Assert.Equal(actualValue, ex.ActualValue);
            }

            return ex;
        }

        // We've re-implemented all the xUnit.net Throws code so that we can get this
        // updated implementation of RecordException which silently unwraps any instances
        // of AggregateException. In addition to unwrapping exceptions, this method ensures
        // that tests are executed in with a known set of Culture and UICulture. This prevents
        // tests from failing when executed on a non-English machine.
        private static Exception RecordException(Action testCode)
        {
            try
            {
                using (new CultureReplacer())
                {
                    testCode();
                }
                return null;
            }
            catch (Exception exception)
            {
                return UnwrapException(exception);
            }
        }

        private static Exception UnwrapException(Exception exception)
        {
            var aggEx = exception as AggregateException;
            return aggEx != null ? aggEx.GetBaseException() : exception;
        }

        private static TException VerifyException<TException>(Exception exception)
        {
            var tie = exception as TargetInvocationException;
            if (tie != null)
            {
                exception = tie.InnerException;
            }
            Assert.NotNull(exception);
            return Assert.IsAssignableFrom<TException>(exception);
        }

        private static void VerifyExceptionMessage(Exception exception, string expectedMessage, bool partialMatch = false)
        {
            if (expectedMessage != null)
            {
                if (!partialMatch)
                {
                    Assert.Equal(expectedMessage, exception.Message);
                }
                else
                {
                    Assert.Contains(expectedMessage, exception.Message);
                }
            }
        }
    }
}