// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Routing
{
    public class AttributeRouteTest
    {
        private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0);

        // This test verifies that AttributeRoute can respond to changes in the AD collection. It does this
        // by running a successful request, then removing that action and verifying the next route isn't
        // successful.
        [Fact]
        public async Task AttributeRoute_UsesUpdatedActionDescriptors()
        {
            // Arrange
            ActionDescriptor selected = null;

            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{key1}"
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Store/Buy/{key2}"
                    },
                },
            };


            Func<ActionDescriptor[], IRouter> handlerFactory = (_) =>
            {
                var handler = new Mock<IRouter>();
                handler
                    .Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
                    .Returns<RouteContext>(routeContext =>
                    {
                        if (routeContext.RouteData.Values.ContainsKey("key1"))
                        {
                            selected = actions[0];
                        }
                        else if (routeContext.RouteData.Values.ContainsKey("key2"))
                        {
                            selected = actions[1];
                        }

                        routeContext.Handler = (c) => Task.CompletedTask;

                        return Task.CompletedTask;

                    });
                return handler.Object;
            };

            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(handlerFactory, actionDescriptorProvider.Object);

            var requestServices = new Mock<IServiceProvider>(MockBehavior.Strict);
            requestServices
                .Setup(s => s.GetService(typeof(ILoggerFactory)))
                .Returns(NullLoggerFactory.Instance);

            var httpContext = new DefaultHttpContext();
            httpContext.Request.Path = new PathString("/api/Store/Buy/5");
            httpContext.RequestServices = requestServices.Object;

            var context = new RouteContext(httpContext);

            // Act 1
            await route.RouteAsync(context);

            // Assert 1
            Assert.NotNull(context.Handler);
            Assert.Equal("5", context.RouteData.Values["key2"]);
            Assert.Same(actions[1], selected);

            // Arrange 2 - remove the action and update the collection
            selected = null;
            actions.RemoveAt(1);
            actionDescriptorProvider
                .SetupGet(ad => ad.ActionDescriptors)
                .Returns(new ActionDescriptorCollection(actions, version: 2));

            context = new RouteContext(httpContext);

            // Act 2
            await route.RouteAsync(context);

            // Assert 2
            Assert.Null(context.Handler);
            Assert.Empty(context.RouteData.Values);
            Assert.Null(selected);
        }

        [Fact]
        public void AttributeRoute_GetEntries_CreatesOutboundEntry()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.OutboundEntries,
                e =>
                {
                    Assert.Empty(e.Constraints);
                    Assert.Empty(e.Defaults);
                    Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
                    Assert.Equal("BLOG_INDEX", e.RouteName);
                    Assert.Equal(17, e.Order);
                    Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
                    Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
                });
        }

        [Fact]
        public void AttributeRoute_GetEntries_CreatesOutboundEntry_WithConstraint()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id:int}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.OutboundEntries,
                e =>
                {
                    Assert.Single(e.Constraints, kvp => kvp.Key == "id");
                    Assert.Empty(e.Defaults);
                    Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
                    Assert.Equal("BLOG_INDEX", e.RouteName);
                    Assert.Equal(17, e.Order);
                    Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
                    Assert.Equal("api/Blog/{id:int}", e.RouteTemplate.TemplateText);
                });
        }

        [Fact]
        public void AttributeRoute_GetEntries_CreatesOutboundEntry_WithDefault()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{*slug=hello}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.OutboundEntries,
                e =>
                {
                    Assert.Empty(e.Constraints);
                    Assert.Equal(new RouteValueDictionary(new { slug = "hello" }), e.Defaults);
                    Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
                    Assert.Equal("BLOG_INDEX", e.RouteName);
                    Assert.Equal(17, e.Order);
                    Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
                    Assert.Equal("api/Blog/{*slug=hello}", e.RouteTemplate.TemplateText);
                });
        }

        // These actions seem like duplicates, but this is a real case that can happen where two different
        // actions define the same route info. Link generation happens based on the action name + controller
        // name.
        [Fact]
        public void AttributeRoute_GetEntries_CreatesOutboundEntry_ForEachAction()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index2" },
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.OutboundEntries,
                e =>
                {
                    Assert.Empty(e.Constraints);
                    Assert.Empty(e.Defaults);
                    Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
                    Assert.Equal("BLOG_INDEX", e.RouteName);
                    Assert.Equal(17, e.Order);
                    Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
                    Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
                },
                e =>
                {
                    Assert.Empty(e.Constraints);
                    Assert.Empty(e.Defaults);
                    Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
                    Assert.Equal("BLOG_INDEX", e.RouteName);
                    Assert.Equal(17, e.Order);
                    Assert.Equal(ToRouteValueDictionary(actions[1].RouteValues), e.RequiredLinkValues);
                    Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
                });
        }

        [Fact]
        public void AttributeRoute_GetEntries_CreatesInboundEntry()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.InboundEntries,
                e =>
                {
                    Assert.Empty(e.Constraints);
                    Assert.Equal(17, e.Order);
                    Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
                    Assert.Equal("BLOG_INDEX", e.RouteName);
                    Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
                    Assert.Empty(e.Defaults);
                });
        }

        [Fact]
        public void AttributeRoute_GetEntries_CreatesInboundEntry_WithConstraint()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id:int}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.InboundEntries,
                e =>
                {
                    Assert.Single(e.Constraints, kvp => kvp.Key == "id");
                    Assert.Equal(17, e.Order);
                    Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
                    Assert.Equal("BLOG_INDEX", e.RouteName);
                    Assert.Equal("api/Blog/{id:int}", e.RouteTemplate.TemplateText);
                    Assert.Empty(e.Defaults);
                });
        }

        [Fact]
        public void AttributeRoute_GetEntries_CreatesInboundEntry_WithDefault()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{*slug=hello}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.InboundEntries,
                e =>
                {
                    Assert.Empty(e.Constraints);
                    Assert.Equal(17, e.Order);
                    Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
                    Assert.Equal("BLOG_INDEX", e.RouteName);
                    Assert.Equal("api/Blog/{*slug=hello}", e.RouteTemplate.TemplateText);
                    Assert.Collection(
                        e.Defaults.OrderBy(kvp => kvp.Key),
                        kvp => Assert.Equal(new KeyValuePair<string, object>("slug", "hello"), kvp));
                });
        }

        // These actions seem like duplicates, but this is a real case that can happen where two different
        // actions define the same route info. Link generation happens based on the action name + controller
        // name.
        [Fact]
        public void AttributeRoute_GetEntries_CreatesInboundEntry_CombinesLikeActions()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index2" },
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.InboundEntries,
                e =>
                {
                    Assert.Empty(e.Constraints);
                    Assert.Equal(17, e.Order);
                    Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
                    Assert.Equal("BLOG_INDEX", e.RouteName);
                    Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
                    Assert.Empty(e.Defaults);
                });
        }

        [Theory]
        [InlineData("")]
        [InlineData("GetBlogById")]
        public void AttributeRoute_ThrowsRouteCreationException_ForConstraintsNotTakingArguments(string routeName)
        {
            // Arrange
            var routeTemplate = "api/Blog/{id:int(10)}";
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = routeTemplate,
                        Name = routeName
                    }
                },
            };
            var expectedErrorMessage = "An error occurred while adding a route to the route builder. " +
                        $"Route name '{routeName}' and template '{routeTemplate}'.";

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act & Assert
            var exception = Assert.Throws<RouteCreationException>(() =>
            {
                route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
            });
            Assert.Equal(expectedErrorMessage, exception.Message);
            Assert.IsType<RouteCreationException>(exception.InnerException);
        }

        [Fact]
        public void GetEntries_DoesNotCreateOutboundEntriesForAttributesWithSuppressForLinkGenerationSetToTrue()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK1",
                        SuppressLinkGeneration = true,
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/{snake-cased-name}",
                        Name = "BLOG_INDEX2",
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/",
                        Name = "BLOG_HOME",
                        SuppressPathMatching = true,
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.OutboundEntries,
                e =>
                {
                    Assert.Equal("BLOG_INDEX2", e.RouteName);
                    Assert.Equal("blog/{snake-cased-name}", e.RouteTemplate.TemplateText);
                },
                e =>
                {
                    Assert.Equal("BLOG_HOME", e.RouteName);
                    Assert.Equal("blog/", e.RouteTemplate.TemplateText);
                });
        }

        [Fact]
        public void GetEntries_DoesNotCreateOutboundEntriesForAttributesWithSuppressForLinkGenerationSetToTrue_WhenMultipleAttributesHaveTheSameTemplate()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK1",
                        SuppressLinkGeneration = true,
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK2",
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/",
                        Name = "BLOG_HOME",
                        SuppressPathMatching = true,
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.OutboundEntries,
                e =>
                {
                    Assert.Equal("BLOG_LINK2", e.RouteName);
                    Assert.Equal("blog/get/{id}", e.RouteTemplate.TemplateText);
                },
                e =>
                {
                    Assert.Equal("BLOG_HOME", e.RouteName);
                    Assert.Equal("blog/", e.RouteTemplate.TemplateText);
                });
        }


        [Fact]
        public void GetEntries_DoesNotCreateInboundEntriesForAttributesWithSuppressForPathMatchingSetToTrue()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK1",
                        SuppressLinkGeneration = true,
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/{snake-cased-name}",
                        Name = "BLOG_LINK2",
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/",
                        Name = "BLOG_HOME",
                        SuppressPathMatching = true,
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.InboundEntries,
                e =>
                {
                    Assert.Equal("BLOG_LINK1", e.RouteName);
                    Assert.Equal("blog/get/{id}", e.RouteTemplate.TemplateText);
                },
                e =>
                {
                    Assert.Equal("BLOG_LINK2", e.RouteName);
                    Assert.Equal("blog/{snake-cased-name}", e.RouteTemplate.TemplateText);
                });
        }

        [Fact]
        public void GetEntries_DoesNotCreateInboundEntriesForAttributesWithSuppressForPathMatchingSetToTrue_WhenMultipleAttributesHaveTheSameTemplate()
        {
            // Arrange
            var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK1",
                        SuppressPathMatching = true,
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK2",
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/",
                        Name = "BLOG_HOME",
                        SuppressLinkGeneration = true,
                    },
                },
            };

            var builder = CreateBuilder();
            var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
            var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);

            // Act
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);

            // Assert
            Assert.Collection(
                builder.InboundEntries,
                e =>
                {
                    Assert.Equal("BLOG_LINK2", e.RouteName);
                    Assert.Equal("blog/get/{id}", e.RouteTemplate.TemplateText);
                },
                e =>
                {
                    Assert.Equal("BLOG_HOME", e.RouteName);
                    Assert.Equal("blog/", e.RouteTemplate.TemplateText);
                });
        }

        private static TreeRouteBuilder CreateBuilder()
        {
            var services = new ServiceCollection()
                .AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)
                .AddLogging()
                .AddRouting()
                .AddOptions()
                .BuildServiceProvider();
            return services.GetRequiredService<TreeRouteBuilder>();
        }

        private static Mock<IRouter> CreateHandler()
        {
            var handler = new Mock<IRouter>(MockBehavior.Strict);
            handler
                .Setup(h => h.RouteAsync(It.IsAny<RouteContext>()))
                .Callback<RouteContext>(c => c.Handler = NullHandler)
                .Returns(Task.CompletedTask)
                .Verifiable();
            return handler;
        }

        private static Mock<IActionDescriptorCollectionProvider> CreateActionDescriptorProvider(
            IReadOnlyList<ActionDescriptor> actions)
        {
            var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>(MockBehavior.Strict);
            actionDescriptorProvider
                .SetupGet(ad => ad.ActionDescriptors)
                .Returns(new ActionDescriptorCollection(actions, version: 1));

            return actionDescriptorProvider;
        }

        private static AttributeRoute CreateRoute(
            IRouter handler,
            IActionDescriptorCollectionProvider actionDescriptorProvider)
        {
            return CreateRoute((_) => handler, actionDescriptorProvider);
        }

        private static AttributeRoute CreateRoute(
            Func<ActionDescriptor[], IRouter> handlerFactory,
            IActionDescriptorCollectionProvider actionDescriptorProvider)
        {
            var services = new ServiceCollection()
                .AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)
                .AddLogging()
                .AddRouting()
                .AddOptions()
                .BuildServiceProvider();
            return new AttributeRoute(actionDescriptorProvider, services, handlerFactory);
        }

        // Needed because new RouteValueDictionary(values) would give us all the properties of
        // the Dictionary class.
        private static RouteValueDictionary ToRouteValueDictionary(IDictionary<string, string> values)
        {
            var result = new RouteValueDictionary();
            foreach (var kvp in values)
            {
                result.Add(kvp.Key, kvp.Value);
            }

            return result;
        }
    }
}
