// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Controllers
{
    public class DefaultControllerFactoryTest
    {
        [Fact]
        public void CreateController_UsesControllerActivatorToInstantiateController()
        {
            // Arrange
            var expected = new MyController();
            var actionDescriptor = new ControllerActionDescriptor
            {
                ControllerTypeInfo = typeof(MyController).GetTypeInfo()
            };

            var context = new ControllerContext()
            {
                ActionDescriptor = actionDescriptor,
                HttpContext = new DefaultHttpContext()
                {
                    RequestServices = GetServices(),
                },
            };

            var activator = new Mock<IControllerActivator>();
            activator.Setup(a => a.Create(context))
                     .Returns(expected)
                     .Verifiable();

            var controllerFactory = CreateControllerFactory(activator.Object);

            // Act
            var result = controllerFactory.CreateController(context);

            // Assert
            var controller = Assert.IsType<MyController>(result);
            Assert.Same(expected, controller);
            activator.Verify();
        }

        [Fact]
        public void CreateController_SetsPropertiesFromActionContextHierarchy()
        {
            // Arrange
            var actionDescriptor = new ControllerActionDescriptor
            {
                ControllerTypeInfo = typeof(ControllerWithAttributes).GetTypeInfo()
            };

            var context = new ControllerContext()
            {
                ActionDescriptor = actionDescriptor,
                HttpContext = new DefaultHttpContext()
                {
                    RequestServices = GetServices(),
                },
            };
            var factory = CreateControllerFactory(new DefaultControllerActivator(new TypeActivatorCache()));

            // Act
            var result = factory.CreateController(context);

            // Assert
            var controller = Assert.IsType<ControllerWithAttributes>(result);
            Assert.Same(context, controller.ActionContext);
        }

        [Fact]
        public void CreateController_SetsControllerContext()
        {
            // Arrange
            var actionDescriptor = new ControllerActionDescriptor
            {
                ControllerTypeInfo = typeof(ControllerWithAttributes).GetTypeInfo()
            };

            var context = new ControllerContext()
            {
                ActionDescriptor = actionDescriptor,
                HttpContext = new DefaultHttpContext()
                {
                    RequestServices = GetServices(),
                },
            };
            var factory = CreateControllerFactory(new DefaultControllerActivator(new TypeActivatorCache()));

            // Act
            var result = factory.CreateController(context);

            // Assert
            var controller = Assert.IsType<ControllerWithAttributes>(result);
            Assert.Same(context, controller.ControllerContext);
        }

        [Fact]
        public void CreateController_IgnoresPropertiesThatAreNotDecoratedWithAttribute()
        {
            // Arrange
            var actionDescriptor = new ControllerActionDescriptor
            {
                ControllerTypeInfo = typeof(ControllerWithoutAttributes).GetTypeInfo()
            };

            var context = new ControllerContext()
            {
                ActionDescriptor = actionDescriptor,
                HttpContext = new DefaultHttpContext()
                {
                    RequestServices = GetServices(),
                },
            };
            var factory = CreateControllerFactory(new DefaultControllerActivator(new TypeActivatorCache()));

            // Act
            var result = factory.CreateController(context);

            // Assert
            var controller = Assert.IsType<ControllerWithoutAttributes>(result);
            Assert.Null(controller.ActionContext);
        }

        [Fact]
        public void CreateController_IgnoresNonPublicProperties()
        {
            // Arrange
            var actionDescriptor = new ControllerActionDescriptor
            {
                ControllerTypeInfo = typeof(ControllerWithNonVisibleProperties).GetTypeInfo()
            };

            var context = new ControllerContext()
            {
                ActionDescriptor = actionDescriptor,
                HttpContext = new DefaultHttpContext()
                {
                    RequestServices = GetServices(),
                },
            };
            var factory = CreateControllerFactory(new DefaultControllerActivator(new TypeActivatorCache()));

            // Act
            var result = factory.CreateController(context);

            // Assert
            var controller = Assert.IsType<ControllerWithNonVisibleProperties>(result);
            Assert.Null(controller.ActionContext);
            Assert.Null(controller.ControllerContext);
        }

        [Fact]
        public void CreateController_ThrowsIConstructorCannotBeActivated()
        {
            // Arrange
            var actionDescriptor = new ControllerActionDescriptor
            {
                ControllerTypeInfo = typeof(ControllerThatCannotBeActivated).GetTypeInfo()
            };

            var context = new ControllerContext()
            {
                ActionDescriptor = actionDescriptor,
                HttpContext = new DefaultHttpContext()
                {
                    RequestServices = GetServices(),
                },
            };
            var factory = CreateControllerFactory(new DefaultControllerActivator(new TypeActivatorCache()));

            // Act and Assert
            var exception = Assert.Throws<InvalidOperationException>(() => factory.CreateController(context));
            Assert.Equal(
                $"Unable to resolve service for type '{typeof(TestService).FullName}' while attempting to activate " +
                $"'{typeof(ControllerThatCannotBeActivated).FullName}'.",
                exception.Message);
        }

        [Fact]
        public void DefaultControllerFactory_DelegatesDisposalToControllerActivator()
        {
            // Arrange
            var activatorMock = new Mock<IControllerActivator>();
            activatorMock.Setup(s => s.Release(It.IsAny<ControllerContext>(), It.IsAny<object>()));

            var factory = CreateControllerFactory(activatorMock.Object);
            var controller = new MyController();

            // Act + Assert
            factory.ReleaseController(new ControllerContext(), controller);

            activatorMock.Verify();
        }

        private IServiceProvider GetServices()
        {
            var metadataProvider = new EmptyModelMetadataProvider();
            var services = new Mock<IServiceProvider>();
            services
                .Setup(s => s.GetService(typeof(IUrlHelper)))
                .Returns(Mock.Of<IUrlHelper>());
            services
                .Setup(s => s.GetService(typeof(IModelMetadataProvider)))
                .Returns(metadataProvider);
            services
                .Setup(s => s.GetService(typeof(IObjectModelValidator)))
                .Returns(new DefaultObjectValidator(
                    metadataProvider,
                    TestModelValidatorProvider.CreateDefaultProvider().ValidatorProviders,
                    new MvcOptions()));
            return services.Object;
        }

        private static DefaultControllerFactory CreateControllerFactory(IControllerActivator controllerActivator = null)
        {
            var activatorMock = new Mock<IControllerActivator>();

            controllerActivator = controllerActivator ?? activatorMock.Object;
            var propertyActivators = new IControllerPropertyActivator[]
            {
                new DefaultControllerPropertyActivator(),
            };

            return new DefaultControllerFactory(controllerActivator, propertyActivators);
        }

        private class ControllerWithoutAttributes
        {
            public ActionContext ActionContext { get; set; }

            public ControllerContext ControllerContext { get; set; }
        }

        public class ControllerWithNonVisibleProperties
        {
            internal ActionContext ActionContext { get; set; }

            public ControllerContext ControllerContext { get; private set; }
        }

        private class ControllerWithAttributes
        {
            [ActionContext]
            public ActionContext ActionContext { get; set; }

            [ControllerContext]
            public ControllerContext ControllerContext { get; set; }
        }

        private class MyController : IDisposable
        {
            public bool Disposed { get; set; }

            public void Dispose()
            {
                Disposed = true;
            }
        }

        private class ControllerThatCannotBeActivated
        {
            public ControllerThatCannotBeActivated(TestService service)
            {
                Service = service;
            }

            public TestService Service { get; }
        }

        private class TestService
        {
        }
    }
}
