// 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.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Controllers
{
    public class DefaultControllerActivatorTest
    {
        [Theory]
        [InlineData(typeof(TypeDerivingFromController))]
        [InlineData(typeof(PocoType))]
        public void Create_CreatesInstancesOfTypes(Type type)
        {
            // Arrange
            var activator = new DefaultControllerActivator(new TypeActivatorCache());
            var serviceProvider = new Mock<IServiceProvider>(MockBehavior.Strict);

            var httpContext = new DefaultHttpContext
            {
                RequestServices = serviceProvider.Object
            };

            var context = new ControllerContext(
                new ActionContext(
                    httpContext,
                    new RouteData(),
                    new ControllerActionDescriptor
                    {
                        ControllerTypeInfo = type.GetTypeInfo()
                    }));

            // Act
            var instance = activator.Create(context);

            // Assert
            Assert.IsType(type, instance);
        }

        [Fact]
        public void Release_DisposesController_IfDisposable()
        {
            // Arrange
            var controller = new MyController();
            var activator = new DefaultControllerActivator(Mock.Of<ITypeActivatorCache>());

            // Act
            activator.Release(new ControllerContext(), controller);

            // Assert
            Assert.True(controller.Disposed);
        }

        [Fact]
        public async Task ReleaseAsync_AsynchronouslyDisposesController_IfAsyncDisposableAsync()
        {
            // Arrange
            var controller = new MyAsyncDisposableController();
            var activator = new DefaultControllerActivator(Mock.Of<ITypeActivatorCache>());

            // Act
            await activator.ReleaseAsync(new ControllerContext(), controller);

            // Assert
            Assert.True(controller.Disposed);
        }

        [Fact]
        public async Task ReleaseAsync_SynchronouslyDisposesController_IfDisposableAsync()
        {
            // Arrange
            var controller = new MyController();
            var activator = new DefaultControllerActivator(Mock.Of<ITypeActivatorCache>());

            // Act
            await activator.ReleaseAsync(new ControllerContext(), controller);

            // Assert
            Assert.True(controller.Disposed);
        }

        [Fact]
        public async Task ReleaseAsync_SynchronouslyDisposesController_PrefersDisposeAsyncOverDispose()
        {
            // Arrange
            var controller = new MyDisposableAndAsyncDisposableController();
            var activator = new DefaultControllerActivator(Mock.Of<ITypeActivatorCache>());

            // Act
            await activator.ReleaseAsync(new ControllerContext(), controller);

            // Assert
            Assert.False(controller.SyncDisposed);
            Assert.True(controller.AsyncDisposed);
        }

        [Fact]
        public void DefaultControllerActivator_ReleasesNonIDisposableController()
        {
            // Arrange
            var activator = new DefaultControllerActivator(Mock.Of<ITypeActivatorCache>());
            var controller = new object();

            // Act + Assert (does not throw)
            activator.Release(Mock.Of<ControllerContext>(), controller);
        }

        [Fact]
        public void Create_TypeActivatesTypesWithServices()
        {
            // Arrange
            var activator = new DefaultControllerActivator(new TypeActivatorCache());
            var serviceProvider = new Mock<IServiceProvider>(MockBehavior.Strict);
            var testService = new TestService();
            serviceProvider.Setup(s => s.GetService(typeof(TestService)))
                           .Returns(testService)
                           .Verifiable();

            var httpContext = new DefaultHttpContext
            {
                RequestServices = serviceProvider.Object
            };

            var context = new ControllerContext(
                new ActionContext(
                    httpContext,
                    new RouteData(),
                    new ControllerActionDescriptor
                    {
                        ControllerTypeInfo = typeof(TypeDerivingFromControllerWithServices).GetTypeInfo()
                    }));

            // Act
            var instance = activator.Create(context);

            // Assert
            var controller = Assert.IsType<TypeDerivingFromControllerWithServices>(instance);
            Assert.Same(testService, controller.TestService);
            serviceProvider.Verify();
        }

        public class Controller
        {
        }

        private class TypeDerivingFromController : Controller
        {
        }

        private class TypeDerivingFromControllerWithServices : Controller
        {
            public TypeDerivingFromControllerWithServices(TestService service)
            {
                TestService = service;
            }

            public TestService TestService { get; }
        }

        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, new List<IModelValidatorProvider>(), new MvcOptions()));
            return services.Object;
        }

        private class PocoType
        {
        }

        private class TestService
        {
        }

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

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

        private class MyAsyncDisposableController : IAsyncDisposable
        {
            public bool Disposed { get; set; }

            public void Dispose()
            {
            }

            public ValueTask DisposeAsync()
            {
                Disposed = true;
                return default;
            }
        }

        private class MyDisposableAndAsyncDisposableController : IDisposable, IAsyncDisposable
        {
            public bool AsyncDisposed { get; set; }
            public bool SyncDisposed { get; set; }

            public void Dispose()
            {
                SyncDisposed = true;
            }

            public ValueTask DisposeAsync()
            {
                AsyncDisposed = true;
                return default;
            }
        }
    }
}
