// 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.Buffers;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Internal
{
    public class ControllerActionInvokerTest : CommonResourceInvokerTest
    {
        #region Diagnostics

        [Fact]
        public async Task Invoke_Success_LogsCorrectValues()
        {
            // Arrange
            var sink = new TestSink();
            var loggerFactory = new TestLoggerFactory(sink, enabled: true);
            var logger = loggerFactory.CreateLogger<ControllerActionInvoker>();

            var displayName = "A.B.C";
            var actionDescriptor = Mock.Of<ControllerActionDescriptor>(a => a.DisplayName == displayName);
            actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod));
            actionDescriptor.ControllerTypeInfo = typeof(TestController).GetTypeInfo();
            actionDescriptor.FilterDescriptors = new List<FilterDescriptor>();
            actionDescriptor.Parameters = new List<ParameterDescriptor>();
            actionDescriptor.BoundProperties = new List<ParameterDescriptor>();

            var filter = Mock.Of<IFilterMetadata>();
            var invoker = CreateInvoker(
                new[] { filter },
                actionDescriptor,
                controller: new TestController(),
                logger: logger);

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.Single(sink.Scopes);
            Assert.Equal(displayName, sink.Scopes[0].Scope?.ToString());

            Assert.Equal(4, sink.Writes.Count);
            Assert.Equal($"Executing action {displayName}", sink.Writes[0].State?.ToString());
            Assert.Equal($"Executing action method {displayName} with arguments ((null)) - ModelState is Valid", sink.Writes[1].State?.ToString());
            Assert.Equal($"Executed action method {displayName}, returned result {Result.GetType().FullName}.", sink.Writes[2].State?.ToString());
            // This message has the execution time embedded, which we don't want to verify.
            Assert.StartsWith($"Executed action {displayName} ", sink.Writes[3].State?.ToString());
        }

        [Fact]
        public async Task Invoke_WritesDiagnostic_ActionSelected()
        {
            // Arrange
            var actionDescriptor = new ControllerActionDescriptor()
            {
                FilterDescriptors = new List<FilterDescriptor>(),
                Parameters = new List<ParameterDescriptor>(),
                BoundProperties = new List<ParameterDescriptor>(),
            };

            actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod));
            actionDescriptor.ControllerTypeInfo = typeof(TestController).GetTypeInfo();

            var listener = new TestDiagnosticListener();

            var routeData = new RouteData();
            routeData.Values.Add("tag", "value");

            var filter = Mock.Of<IFilterMetadata>();
            var invoker = CreateInvoker(
                new[] { filter },
                actionDescriptor,
                controller: new TestController(),
                diagnosticListener: listener,
                routeData: routeData);

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.NotNull(listener.BeforeAction?.ActionDescriptor);
            Assert.NotNull(listener.BeforeAction?.HttpContext);

            var routeValues = listener.BeforeAction?.RouteData?.Values;
            Assert.NotNull(routeValues);

            Assert.Equal(1, routeValues.Count);
            Assert.Contains(routeValues, kvp => kvp.Key == "tag" && string.Equals(kvp.Value, "value"));
        }

        [Fact]
        public async Task Invoke_WritesDiagnostic_ActionInvoked()
        {
            // Arrange
            var actionDescriptor = new ControllerActionDescriptor()
            {
                FilterDescriptors = new List<FilterDescriptor>(),
                Parameters = new List<ParameterDescriptor>(),
                BoundProperties = new List<ParameterDescriptor>(),
            };

            actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod));
            actionDescriptor.ControllerTypeInfo = typeof(TestController).GetTypeInfo();

            var listener = new TestDiagnosticListener();

            var filter = Mock.Of<IFilterMetadata>();
            var invoker = CreateInvoker(
                new[] { filter },
                actionDescriptor,
                controller: new TestController(),
                diagnosticListener: listener);

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.NotNull(listener.AfterAction?.ActionDescriptor);
            Assert.NotNull(listener.AfterAction?.HttpContext);
        }

        #endregion

        #region Controller Context

        [Fact]
        public async Task AddingValueProviderFactory_AtResourceFilter_IsAvailableInControllerContext()
        {
            // Arrange
            var valueProviderFactory2 = Mock.Of<IValueProviderFactory>();
            var resourceFilter = new Mock<IResourceFilter>();
            resourceFilter
                .Setup(f => f.OnResourceExecuting(It.IsAny<ResourceExecutingContext>()))
                .Callback<ResourceExecutingContext>((resourceExecutingContext) =>
                {
                    resourceExecutingContext.ValueProviderFactories.Add(valueProviderFactory2);
                });
            var valueProviderFactory1 = Mock.Of<IValueProviderFactory>();
            var valueProviderFactories = new List<IValueProviderFactory>();
            valueProviderFactories.Add(valueProviderFactory1);

            var invoker = CreateInvoker(
                new IFilterMetadata[] { resourceFilter.Object }, valueProviderFactories: valueProviderFactories);

            // Act
            await invoker.InvokeAsync();

            // Assert
            var controllerContext = Assert.IsType<ControllerActionInvoker>(invoker).ControllerContext;
            Assert.NotNull(controllerContext);
            Assert.Equal(2, controllerContext.ValueProviderFactories.Count);
            Assert.Same(valueProviderFactory1, controllerContext.ValueProviderFactories[0]);
            Assert.Same(valueProviderFactory2, controllerContext.ValueProviderFactories[1]);
        }

        [Fact]
        public async Task DeletingValueProviderFactory_AtResourceFilter_IsNotAvailableInControllerContext()
        {
            // Arrange
            var resourceFilter = new Mock<IResourceFilter>();
            resourceFilter
                .Setup(f => f.OnResourceExecuting(It.IsAny<ResourceExecutingContext>()))
                .Callback<ResourceExecutingContext>((resourceExecutingContext) =>
                {
                    resourceExecutingContext.ValueProviderFactories.RemoveAt(0);
                });

            var valueProviderFactory1 = Mock.Of<IValueProviderFactory>();
            var valueProviderFactory2 = Mock.Of<IValueProviderFactory>();
            var valueProviderFactories = new List<IValueProviderFactory>();
            valueProviderFactories.Add(valueProviderFactory1);
            valueProviderFactories.Add(valueProviderFactory2);

            var invoker = CreateInvoker(
                new IFilterMetadata[] { resourceFilter.Object }, valueProviderFactories: valueProviderFactories);

            // Act
            await invoker.InvokeAsync();

            // Assert
            var controllerContext = Assert.IsType<ControllerActionInvoker>(invoker).ControllerContext;
            Assert.NotNull(controllerContext);
            Assert.Equal(1, controllerContext.ValueProviderFactories.Count);
            Assert.Same(valueProviderFactory2, controllerContext.ValueProviderFactories[0]);
        }

        #endregion

        #region Action Filters

        [Fact]
        public async Task InvokeAction_InvokesActionFilter()
        {
            // Arrange
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(filter.Object, result: Result);

            // Act
            await invoker.InvokeAsync();

            // Assert
            filter.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());
            filter.Verify(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()), Times.Once());

            Assert.Same(Result, result);
        }

        [Fact]
        public async Task InvokeAction_InvokesAsyncActionFilter()
        {
            // Arrange
            IActionResult result = null;

            var filter = new Mock<IAsyncActionFilter>(MockBehavior.Strict);
            filter
                .Setup(f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()))
                .Returns<ActionExecutingContext, ActionExecutionDelegate>(async (context, next) =>
                {
                    var resultContext = await next();
                    result = resultContext.Result;
                })
                .Verifiable();

            var invoker = CreateInvoker(filter.Object, result: Result);

            // Act
            await invoker.InvokeAsync();

            // Assert
            filter.Verify(
                f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()),
                Times.Once());

            Assert.Same(Result, result);
        }

        [Fact]
        public async Task InvokeAction_InvokesActionFilter_ShortCircuit()
        {
            // Arrange
            var result = new Mock<IActionResult>(MockBehavior.Strict);
            result
                .Setup(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()))
                .Returns(Task.FromResult(true))
                .Verifiable();

            ActionExecutedContext context = null;

            var actionFilter1 = new Mock<IActionFilter>(MockBehavior.Strict);
            actionFilter1.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            actionFilter1
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => context = c)
                .Verifiable();

            var actionFilter2 = new Mock<IActionFilter>(MockBehavior.Strict);
            actionFilter2
                .Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()))
                .Callback<ActionExecutingContext>(c => c.Result = result.Object)
                .Verifiable();

            var actionFilter3 = new Mock<IActionFilter>(MockBehavior.Strict);

            var resultFilter = new Mock<IResultFilter>(MockBehavior.Strict);
            resultFilter.Setup(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>())).Verifiable();
            resultFilter.Setup(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>())).Verifiable();

            var invoker = CreateInvoker(new IFilterMetadata[]
            {
                actionFilter1.Object,
                actionFilter2.Object,
                actionFilter3.Object,
                resultFilter.Object,
            });

            // Act
            await invoker.InvokeAsync();

            // Assert
            result.Verify(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()), Times.Once());
            actionFilter1.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());
            actionFilter1.Verify(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()), Times.Once());

            actionFilter2.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());
            actionFilter2.Verify(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()), Times.Never());

            resultFilter.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once());
            resultFilter.Verify(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()), Times.Once());

            Assert.True(context.Canceled);
            Assert.Same(context.Result, result.Object);
        }

        [Fact]
        public async Task InvokeAction_InvokesAsyncActionFilter_ShortCircuit_WithResult()
        {
            // Arrange
            var result = new Mock<IActionResult>(MockBehavior.Strict);
            result
                .Setup(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()))
                .Returns(Task.FromResult(true))
                .Verifiable();

            ActionExecutedContext context = null;

            var actionFilter1 = new Mock<IActionFilter>(MockBehavior.Strict);
            actionFilter1.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            actionFilter1
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => context = c)
                .Verifiable();

            var actionFilter2 = new Mock<IAsyncActionFilter>(MockBehavior.Strict);
            actionFilter2
                .Setup(f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()))
                .Returns<ActionExecutingContext, ActionExecutionDelegate>((c, next) =>
                {
                    // Notice we're not calling next
                    c.Result = result.Object;
                    return Task.FromResult(true);
                })
                .Verifiable();

            var actionFilter3 = new Mock<IActionFilter>(MockBehavior.Strict);

            var resultFilter1 = new Mock<IResultFilter>(MockBehavior.Strict);
            resultFilter1.Setup(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>())).Verifiable();
            resultFilter1.Setup(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>())).Verifiable();
            var resultFilter2 = new Mock<IResultFilter>(MockBehavior.Strict);
            resultFilter2.Setup(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>())).Verifiable();
            resultFilter2.Setup(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>())).Verifiable();

            var invoker = CreateInvoker(new IFilterMetadata[]
            {
                actionFilter1.Object,
                actionFilter2.Object,
                actionFilter3.Object,
                resultFilter1.Object,
                resultFilter2.Object,
            });

            // Act
            await invoker.InvokeAsync();

            // Assert
            result.Verify(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()), Times.Once());
            actionFilter1.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());
            actionFilter1.Verify(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()), Times.Once());

            actionFilter2.Verify(
                f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()),
                Times.Once());

            resultFilter1.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once());
            resultFilter1.Verify(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()), Times.Once());
            resultFilter2.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once());
            resultFilter2.Verify(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()), Times.Once());

            Assert.True(context.Canceled);
            Assert.Same(context.Result, result.Object);
        }

        [Fact]
        public async Task InvokeAction_InvokesAsyncActionFilter_ShortCircuit_WithoutResult()
        {
            // Arrange
            ActionExecutedContext context = null;

            var actionFilter1 = new Mock<IActionFilter>(MockBehavior.Strict);
            actionFilter1.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            actionFilter1
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => context = c)
                .Verifiable();

            var actionFilter2 = new Mock<IAsyncActionFilter>(MockBehavior.Strict);
            actionFilter2
                .Setup(f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()))
                .Returns<ActionExecutingContext, ActionExecutionDelegate>((c, next) =>
                {
                    // Notice we're not calling next
                    return Task.FromResult(true);
                })
                .Verifiable();

            var actionFilter3 = new Mock<IActionFilter>(MockBehavior.Strict);

            var resultFilter = new Mock<IResultFilter>(MockBehavior.Strict);
            resultFilter.Setup(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>())).Verifiable();
            resultFilter.Setup(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>())).Verifiable();

            var invoker = CreateInvoker(new IFilterMetadata[]
            {
                actionFilter1.Object,
                actionFilter2.Object,
                actionFilter3.Object,
                resultFilter.Object,
            });

            // Act
            await invoker.InvokeAsync();

            // Assert
            actionFilter1.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());
            actionFilter1.Verify(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()), Times.Once());

            actionFilter2.Verify(
                f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()),
                Times.Once());

            resultFilter.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once());
            resultFilter.Verify(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()), Times.Once());

            Assert.True(context.Canceled);
            Assert.Null(context.Result);
        }

        [Fact]
        public async Task InvokeAction_InvokesAsyncActionFilter_ShortCircuit_WithResult_CallNext()
        {
            // Arrange
            var actionFilter = new Mock<IAsyncActionFilter>(MockBehavior.Strict);
            actionFilter
                .Setup(f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()))
                .Returns<ActionExecutingContext, ActionExecutionDelegate>(async (c, next) =>
                {
                    c.Result = new EmptyResult();
                    await next();
                })
                .Verifiable();

            var message =
                "If an IAsyncActionFilter provides a result value by setting the Result property of " +
                "ActionExecutingContext to a non-null value, then it cannot call the next filter by invoking " +
                "ActionExecutionDelegate.";

            var invoker = CreateInvoker(actionFilter.Object);

            // Act & Assert
            await ExceptionAssert.ThrowsAsync<InvalidOperationException>(
                async () => await invoker.InvokeAsync(),
                message);
        }

        [Fact]
        public async Task InvokeAction_InvokesActionFilter_WithExceptionThrownByAction()
        {
            // Arrange
            ActionExecutedContext context = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c =>
                {
                    context = c;

                    // Handle the exception so the test doesn't throw.
                    Assert.False(c.ExceptionHandled);
                    c.ExceptionHandled = true;
                })
                .Verifiable();

            var invoker = CreateInvoker(filter.Object, exception: Exception);

            // Act
            await invoker.InvokeAsync();

            // Assert
            filter.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());
            filter.Verify(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()), Times.Once());

            Assert.Same(Exception, context.Exception);
            Assert.Null(context.Result);
        }

        [Fact]
        public async Task InvokeAction_InvokesActionFilter_WithExceptionThrownByActionFilter()
        {
            // Arrange
            var exception = new DataMisalignedException();
            ActionExecutedContext context = null;

            var filter1 = new Mock<IActionFilter>(MockBehavior.Strict);
            filter1.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter1
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c =>
                {
                    context = c;

                    // Handle the exception so the test doesn't throw.
                    Assert.False(c.ExceptionHandled);
                    c.ExceptionHandled = true;
                })
                .Verifiable();

            var filter2 = new Mock<IActionFilter>(MockBehavior.Strict);
            filter2
                .Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()))
                .Callback<ActionExecutingContext>(c => { throw exception; })
                .Verifiable();

            var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object });

            // Act
            await invoker.InvokeAsync();

            // Assert
            filter1.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());
            filter1.Verify(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()), Times.Once());

            filter2.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());
            filter2.Verify(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()), Times.Never());

            Assert.Same(exception, context.Exception);
            Assert.Null(context.Result);
        }

        [Fact]
        public async Task InvokeAction_InvokesAsyncActionFilter_WithExceptionThrownByActionFilter()
        {
            // Arrange
            var exception = new DataMisalignedException();
            ActionExecutedContext context = null;

            var filter1 = new Mock<IAsyncActionFilter>(MockBehavior.Strict);
            filter1
                .Setup(f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()))
                .Returns<ActionExecutingContext, ActionExecutionDelegate>(async (c, next) =>
                {
                    context = await next();

                    // Handle the exception so the test doesn't throw.
                    Assert.False(context.ExceptionHandled);
                    context.ExceptionHandled = true;
                })
                .Verifiable();

            var filter2 = new Mock<IActionFilter>(MockBehavior.Strict);
            filter2.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter2
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => { throw exception; })
                .Verifiable();

            var invoker = CreateInvoker(new IFilterMetadata[] { filter1.Object, filter2.Object });

            // Act
            await invoker.InvokeAsync();

            // Assert
            filter1.Verify(
                f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()),
                Times.Once());

            filter2.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());

            Assert.Same(exception, context.Exception);
            Assert.Null(context.Result);
        }

        [Fact]
        public async Task InvokeAction_InvokesActionFilter_HandleException()
        {
            // Arrange
            var result = new Mock<IActionResult>(MockBehavior.Strict);
            result
                .Setup(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()))
                .Returns<ActionContext>((context) => Task.FromResult(true))
                .Verifiable();

            var actionFilter = new Mock<IActionFilter>(MockBehavior.Strict);
            actionFilter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            actionFilter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c =>
                {
                    // Handle the exception so the test doesn't throw.
                    Assert.False(c.ExceptionHandled);
                    c.ExceptionHandled = true;

                    c.Result = result.Object;
                })
                .Verifiable();

            var resultFilter = new Mock<IResultFilter>(MockBehavior.Strict);
            resultFilter.Setup(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>())).Verifiable();
            resultFilter.Setup(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>())).Verifiable();

            var invoker = CreateInvoker(
                new IFilterMetadata[] { actionFilter.Object, resultFilter.Object },
                exception: Exception);

            // Act
            await invoker.InvokeAsync();

            // Assert
            actionFilter.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());
            actionFilter.Verify(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()), Times.Once());

            resultFilter.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once());
            resultFilter.Verify(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()), Times.Once());

            result.Verify(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()), Times.Once());
        }

        [Fact]
        public async Task InvokeAction_InvokesAsyncResourceFilter_WithActionResult_FromActionFilter()
        {
            // Arrange
            var expected = Mock.Of<IActionResult>();

            ResourceExecutedContext context = null;
            var resourceFilter = new Mock<IAsyncResourceFilter>(MockBehavior.Strict);
            resourceFilter
                .Setup(f => f.OnResourceExecutionAsync(It.IsAny<ResourceExecutingContext>(), It.IsAny<ResourceExecutionDelegate>()))
                .Returns<ResourceExecutingContext, ResourceExecutionDelegate>(async (c, next) =>
                {
                    context = await next();
                })
                .Verifiable();

            var actionFilter = new Mock<IActionFilter>(MockBehavior.Strict);
            actionFilter
                .Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()))
                .Callback<ActionExecutingContext>((c) =>
                {
                    c.Result = expected;
                });

            var invoker = CreateInvoker(new IFilterMetadata[] { resourceFilter.Object, actionFilter.Object });

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.Same(expected, context.Result);

            resourceFilter.Verify(
                f => f.OnResourceExecutionAsync(It.IsAny<ResourceExecutingContext>(), It.IsAny<ResourceExecutionDelegate>()),
                Times.Once());
        }

        [Fact]
        public async Task InvokeAction_InvokesAsyncResourceFilter_HandleException_FromActionFilter()
        {
            // Arrange
            var expected = new DataMisalignedException();

            ResourceExecutedContext context = null;
            var resourceFilter = new Mock<IAsyncResourceFilter>(MockBehavior.Strict);
            resourceFilter
                .Setup(f => f.OnResourceExecutionAsync(It.IsAny<ResourceExecutingContext>(), It.IsAny<ResourceExecutionDelegate>()))
                .Returns<ResourceExecutingContext, ResourceExecutionDelegate>(async (c, next) =>
                {
                    context = await next();
                    context.ExceptionHandled = true;
                })
                .Verifiable();

            var actionFilter = new Mock<IActionFilter>(MockBehavior.Strict);
            actionFilter
                .Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()))
                .Callback<ActionExecutingContext>((c) =>
                {
                    throw expected;
                });

            var invoker = CreateInvoker(new IFilterMetadata[] { resourceFilter.Object, actionFilter.Object });

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.Same(expected, context.Exception);
            Assert.Same(expected, context.ExceptionDispatchInfo.SourceException);

            resourceFilter.Verify(
                f => f.OnResourceExecutionAsync(It.IsAny<ResourceExecutingContext>(), It.IsAny<ResourceExecutionDelegate>()),
                Times.Once());
        }

        [Fact]
        public async Task InvokeAction_InvokesAsyncResourceFilter_HandlesException_FromExceptionFilter()
        {
            // Arrange
            var expected = new DataMisalignedException();

            ResourceExecutedContext context = null;
            var resourceFilter = new Mock<IAsyncResourceFilter>(MockBehavior.Strict);
            resourceFilter
                .Setup(f => f.OnResourceExecutionAsync(It.IsAny<ResourceExecutingContext>(), It.IsAny<ResourceExecutionDelegate>()))
                .Returns<ResourceExecutingContext, ResourceExecutionDelegate>(async (c, next) =>
                {
                    context = await next();
                    context.ExceptionHandled = true;
                })
                .Verifiable();

            var exceptionFilter = new Mock<IExceptionFilter>(MockBehavior.Strict);
            exceptionFilter
                .Setup(f => f.OnException(It.IsAny<ExceptionContext>()))
                .Callback<ExceptionContext>((c) =>
                {
                    throw expected;
                });

            var invoker = CreateInvoker(new IFilterMetadata[] { resourceFilter.Object, exceptionFilter.Object }, exception: Exception);

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.Same(expected, context.Exception);
            Assert.Same(expected, context.ExceptionDispatchInfo.SourceException);

            resourceFilter.Verify(
                f => f.OnResourceExecutionAsync(It.IsAny<ResourceExecutingContext>(), It.IsAny<ResourceExecutionDelegate>()),
                Times.Once());
        }

        [Fact]
        public async Task InvokeAction_ExceptionBubbling_AsyncActionFilter_To_ResourceFilter()
        {
            // Arrange
            var resourceFilter = new Mock<IAsyncResourceFilter>(MockBehavior.Strict);
            resourceFilter
                .Setup(f => f.OnResourceExecutionAsync(It.IsAny<ResourceExecutingContext>(), It.IsAny<ResourceExecutionDelegate>()))
                .Returns<ResourceExecutingContext, ResourceExecutionDelegate>(async (c, next) =>
                {
                    var context = await next();
                    Assert.Same(Exception, context.Exception);
                    context.ExceptionHandled = true;
                });

            var actionFilter1 = new Mock<IAsyncActionFilter>(MockBehavior.Strict);
            actionFilter1
                .Setup(f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()))
                .Returns<ActionExecutingContext, ActionExecutionDelegate>(async (c, next) =>
                {
                    await next();
                });

            var actionFilter2 = new Mock<IAsyncActionFilter>(MockBehavior.Strict);
            actionFilter2
                .Setup(f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()))
                .Returns<ActionExecutingContext, ActionExecutionDelegate>(async (c, next) =>
                {
                    await next();
                });

            var invoker = CreateInvoker(
                new IFilterMetadata[]
                {
                    resourceFilter.Object,
                    actionFilter1.Object,
                    actionFilter2.Object,
                },
                // The action won't run
                exception: Exception);

            // Act & Assert
            await invoker.InvokeAsync();
        }

        #endregion

        #region Action Method Signatures

        [Fact]
        public async Task InvokeAction_AsyncAction_TaskReturnType()
        {
            // Arrange
            var inputParam1 = 1;
            var inputParam2 = "Second Parameter";
            var actionParameters = new Dictionary<string, object> { { "i", inputParam1 }, { "s", inputParam2 } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.TaskAction), actionParameters);

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.IsType<EmptyResult>(result);
        }

        [Fact]
        public async Task InvokeAction_AsyncAction_TaskOfValueReturnType()
        {
            // Arrange
            var inputParam1 = 1;
            var inputParam2 = "Second Parameter";
            var actionParameters = new Dictionary<string, object> { { "i", inputParam1 }, { "s", inputParam2 } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.TaskValueTypeAction), actionParameters);

            // Act
            await invoker.InvokeAsync();

            // Assert
            var contentResult = Assert.IsType<ObjectResult>(result);
            Assert.Equal(inputParam1, contentResult.Value);
        }

        [Fact]
        public async Task InvokeAction_AsyncAction_WithAsyncKeywordThrows()
        {
            // Arrange
            var inputParam1 = 1;
            var inputParam2 = "Second Parameter";
            var actionParameters = new Dictionary<string, object> { { "i", inputParam1 }, { "s", inputParam2 } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.TaskActionWithException), actionParameters);

            // Act and Assert
            await Assert.ThrowsAsync<NotImplementedException>(
                    () => invoker.InvokeAsync());
        }

        [Fact]
        public async Task InvokeAction_AsyncAction_WithoutAsyncThrows()
        {
            // Arrange
            var inputParam1 = 1;
            var inputParam2 = "Second Parameter";
            var actionParameters = new Dictionary<string, object> { { "i", inputParam1 }, { "s", inputParam2 } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.TaskActionWithExceptionWithoutAsync), actionParameters);

            // Act and Assert
            await Assert.ThrowsAsync<NotImplementedException>(
                    () => invoker.InvokeAsync());
        }

        [Fact]
        public async Task InvokeAction_AsyncAction_WithExceptionsAfterAwait()
        {
            // Arrange
            var inputParam1 = 1;
            var inputParam2 = "Second Parameter";
            var actionParameters = new Dictionary<string, object> { { "i", inputParam1 }, { "s", inputParam2 } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.TaskActionThrowAfterAwait), actionParameters);
            var expectedException = "Argument Exception";

            // Act and Assert
            var ex = await Assert.ThrowsAsync<ArgumentException>(
                () => invoker.InvokeAsync());
            Assert.Equal(expectedException, ex.Message);
        }

        [Fact]
        public async Task InvokeAction_SyncAction()
        {
            // Arrange
            var inputString = "hello";
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.Echo), new Dictionary<string, object>() { { "input", inputString } });

            // Act
            await invoker.InvokeAsync();

            // Assert
            var contentResult = Assert.IsType<ObjectResult>(result);
            Assert.Equal(inputString, contentResult.Value);
        }

        [Fact]
        public async Task InvokeAction_SyncAction_WithException()
        {
            // Arrange
            var inputString = "hello";
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.EchoWithException),
                new Dictionary<string, object>() { { "input", inputString } });

            // Act & Assert
            await Assert.ThrowsAsync<NotImplementedException>(
                () => invoker.InvokeAsync());
        }

        [Fact]
        public async Task InvokeAction_SyncMethod_WithArgumentDictionary_DefaultValueAttributeUsed()
        {
            // Arrange
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.EchoWithDefaultValue),
                new Dictionary<string, object>());

            // Act
            await invoker.InvokeAsync();

            // Assert
            var contentResult = Assert.IsType<ObjectResult>(result);
            Assert.Equal("hello", contentResult.Value);
        }

        [Fact]
        public async Task InvokeAction_SyncMethod_WithArgumentArray_DefaultValueAttributeIgnored()
        {
            // Arrange
            var inputString = "test";
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.EchoWithDefaultValue),
                new Dictionary<string, object>() { { "input", inputString } });

            // Act
            await invoker.InvokeAsync();

            // Assert
            var contentResult = Assert.IsType<ObjectResult>(result);
            Assert.Equal(inputString, contentResult.Value);
        }

        [Fact]
        public async Task InvokeAction_SyncMethod_WithArgumentDictionary_DefaultParameterValueUsed()
        {
            // Arrange
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.EchoWithDefaultValueAndAttribute),
                new Dictionary<string, object>());

            // Act
            await invoker.InvokeAsync();

            // Assert
            var contentResult = Assert.IsType<ObjectResult>(result);
            Assert.Equal("world", contentResult.Value);
        }

        [Fact]
        public async Task InvokeAction_SyncMethod_WithArgumentDictionary_AnyValue_HasPrecedenceOverDefaults()
        {
            // Arrange
            var inputString = "test";
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.EchoWithDefaultValueAndAttribute),
                new Dictionary<string, object>() { { "input", inputString } });

            // Act
            await invoker.InvokeAsync();

            // Assert
            var contentResult = Assert.IsType<ObjectResult>(result);
            Assert.Equal(inputString, contentResult.Value);
        }

        [Fact]
        public async Task InvokeAction_AsyncAction_WithCustomTaskReturnType()
        {
            // Arrange
            var inputParam1 = 1;
            var inputParam2 = "Second Parameter";
            var actionParameters = new Dictionary<string, object> { { "i", inputParam1 }, { "s", inputParam2 } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.TaskActionWithCustomTaskReturnType),
                actionParameters);

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.IsType(typeof(EmptyResult), result);
        }

        [Fact]
        public async Task InvokeAction_AsyncAction_WithCustomTaskOfTReturnType()
        {
            // Arrange
            var inputParam1 = 1;
            var inputParam2 = "Second Parameter";
            var actionParameters = new Dictionary<string, object> { { "i", inputParam1 }, { "s", inputParam2 } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.TaskActionWithCustomTaskOfTReturnType),
                actionParameters);

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.IsType(typeof(ObjectResult), result);
            Assert.IsType(typeof(int), ((ObjectResult)result).Value);
            Assert.Equal(1, ((ObjectResult)result).Value);
        }

        [Fact]
        public async Task InvokeAction_AsyncAction_ReturningUnwrappedTask()
        {
            // Arrange
            var inputParam1 = 1;
            var inputParam2 = "Second Parameter";
            var actionParameters = new Dictionary<string, object> { { "i", inputParam1 }, { "s", inputParam2 } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.UnwrappedTask), actionParameters);

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.IsType<EmptyResult>(result);
        }

        [Fact]
        public async Task InvokeAction_AsyncActionWithTaskOfObjectReturnType_AndReturningTaskOfActionResult()
        {
            // Arrange
            var actionParameters = new Dictionary<string, object> { ["value"] = 3 };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result);

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.AsyncActionMethodReturningActionResultWithTaskOfObjectAsReturnType),
                actionParameters);

            // Act
            await invoker.InvokeAsync();

            // Assert
            var testResult = Assert.IsType<TestActionResult>(result);
            Assert.Equal(3, testResult.Value);
        }

        [Fact]
        public async Task InvokeAction_ActionWithObjectReturnType_AndReturningActionResult()
        {
            // Arrange
            var actionParameters = new Dictionary<string, object> { ["value"] = 3 };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result);

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.ActionMethodReturningActionResultWithObjectAsReturnType),
                actionParameters);

            // Act
            await invoker.InvokeAsync();

            // Assert
            var testResult = Assert.IsType<TestActionResult>(result);
            Assert.Equal(3, testResult.Value);
        }

        [Fact]
        public async Task InvokeAction_AsyncMethod_ParametersInRandomOrder()
        {
            //Arrange
            var inputParam1 = 1;
            var inputParam2 = "Second Parameter";

            // Note that the order of parameters is reversed
            var actionParameters = new Dictionary<string, object> { { "s", inputParam2 }, { "i", inputParam1 } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.TaskValueTypeAction),
                actionParameters);

            // Act
            await invoker.InvokeAsync();

            // Assert
            var contentResult = Assert.IsType<ObjectResult>(result);
            Assert.Equal(inputParam1, contentResult.Value);
        }

        [Theory]
        [InlineData(nameof(TestController.AsynActionMethodWithTestActionResult))]
        [InlineData(nameof(TestController.ActionMethodWithTestActionResult))]
        public async Task InvokeAction_ReturnTypeAsIActionResult_ReturnsExpected(string methodName)
        {
            //Arrange
            var inputParam = 1;
            var actionParameters = new Dictionary<string, object> { { "value", inputParam } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                methodName,
                actionParameters);

            // Act
            await invoker.InvokeAsync();

            // Assert
            var contentResult = Assert.IsType<TestActionResult>(result);
            Assert.Equal(inputParam, contentResult.Value);
        }

        [Fact]
        public async Task InvokeAction_AsyncMethod_InvalidParameterValueThrows()
        {
            //Arrange
            var inputParam2 = "Second Parameter";

            var actionParameters = new Dictionary<string, object> { { "i", "Some Invalid Value" }, { "s", inputParam2 } };
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                nameof(TestController.TaskValueTypeAction),
                actionParameters);

            // Act & Assert
            await Assert.ThrowsAsync<InvalidCastException>(
                () => invoker.InvokeAsync());
        }

        [Theory]
        [InlineData(nameof(TestController.ActionMethodWithNullActionResult), typeof(IActionResult))]
        [InlineData(nameof(TestController.TestActionMethodWithNullActionResult), typeof(TestActionResult))]
        [InlineData(nameof(TestController.AsyncActionMethodWithNullActionResult), typeof(IActionResult))]
        [InlineData(nameof(TestController.AsyncActionMethodWithNullTestActionResult), typeof(TestActionResult))]
        [ReplaceCulture]
        public async Task InvokeAction_WithNullActionResultThrows(string methodName, Type resultType)
        {
            // Arrange
            IActionResult result = null;

            var filter = new Mock<IActionFilter>(MockBehavior.Strict);
            filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
            filter
                .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
                .Callback<ActionExecutedContext>(c => result = c.Result)
                .Verifiable();

            var invoker = CreateInvoker(
                new[] { filter.Object },
                methodName,
                new Dictionary<string, object>());

            // Act & Assert
            await ExceptionAssert.ThrowsAsync<InvalidOperationException>(
                () => invoker.InvokeAsync(),
                $"Cannot return null from an action method with a return type of '{resultType}'.");
        }

        [Fact]
        public async Task Invoke_UsesDefaultValuesIfNotBound()
        {
            // Arrange
            var actionDescriptor = new ControllerActionDescriptor
            {
                ControllerTypeInfo = typeof(TestController).GetTypeInfo(),
                BoundProperties = new List<ParameterDescriptor>(),
                MethodInfo = typeof(TestController).GetTypeInfo()
                    .DeclaredMethods
                    .First(m => m.Name.Equals("ActionMethodWithDefaultValues", StringComparison.Ordinal)),

                Parameters = new List<ParameterDescriptor>
                {
                    new ParameterDescriptor
                    {
                        Name = "value",
                        ParameterType = typeof(int),
                        BindingInfo = new BindingInfo(),
                    }
                },
                FilterDescriptors = new List<FilterDescriptor>()
            };

            var context = new Mock<HttpContext>();
            context.SetupGet(c => c.Items)
                .Returns(new Dictionary<object, object>());
            context.Setup(c => c.RequestServices.GetService(typeof(ILoggerFactory)))
                .Returns(new NullLoggerFactory());

            var actionContext = new ActionContext(context.Object, new RouteData(), actionDescriptor);

            var controllerContext = new ControllerContext(actionContext)
            {
                ValueProviderFactories = new IValueProviderFactory[0]
            };
            controllerContext.ModelState.MaxAllowedErrors = 200;
            var objectMethodExecutor = ObjectMethodExecutor.Create(
                actionDescriptor.MethodInfo,
                actionDescriptor.ControllerTypeInfo,
                ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo));

            var cacheEntry = new ControllerActionInvokerCacheEntry(
                new FilterItem[0],
                _ => new TestController(),
                (_, __) => { },
                (_, __, ___) => Task.CompletedTask,
                actionMethodExecutor: objectMethodExecutor);

            var invoker = new ControllerActionInvoker(
                new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(),
                new DiagnosticListener("Microsoft.AspNetCore"),
                controllerContext,
                cacheEntry,
                new IFilterMetadata[0]);

            // Act
            await invoker.InvokeAsync();

            // Assert
            Assert.Equal(5, context.Object.Items["Result"]);
        }

        #endregion

        protected override ResourceInvoker CreateInvoker(
            IFilterMetadata[] filters,
            Exception exception = null,
            IActionResult result = null,
            IList<IValueProviderFactory> valueProviderFactories = null)
        {
            var actionDescriptor = new ControllerActionDescriptor()
            {
                ControllerTypeInfo = typeof(TestController).GetTypeInfo(),
                FilterDescriptors = new List<FilterDescriptor>(),
                Parameters = new List<ParameterDescriptor>(),
                BoundProperties = new List<ParameterDescriptor>(),
            };

            if (result == Result)
            {
                actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod));
            }
            else if (result != null)
            {
                throw new InvalidOperationException($"Unexpected action result {result}.");
            }
            else if (exception == Exception)
            {
                actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ThrowingActionMethod));
            }
            else if (exception != null)
            {
                throw new InvalidOperationException($"Unexpected exception {exception}.");
            }
            else
            {
                actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod));
            }

            return CreateInvoker(
                filters,
                actionDescriptor,
                new TestController(),
                valueProviderFactories: valueProviderFactories);
        }

        // Used by tests which directly test different types of signatures for controller methods.
        private ControllerActionInvoker CreateInvoker(
            IFilterMetadata[] filters,
            string methodName,
            IDictionary<string, object> arguments)
        {
            var actionDescriptor = new ControllerActionDescriptor()
            {
                ControllerTypeInfo = typeof(TestController).GetTypeInfo(),
                FilterDescriptors = new List<FilterDescriptor>(),
                Parameters = new List<ParameterDescriptor>(),
                BoundProperties = new List<ParameterDescriptor>(),
            };

            var method = typeof(TestController).GetTypeInfo().GetMethod(methodName);
            Assert.NotNull(method);
            actionDescriptor.MethodInfo = method;

            foreach (var kvp in arguments)
            {
                actionDescriptor.Parameters.Add(new ControllerParameterDescriptor()
                {
                    Name = kvp.Key,
                    ParameterInfo = method.GetParameters().Where(p => p.Name == kvp.Key).Single(),
                });
            }

            return CreateInvoker(filters, actionDescriptor, new TestController(), arguments);
        }

        private ControllerActionInvoker CreateInvoker(
            IFilterMetadata[] filters,
            ControllerActionDescriptor actionDescriptor,
            object controller,
            IDictionary<string, object> arguments = null,
            IList<IValueProviderFactory> valueProviderFactories = null,
            RouteData routeData = null,
            ILogger logger = null,
            object diagnosticListener = null)
        {
            Assert.NotNull(actionDescriptor.MethodInfo);

            if (arguments == null)
            {
                arguments = new Dictionary<string, object>();
            }

            if (valueProviderFactories == null)
            {
                valueProviderFactories = new List<IValueProviderFactory>();
            }

            if (routeData == null)
            {
                routeData = new RouteData();
            }

            if (logger == null)
            {
                logger = new NullLoggerFactory().CreateLogger<ControllerActionInvoker>();
            }

            var httpContext = new DefaultHttpContext();
            var options = new MvcOptions();
            var mvcOptionsAccessor = new TestOptionsManager<MvcOptions>(options);

            var services = new ServiceCollection();
            services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
            services.AddSingleton<IOptions<MvcOptions>>(mvcOptionsAccessor);
            services.AddSingleton(new ObjectResultExecutor(
                mvcOptionsAccessor,
                new TestHttpResponseStreamWriterFactory(),
                NullLoggerFactory.Instance));

            httpContext.Response.Body = new MemoryStream();
            httpContext.RequestServices = services.BuildServiceProvider();

            var formatter = new Mock<IOutputFormatter>();
            formatter
                .Setup(f => f.CanWriteResult(It.IsAny<OutputFormatterCanWriteContext>()))
                .Returns(true);

            formatter
                .Setup(f => f.WriteAsync(It.IsAny<OutputFormatterWriteContext>()))
                .Returns<OutputFormatterWriteContext>(async c =>
                {
                    await c.HttpContext.Response.WriteAsync(c.Object.ToString());
                });

            options.OutputFormatters.Add(formatter.Object);

            var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
            if (diagnosticListener != null)
            {
                diagnosticSource.SubscribeWithAdapter(diagnosticListener);
            }

            var objectMethodExecutor = ObjectMethodExecutor.Create(
                actionDescriptor.MethodInfo,
                actionDescriptor.ControllerTypeInfo,
                ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo));

            var cacheEntry = new ControllerActionInvokerCacheEntry(
                new FilterItem[0],
                (c) => controller,
                null,
                (_, __, args) =>
                {
                    foreach (var item in arguments)
                    {
                        args[item.Key] = item.Value;
                    }

                    return Task.CompletedTask;
                },
                objectMethodExecutor);

            var actionContext = new ActionContext(httpContext, routeData, actionDescriptor);
            var controllerContext = new ControllerContext(actionContext)
            {
                ValueProviderFactories = valueProviderFactories,
            };

            var invoker = new ControllerActionInvoker(
                logger,
                diagnosticSource,
                controllerContext,
                cacheEntry,
                filters);
            return invoker;
        }

        public sealed class TestController
        {
            public IActionResult ActionMethod()
            {
                return Result;
            }

            public ObjectResult ThrowingActionMethod()
            {
                throw Exception;
            }

            public IActionResult ActionMethodWithDefaultValues(int value = 5)
            {
                return new TestActionResult { Value = value };
            }

            public TestActionResult ActionMethodWithTestActionResult(int value)
            {
                return new TestActionResult { Value = value };
            }

            public async Task<TestActionResult> AsynActionMethodWithTestActionResult(int value)
            {
                return await Task.FromResult<TestActionResult>(new TestActionResult { Value = value });
            }

            public IActionResult ActionMethodWithNullActionResult()
            {
                return null;
            }

            public object ActionMethodReturningActionResultWithObjectAsReturnType(int value = 5)
            {
                return new TestActionResult { Value = value };
            }

            public async Task<object> AsyncActionMethodReturningActionResultWithTaskOfObjectAsReturnType(int value = 5)
            {
                return await Task.FromResult(new TestActionResult { Value = value });
            }

            public TestActionResult TestActionMethodWithNullActionResult()
            {
                return null;
            }

            public async Task<IActionResult> AsyncActionMethodWithNullActionResult()
            {
                return await Task.FromResult<IActionResult>(null);
            }

            public async Task<TestActionResult> AsyncActionMethodWithNullTestActionResult()
            {
                return await Task.FromResult<TestActionResult>(null);
            }
#pragma warning disable 1998
            public async Task TaskAction(int i, string s)
            {
                return;
            }
#pragma warning restore 1998

#pragma warning disable 1998
            public async Task<int> TaskValueTypeAction(int i, string s)
            {
                return i;
            }
#pragma warning restore 1998

#pragma warning disable 1998
            public async Task<Task<int>> TaskOfTaskAction(int i, string s)
            {
                return TaskValueTypeAction(i, s);
            }
#pragma warning restore 1998

            public Task<int> TaskValueTypeActionWithoutAsync(int i, string s)
            {
                return TaskValueTypeAction(i, s);
            }

#pragma warning disable 1998
            public async Task<int> TaskActionWithException(int i, string s)
            {
                throw new NotImplementedException("Not Implemented Exception");
            }
#pragma warning restore 1998

            public Task<int> TaskActionWithExceptionWithoutAsync(int i, string s)
            {
                throw new NotImplementedException("Not Implemented Exception");
            }

            public async Task<int> TaskActionThrowAfterAwait(int i, string s)
            {
                await Task.Delay(500);
                throw new ArgumentException("Argument Exception");
            }

            public TaskDerivedType TaskActionWithCustomTaskReturnType(int i, string s)
            {
                var task = new TaskDerivedType();
                task.Start();
                return task;
            }

            public TaskOfTDerivedType<int> TaskActionWithCustomTaskOfTReturnType(int i, string s)
            {
                var task = new TaskOfTDerivedType<int>(1);
                task.Start();
                return task;
            }

            /// <summary>
            /// Returns a <see cref="Task{TResult}"/> instead of a <see cref="Task"/>.
            /// </summary>
            public Task UnwrappedTask(int i, string s)
            {
                return Task.Factory.StartNew(async () => await Task.Factory.StartNew(() => i));
            }

            public string Echo(string input)
            {
                return input;
            }

            public string EchoWithException(string input)
            {
                throw new NotImplementedException();
            }

            public string EchoWithDefaultValue([DefaultValue("hello")] string input)
            {
                return input;
            }

            public string EchoWithDefaultValueAndAttribute([DefaultValue("hello")] string input = "world")
            {
                return input;
            }

            public class TaskDerivedType : Task
            {
                public TaskDerivedType()
                    : base(() => Console.WriteLine("In The Constructor"))
                {
                }
            }

            public class TaskOfTDerivedType<T> : Task<T>
            {
                public TaskOfTDerivedType(T input)
                    : base(() => input)
                {
                }
            }
        }

        public sealed class TestActionResult : IActionResult
        {
            public int Value { get; set; }

            public Task ExecuteResultAsync(ActionContext context)
            {
                context.HttpContext.Items["Result"] = Value;
                return Task.FromResult(0);
            }
        }

        private static ObjectMethodExecutor CreateExecutor(ControllerActionDescriptor actionDescriptor)
        {
            return ObjectMethodExecutor.Create(
                actionDescriptor.MethodInfo,
                actionDescriptor.ControllerTypeInfo,
                ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo));
        }
    }
}
