// 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.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
    public class ExecutorFactoryTest
    {
        [Fact]
        public async Task CreateExecutor_ForActionResultMethod()
        {
            // Arrange
            var handler = new HandlerMethodDescriptor()
            {
                MethodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ActionResultReturningHandler)),
                Parameters = new HandlerParameterDescriptor[0],
            };

            // Act
            var executor = ExecutorFactory.CreateExecutor(handler);

            // Assert
            Assert.NotNull(executor);
            var actionResultTask = executor(new TestPage(), null);
            var actionResult = await actionResultTask;
            Assert.IsType<EmptyResult>(actionResult);
        }

        [Fact]
        public async Task CreateExecutor_ForMethodReturningConcreteSubtypeOfIActionResult()
        {
            // Arrange
            var handler = new HandlerMethodDescriptor()
            {
                MethodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ConcreteActionResult)),
                Parameters = new HandlerParameterDescriptor[0],
            };

            // Act
            var executor = ExecutorFactory.CreateExecutor(handler);

            // Assert
            Assert.NotNull(executor);
            var actionResultTask = executor(new TestPage(), null);
            var actionResult = await actionResultTask;
            Assert.IsType<ViewResult>(actionResult);
        }

        [Fact]
        public async Task CreateExecutor_ForActionResultReturningMethod_WithParameters()
        {
            // Arrange
            var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ActionResultReturnHandlerWithParameters));
            var handler = new HandlerMethodDescriptor()
            {
                MethodInfo = methodInfo,
                Parameters = CreateParameters(methodInfo),
            };

            // Act
            var executor = ExecutorFactory.CreateExecutor(handler);

            // Assert
            Assert.NotNull(executor);
            var actionResultTask = executor(new TestPage(), CreateArguments(methodInfo));
            var actionResult = await actionResultTask;
            var contentResult = Assert.IsType<ContentResult>(actionResult);
            Assert.Equal("Hello 0", contentResult.Content);
        }

        [Fact]
        public async Task CreateExecutor_ForVoidReturningMethod()
        {
            // Arrange
            var handler = new HandlerMethodDescriptor()
            {
                MethodInfo = typeof(TestPage).GetMethod(nameof(TestPage.VoidReturningHandler)),
                Parameters = new HandlerParameterDescriptor[0],
            };

            var page = new TestPage();

            // Act
            var executor = ExecutorFactory.CreateExecutor(handler);

            // Assert
            Assert.NotNull(executor);
            var actionResultTask = executor(page, null);
            var actionResult = await actionResultTask;
            Assert.Null(actionResult);
            Assert.True(page.SideEffects);
        }

        [Fact]
        public async Task CreateExecutor_ForVoidTaskReturningMethod()
        {
            // Arrange
            var handler = new HandlerMethodDescriptor()
            {
                MethodInfo = typeof(TestPage).GetMethod(nameof(TestPage.VoidTaskReturningHandler)),
                Parameters = new HandlerParameterDescriptor[0],
            };

            var page = new TestPage();

            // Act
            var executor = ExecutorFactory.CreateExecutor(handler);

            // Assert
            Assert.NotNull(executor);
            var actionResultTask = executor(page, null);
            var actionResult = await actionResultTask;
            Assert.Null(actionResult);
            Assert.True(page.SideEffects);
        }

        [Fact]
        public async Task CreateExecutor_ForTaskOfIActionResultReturningMethod()
        {
            // Arrange
            var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.GenericTaskHandler));
            var handler = new HandlerMethodDescriptor()
            {
                MethodInfo = methodInfo,
                Parameters = CreateParameters(methodInfo),
            };

            // Act
            var executor = ExecutorFactory.CreateExecutor(handler);

            // Assert
            Assert.NotNull(executor);
            var actionResultTask = executor(new TestPage(), null);
            var actionResult = await actionResultTask;
            Assert.IsType<EmptyResult>(actionResult);
        }

        [Fact]
        public async Task CreateExecutor_ForTaskOfConcreteActionResultReturningMethod()
        {
            // Arrange
            var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.TaskReturningConcreteSubtype));
            var handler = new HandlerMethodDescriptor()
            {
                MethodInfo = methodInfo,
                Parameters = CreateParameters(methodInfo),
            };

            // Act
            var executor = ExecutorFactory.CreateExecutor(handler);

            // Assert
            Assert.NotNull(executor);
            var actionResultTask = executor(new TestPage(), CreateArguments(methodInfo));
            var actionResult = await actionResultTask;
            var contentResult = Assert.IsType<ContentResult>(actionResult);
            Assert.Equal("value", contentResult.Content);
        }

        [Theory]
        [InlineData(nameof(TestPageModel.StringResult))]
        [InlineData(nameof(TestPageModel.TaskOfObject))]
        [InlineData(nameof(TestPageModel.ViewComponent))]
        public void CreateExecutor_ThrowsIfTypeIsNotAValidReturnType(string methodName)
        {
            // Arrange
            var methodInfo = typeof(TestPageModel).GetMethod(methodName);
            var handler = new HandlerMethodDescriptor()
            {
                MethodInfo = methodInfo,
                Parameters = CreateParameters(methodInfo),
            };

            // Act & Assert
            var ex = Assert.Throws<InvalidOperationException>(() => ExecutorFactory.CreateExecutor(handler));
            Assert.Equal($"Unsupported handler method return type '{methodInfo.ReturnType}'.", ex.Message);
        }

        private static object[] CreateArguments(MethodInfo methodInfo)
        {
            var parameters = methodInfo.GetParameters();

            return parameters.Select(s => GetDefaultValue(s)).ToArray();
        }

        private static object GetDefaultValue(ParameterInfo methodParameter)
        {
            object defaultValue = null;
            if (methodParameter.HasDefaultValue)
            {
                defaultValue = methodParameter.DefaultValue;
            }
            else if (methodParameter.ParameterType.GetTypeInfo().IsValueType)
            {
                defaultValue = Activator.CreateInstance(methodParameter.ParameterType);
            }

            return defaultValue;
        }

        private static HandlerParameterDescriptor[] CreateParameters(MethodInfo methodInfo)
        {
            var parameters = methodInfo.GetParameters();

            return parameters.Select(p => new HandlerParameterDescriptor()
            {
                BindingInfo = BindingInfo.GetBindingInfo(p.GetCustomAttributes()),
                Name = p.Name,
                ParameterInfo = p,
                ParameterType = p.ParameterType,
            }).ToArray();
        }

        private class TestPage : Page
        {
            public TestPage()
            {
            }

            public bool SideEffects { get; private set; }

            public IActionResult ActionResultReturningHandler() => new EmptyResult();

            public IActionResult ActionResultReturnHandlerWithParameters(int arg1, string arg2 = "Hello")
            {
                return new ContentResult
                {
                    Content = $"{arg2} {arg1}",
                };
            }

            public ViewResult ConcreteActionResult() => new ViewResult();

            public void VoidReturningHandler()
            {
                SideEffects = true;
            }

            public async Task VoidTaskReturningHandler()
            {
                await Task.Run(() =>
                {
                    SideEffects = true;
                });
            }

            public Task<IActionResult> GenericTaskHandler() => Task.FromResult<IActionResult>(new EmptyResult());

            public Task<ContentResult> TaskReturningConcreteSubtype(string arg = "value")
            {
                return Task.FromResult(new ContentResult
                {
                    Content = arg,
                });
            }


            public override Task ExecuteAsync()
            {
                throw new NotImplementedException();
            }
        }

        private class TestPageModel
        {
            public bool SideEffects { get; private set; }

            public IActionResult ActionResultReturningHandler() => new EmptyResult();

            public IActionResult ActionResultReturnHandlerWithParameters(int arg1, string arg2 = "Hello")
            {
                return new ContentResult
                {
                    Content = $"{arg2} {arg1}",
                };
            }

            public ViewResult ConcreteActionResult() => new ViewResult();

            public void VoidReturningHandler()
            {
                SideEffects = true;
            }

            public async Task VoidTaskReturningHandler()
            {
                await Task.Run(() =>
                {
                    SideEffects = true;
                });
            }

            public Task<IActionResult> GenericTaskHandler() => Task.FromResult<IActionResult>(new EmptyResult());

            public Task<ContentResult> TaskReturningConcreteSubtype(string arg = "value")
            {
                return Task.FromResult(new ContentResult
                {
                    Content = arg,
                });
            }

            public string StringResult() => "";

            public Task<object> TaskOfObject() => Task.FromResult(new object());

            public IViewComponentResult ViewComponent() => new ViewViewComponentResult();
        }

        private class EmptyPage : Page
        {
            public EmptyPage()
            {
            }

            public override Task ExecuteAsync()
            {
                throw new NotImplementedException();
            }
        }
    }
}
