// 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.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Primitives;
using Moq;

namespace Microsoft.AspNetCore.Mvc.Routing;

// There are some basic sanity tests here for the details of how actions
// are turned into endpoints. See ActionEndpointFactoryTest for detailed tests.
public abstract class ActionEndpointDataSourceBaseTest
{
    [Fact]
    public void Endpoints_AccessParameters_InitializedFromProvider()
    {
        // Arrange
        var actions = new Mock<IActionDescriptorCollectionProvider>();
        actions.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>
            {
                CreateActionDescriptor(new { Name = "Value", }, "/Template!"),
            }, 0));

        var dataSource = CreateDataSource(actions.Object);

        // Act
        var endpoints = dataSource.Endpoints;

        // Assert
        var endpoint = Assert.IsType<RouteEndpoint>(Assert.Single(endpoints));
        Assert.Equal("Value", endpoint.RoutePattern.RequiredValues["Name"]);
        Assert.Equal("/Template!", endpoint.RoutePattern.RawText);
    }

    [Fact]
    public void Endpoints_CalledMultipleTimes_ReturnsSameInstance()
    {
        // Arrange
        var actionDescriptorCollectionProviderMock = new Mock<IActionDescriptorCollectionProvider>();
        actionDescriptorCollectionProviderMock
            .Setup(m => m.ActionDescriptors)
            .Returns(new ActionDescriptorCollection(new[]
            {
                    CreateActionDescriptor(new { controller = "TestController", action = "TestAction" }, "/test"),
            }, version: 0));

        var dataSource = CreateDataSource(actionDescriptorCollectionProviderMock.Object);

        // Act
        var endpoints1 = dataSource.Endpoints;
        var endpoints2 = dataSource.Endpoints;

        // Assert
        Assert.Collection(
            endpoints1,
            (e) => Assert.Equal("/test", Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText));
        Assert.Same(endpoints1, endpoints2);

        actionDescriptorCollectionProviderMock.VerifyGet(m => m.ActionDescriptors, Times.Once);
    }

    [Fact]
    public void Endpoints_ChangeTokenTriggered_EndpointsRecreated()
    {
        // Arrange
        var actionDescriptorCollectionProviderMock = new Mock<ActionDescriptorCollectionProvider>();
        actionDescriptorCollectionProviderMock
            .Setup(m => m.ActionDescriptors)
            .Returns(new ActionDescriptorCollection(new[]
            {
                    CreateActionDescriptor(new { controller = "TestController", action = "TestAction" }, "/test")
            }, version: 0));

        CancellationTokenSource cts = null;
        actionDescriptorCollectionProviderMock
            .Setup(m => m.GetChangeToken())
            .Returns(() =>
            {
                cts = new CancellationTokenSource();
                var changeToken = new CancellationChangeToken(cts.Token);

                return changeToken;
            });

        var dataSource = CreateDataSource(actionDescriptorCollectionProviderMock.Object);

        // Act
        var endpoints = dataSource.Endpoints;

        Assert.Collection(endpoints,
            (e) =>
            {
                var routePattern = Assert.IsType<RouteEndpoint>(e).RoutePattern;
                Assert.Equal("/test", routePattern.RawText);
                Assert.Equal("TestController", routePattern.RequiredValues["controller"]);
                Assert.Equal("TestAction", routePattern.RequiredValues["action"]);
            });

        actionDescriptorCollectionProviderMock
            .Setup(m => m.ActionDescriptors)
            .Returns(new ActionDescriptorCollection(new[]
            {
                    CreateActionDescriptor(new { controller = "NewTestController", action = "NewTestAction" }, "/test")
            }, version: 1));

        cts.Cancel();

        // Assert
        var newEndpoints = dataSource.Endpoints;

        Assert.NotSame(endpoints, newEndpoints);
        Assert.Collection(newEndpoints,
            (e) =>
            {
                var routePattern = Assert.IsType<RouteEndpoint>(e).RoutePattern;
                Assert.Equal("/test", routePattern.RawText);
                Assert.Equal("NewTestController", routePattern.RequiredValues["controller"]);
                Assert.Equal("NewTestAction", routePattern.RequiredValues["action"]);
            });
    }

    private protected ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions = null)
    {
        if (actions == null)
        {
            actions = new DefaultActionDescriptorCollectionProvider(
                Array.Empty<IActionDescriptorProvider>(),
                Array.Empty<IActionDescriptorChangeProvider>(),
                NullLogger<DefaultActionDescriptorCollectionProvider>.Instance);
        }

        var services = new ServiceCollection();
        services.AddSingleton(actions);

        var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
        services.Configure<RouteOptions>(routeOptionsSetup.Configure);
        services.AddRouting(options =>
        {
            options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
        });

        var serviceProvider = services.BuildServiceProvider();

        var endpointFactory = new ActionEndpointFactory(serviceProvider.GetRequiredService<RoutePatternTransformer>(), Enumerable.Empty<IRequestDelegateFactory>(), serviceProvider);

        return CreateDataSource(actions, endpointFactory);
    }

    private protected abstract ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory);

    private class UpperCaseParameterTransform : IOutboundParameterTransformer
    {
        public string TransformOutbound(object value)
        {
            return value?.ToString().ToUpperInvariant();
        }
    }

    protected abstract ActionDescriptor CreateActionDescriptor(
        object values,
        string pattern = null,
        IList<object> metadata = null);
}
