// 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.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.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.Infrastructure
{
    public class ControllerActionInvokerTest : CommonResourceInvokerTest
    {
        #region Diagnostics

        [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<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<ObjectResult>(result);
            Assert.IsType<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.AsyncActionMethodWithTestActionResult))]
        [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 controllerMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);

            var cacheEntry = new ControllerActionInvokerCacheEntry(
                new FilterItem[0],
                _ => new TestController(),
                (_, __) => default,
                (_, __, ___) => Task.CompletedTask,
                objectMethodExecutor,
                controllerMethodExecutor);

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

            // Act
            await invoker.InvokeAsync();

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

        [Fact]
        public async Task InvokeAction_ConvertibleToActionResult()
        {
            // Arrange
            var inputParam = 12;
            var actionParameters = new Dictionary<string, object> { { "input", 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 }, nameof(TestController.ActionReturningConvertibleToActionResult), actionParameters);

            // Act
            await invoker.InvokeAsync();

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

        [Fact]
        public async Task InvokeAction_AsyncAction_ConvertibleToActionResult()
        {
            // Arrange
            var inputParam = 13;
            var actionParameters = new Dictionary<string, object> { { "input", 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 }, nameof(TestController.ActionReturningConvertibleToActionResultAsync), actionParameters);

            // Act
            await invoker.InvokeAsync();

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

        [Fact]
        public async Task InvokeAction_ConvertibleToActionResult_AsObject()
        {
            // Arrange
            var actionParameters = new Dictionary<string, object>();
            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.ActionReturningConvertibleAsObject), actionParameters);

            // Act
            await invoker.InvokeAsync();

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

        [Fact]
        public async Task InvokeAction_ConvertibleToActionResult_ReturningNull_Throws()
        {
            // Arrange
            var expectedMessage = @"Cannot return null from an action method with a return type of 'Microsoft.AspNetCore.Mvc.Infrastructure.IConvertToActionResult'.";
            var actionParameters = new Dictionary<string, object>();
            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.ConvertibleToActionResultReturningNull), actionParameters);

            // Act & Assert
            var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => invoker.InvokeAsync());
            Assert.Equal(expectedMessage, exception.Message);
        }

        #endregion

        #region Logs

        [Fact]
        public async Task InvokeAsync_Logs()
        {
            // Arrange
            var testSink = new TestSink();
            var loggerFactory = new TestLoggerFactory(testSink, enabled: true);
            var logger = loggerFactory.CreateLogger("test");

            var actionDescriptor = new ControllerActionDescriptor()
            {
                ControllerTypeInfo = typeof(TestController).GetTypeInfo(),
                FilterDescriptors = new List<FilterDescriptor>(),
                Parameters = new List<ParameterDescriptor>(),
                BoundProperties = new List<ParameterDescriptor>(),
                MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod)),
            };

            var invoker = CreateInvoker(
                new IFilterMetadata[0],
                actionDescriptor,
                new TestController(),
                logger: logger);

            // Act
            await invoker.InvokeAsync();

            // Assert
            var messages = testSink.Writes.Select(write => write.State.ToString()).ToList();
            var actionSignature = $"{typeof(IActionResult).FullName} {nameof(TestController.ActionMethod)}()";
            var controllerName = $"{typeof(ControllerActionInvokerTest).FullName}+{nameof(TestController)} ({typeof(ControllerActionInvokerTest).Assembly.GetName().Name})";
            var actionName = $"{typeof(ControllerActionInvokerTest).FullName}+{nameof(TestController)}.{nameof(TestController.ActionMethod)} ({typeof(ControllerActionInvokerTest).Assembly.GetName().Name})";
            var actionResultName = $"{typeof(CommonResourceInvokerTest).FullName}+{nameof(TestResult)}";

            Assert.Collection(
                messages,
                m => Assert.Equal($"Route matched with {{}}. Executing controller action with signature {actionSignature} on controller {controllerName}.", m),
                m => Assert.Equal("Execution plan of authorization filters (in the following order): None", m),
                m => Assert.Equal("Execution plan of resource filters (in the following order): None", m),
                m => Assert.Equal("Execution plan of action filters (in the following order): None", m),
                m => Assert.Equal("Execution plan of exception filters (in the following order): None", m),
                m => Assert.Equal("Execution plan of result filters (in the following order): None", m),
                m => Assert.Equal($"Executing controller factory for controller {controllerName}", m),
                m => Assert.Equal($"Executed controller factory for controller {controllerName}", m),
                m => Assert.Equal($"Executing action method {actionName} - Validation state: Valid", m),
                m => Assert.StartsWith($"Executed action method {actionName}, returned result {actionResultName} in ", m),
                m => Assert.Equal($"Before executing action result {actionResultName}.", m),
                m => Assert.Equal($"After executing action result {actionResultName}.", m),
                m => Assert.StartsWith($"Executed action {actionName} in ", m));
        }

        #endregion

        protected override IActionInvoker 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 = Options.Create(new MvcOptions());

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

            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.Value.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 actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);

            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,
                actionMethodExecutor);

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

            var invoker = new ControllerActionInvoker(
                logger,
                diagnosticSource,
                ActionContextAccessor.Null,
                new ActionResultTypeMapper(),
                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> AsyncActionMethodWithTestActionResult(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 ConvertibleToActionResult ActionReturningConvertibleToActionResult(int input)
                => new ConvertibleToActionResult { Value = input };

            public Task<ConvertibleToActionResult> ActionReturningConvertibleToActionResultAsync(int input)
                => Task.FromResult(new ConvertibleToActionResult { Value = input });

            public object ActionReturningConvertibleAsObject() => new ConvertibleToActionResult();

            public IConvertToActionResult ConvertibleToActionResultReturningNull()
            {
                var mock = new Mock<IConvertToActionResult>();
                mock.Setup(m => m.Convert()).Returns((IActionResult)null);

                return mock.Object;
            }

            public class TaskDerivedType : Task
            {
                public TaskDerivedType()
                    : base(() => { })
                {
                }
            }

            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));
        }

        public class ConvertibleToActionResult : IConvertToActionResult
        {
            public int Value { get; set; }

            public IActionResult Convert() => new TestActionResult { Value = Value };
        }
    }
}
