// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Moq;

namespace Microsoft.AspNetCore.Mvc;

public class MvcCoreServiceCollectionExtensionsTest
{
    // Some MVC services can be registered multiple times, for example, 'IConfigureOptions<MvcOptions>' can
    // be registered by calling 'ConfigureMvc(...)' before the call to 'AddMvc()' in which case the options
    // configuration is run in the order they were registered.
    //
    // For these kind of multi registration service types, we want to make sure that MVC will still add its
    // services if the implementation type is different.
    [Fact]
    public void MultiRegistrationServiceTypes_AreRegistered_MultipleTimes()
    {
        // Arrange
        var services = new ServiceCollection();

        // Register a mock implementation of each service, AddMvcServices should add another implementation.
        foreach (var serviceType in MultiRegistrationServiceTypes)
        {
            var mockType = typeof(Mock<>).MakeGenericType(serviceType.Key);
            services.Add(ServiceDescriptor.Transient(serviceType.Key, mockType));
        }

        // Act
        MvcCoreServiceCollectionExtensions.AddMvcCoreServices(services);

        // Assert
        foreach (var serviceType in MultiRegistrationServiceTypes)
        {
            AssertServiceCountEquals(services, serviceType.Key, serviceType.Value.Length + 1);

            foreach (var implementationType in serviceType.Value)
            {
                AssertContainsSingle(services, serviceType.Key, implementationType);
            }
        }
    }

    [Fact]
    public void SingleRegistrationServiceTypes_AreNotRegistered_MultipleTimes()
    {
        // Arrange
        var services = new ServiceCollection();

        // Register a mock implementation of each service, AddMvcServices should not replace it.
        foreach (var serviceType in SingleRegistrationServiceTypes)
        {
            var mockType = typeof(Mock<>).MakeGenericType(serviceType);
            services.Add(ServiceDescriptor.Transient(serviceType, mockType));
        }

        // Act
        MvcCoreServiceCollectionExtensions.AddMvcCoreServices(services);

        // Assert
        foreach (var singleRegistrationType in SingleRegistrationServiceTypes)
        {
            AssertServiceCountEquals(services, singleRegistrationType, 1);
        }
    }

    [Fact]
    public void AddMvcServicesTwice_DoesNotAddDuplicates()
    {
        // Arrange
        var services = new ServiceCollection();

        // Act
        MvcCoreServiceCollectionExtensions.AddMvcCoreServices(services);
        MvcCoreServiceCollectionExtensions.AddMvcCoreServices(services);

        // Assert
        var singleRegistrationServiceTypes = SingleRegistrationServiceTypes;
        foreach (var service in services)
        {
            if (singleRegistrationServiceTypes.Contains(service.ServiceType))
            {
                // 'single-registration' services should only have one implementation registered.
                AssertServiceCountEquals(services, service.ServiceType, 1);
            }
            else
            {
                // 'multi-registration' services should only have one *instance* of each implementation registered.
                AssertContainsSingle(services, service.ServiceType, service.ImplementationType);
            }
        }
    }

    [Fact]
    public void AddMvcCore_UsesOriginalPartManager()
    {
        // Arrange
        var manager = new ApplicationPartManager();
        var services = new ServiceCollection();
        services.AddSingleton(manager);

        // Act
        var builder = services.AddMvcCore();

        // Assert
        // SingleRegistrationServiceTypes_AreNotRegistered_MultipleTimes already checks that no other
        // ApplicationPartManager (but manager) is registered.
        Assert.Same(manager, builder.PartManager);
        Assert.Contains(manager.FeatureProviders, provider => provider is ControllerFeatureProvider);
    }

    // Regression test for aspnet/Mvc#5554.
    [Fact]
    public void AddMvcCore_UsesLastPartManager()
    {
        // Arrange
        var services = new ServiceCollection();
        var mockManager = new Mock<ApplicationPartManager>(MockBehavior.Strict);
        services.AddSingleton(mockManager.Object);

        var manager = new ApplicationPartManager();
        services.AddSingleton(manager);

        // Act
        var builder = services.AddMvcCore();

        // Assert
        Assert.Same(manager, builder.PartManager);
        Assert.Contains(manager.FeatureProviders, provider => provider is ControllerFeatureProvider);
    }

    [Fact]
    public void AddMvcCore_UsesOriginalHostingEnvironment()
    {
        // Arrange
        var services = new ServiceCollection();
        var environment = new Mock<IWebHostEnvironment>();
        environment.SetupGet(e => e.ApplicationName).Returns((string)null).Verifiable();
        services.AddSingleton<IWebHostEnvironment>(environment.Object);

        // Act
        var builder = services.AddMvcCore();

        // Assert
        Assert.NotNull(builder.PartManager);
        Assert.Empty(builder.PartManager.ApplicationParts);
        Assert.Contains(builder.PartManager.FeatureProviders, provider => provider is ControllerFeatureProvider);

        environment.VerifyAll();
    }

    // Second regression test for aspnet/Mvc#5554.
    [Fact]
    public void AddMvcCore_UsesLastHostingEnvironment()
    {
        // Arrange
        var services = new ServiceCollection();
        var environment = new Mock<IWebHostEnvironment>();
        services.AddSingleton<IWebHostEnvironment>(environment.Object);

        environment = new Mock<IWebHostEnvironment>();
        environment.SetupGet(e => e.ApplicationName).Returns((string)null).Verifiable();
        services.AddSingleton<IWebHostEnvironment>(environment.Object);

        // Act
        var builder = services.AddMvcCore();

        // Assert
        Assert.NotNull(builder.PartManager);
        Assert.Empty(builder.PartManager.ApplicationParts);
        Assert.Contains(builder.PartManager.FeatureProviders, provider => provider is ControllerFeatureProvider);

        environment.VerifyAll();
    }

    [Fact]
    public void AddMvcCore_GetsPartsForApplication()
    {
        // Arrange
        var services = new ServiceCollection();
        var environment = new Mock<IWebHostEnvironment>();
        var assemblyName = typeof(MvcCoreServiceCollectionExtensionsTest).Assembly.GetName();
        var applicationName = assemblyName.FullName;
        environment.SetupGet(e => e.ApplicationName).Returns(applicationName).Verifiable();
        services.AddSingleton<IWebHostEnvironment>(environment.Object);

        // Act
        var builder = services.AddMvcCore();

        // Assert
        Assert.NotNull(builder.PartManager);
        Assert.Contains(
            builder.PartManager.ApplicationParts,
            part => string.Equals(assemblyName.Name, part.Name, StringComparison.Ordinal));
        Assert.Contains(builder.PartManager.FeatureProviders, provider => provider is ControllerFeatureProvider);

        environment.VerifyAll();
    }

    private IEnumerable<Type> SingleRegistrationServiceTypes
    {
        get
        {
            var services = new ServiceCollection();
            MvcCoreServiceCollectionExtensions.AddMvcCoreServices(services);

            var multiRegistrationServiceTypes = MultiRegistrationServiceTypes;
            return services
                .Where(sd => !multiRegistrationServiceTypes.ContainsKey(sd.ServiceType))
                .Select(sd => sd.ServiceType);
        }
    }

    private Dictionary<Type, Type[]> MultiRegistrationServiceTypes
    {
        get
        {
            return new Dictionary<Type, Type[]>()
                {
                    {
                        typeof(IConfigureOptions<MvcOptions>),
                        new Type[]
                        {
                            typeof(MvcCoreMvcOptionsSetup),
                        }
                    },
                    {
                        typeof(IPostConfigureOptions<MvcOptions>),
                        new Type[]
                        {
                            typeof(MvcCoreMvcOptionsSetup),
                        }
                    },
                    {
                        typeof(IConfigureOptions<RouteOptions>),
                        new Type[]
                        {
                            typeof(MvcCoreRouteOptionsSetup),
                        }
                    },
                    {
                        typeof(IConfigureOptions<ApiBehaviorOptions>),
                        new Type[]
                        {
                            typeof(ApiBehaviorOptionsSetup),
                        }
                    },
                    {
                        typeof(IActionConstraintProvider),
                        new Type[]
                        {
                            typeof(DefaultActionConstraintProvider),
                        }
                    },
                    {
                        typeof(IActionDescriptorProvider),
                        new Type[]
                        {
                            typeof(ControllerActionDescriptorProvider),
                        }
                    },
                    {
                        typeof(IActionInvokerProvider),
                        new Type[]
                        {
                            typeof(ControllerActionInvokerProvider),
                        }
                    },
                    {
                        typeof(IRequestDelegateFactory),
                        new Type[]
                        {
                            typeof(ControllerRequestDelegateFactory)
                        }
                    },
                    {
                        typeof(IFilterProvider),
                        new Type[]
                        {
                            typeof(DefaultFilterProvider),
                        }
                    },
                    {
                        typeof(IControllerPropertyActivator),
                        new Type[]
                        {
                            typeof(DefaultControllerPropertyActivator),
                        }
                    },
                    {
                        typeof(IApplicationModelProvider),
                        new Type[]
                        {
                            typeof(DefaultApplicationModelProvider),
                            typeof(ApiBehaviorApplicationModelProvider),
                        }
                    },
                    {
                        typeof(IStartupFilter),
                        new Type[]
                        {
                            typeof(MiddlewareFilterBuilderStartupFilter)
                        }
                    },
                    {
                        typeof(MatcherPolicy),
                        new Type[]
                        {
                            typeof(ActionConstraintMatcherPolicy),
                            typeof(DynamicControllerEndpointMatcherPolicy),
                        }
                    },
                    {
                        typeof(IProblemDetailsWriter),
                        new Type[]
                        {
                            typeof(DefaultApiProblemDetailsWriter),
                        }
                    },
                };
        }
    }

    private void AssertServiceCountEquals(
        IServiceCollection services,
        Type serviceType,
        int expectedServiceRegistrationCount)
    {
        var serviceDescriptors = services.Where(serviceDescriptor => serviceDescriptor.ServiceType == serviceType);
        var actual = serviceDescriptors.Count();

        Assert.True(
            (expectedServiceRegistrationCount == actual),
            $"Expected service type '{serviceType}' to be registered {expectedServiceRegistrationCount}" +
            $" time(s) but was actually registered {actual} time(s).");
    }

    private void AssertContainsSingle(
        IServiceCollection services,
        Type serviceType,
        Type implementationType)
    {
        var matches = services
            .Where(sd =>
                sd.ServiceType == serviceType &&
                sd.ImplementationType == implementationType)
            .ToArray();

        if (matches.Length == 0)
        {
            Assert.True(
                false,
                $"Could not find an instance of {implementationType} registered as {serviceType}");
        }
        else if (matches.Length > 1)
        {
            Assert.True(
                false,
                $"Found multiple instances of {implementationType} registered as {serviceType}");
        }
    }
}
