// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Xunit;

namespace Microsoft.AspNetCore.Routing.Matching
{
    // This class includes features that we have not yet implemented in the DFA
    // and instruction matchers.
    //
    // As those matchers add features we can move tests from this class into
    // MatcherConformanceTest and delete this.
    public abstract class FullFeaturedMatcherConformanceTest : MatcherConformanceTest
    {
        [Theory]
        [InlineData("/a/{b=15}", "/a/b", new string[] { "b", }, new string[] { "b", })]
        [InlineData("/a/{b=15}", "/a/", new string[] { "b", }, new string[] { "15", })]
        [InlineData("/a/{b=15}", "/a", new string[] { "b", }, new string[] { "15", })]
        [InlineData("/{a}/{b=15}", "/54/b", new string[] { "a", "b", }, new string[] { "54", "b", })]
        [InlineData("/{a=19}/{b=15}", "/54/b", new string[] { "a", "b", }, new string[] { "54", "b", })]
        [InlineData("/{a=19}/{b=15}", "/54/", new string[] { "a", "b", }, new string[] { "54", "15", })]
        [InlineData("/{a=19}/{b=15}", "/54", new string[] { "a", "b", }, new string[] { "54", "15", })]
        [InlineData("/{a=19}/{b=15}", "/", new string[] { "a", "b", }, new string[] { "19", "15", })]
        public virtual async Task Match_DefaultValues(string template, string path, string[] keys, string[] values)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, endpoint, keys, values);
        }

        [Fact]
        public virtual async Task Match_NonInlineDefaultValues()
        {
            // Arrange
            var endpoint = CreateEndpoint("/a/{b}/{c}", new { b = "17", c = "18", });
            var matcher = CreateMatcher(endpoint);
            var httpContext = CreateContext("/a");

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, endpoint, new { b = "17", c = "18", });
        }

        [Fact]
        public virtual async Task Match_ExtraDefaultValues()
        {
            // Arrange
            var endpoint = CreateEndpoint("/a/{b}/{c}", new { b = "17", c = "18", d = "19" });
            var matcher = CreateMatcher(endpoint);
            var httpContext = CreateContext("/a");

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, endpoint, new { b = "17", c = "18", d = "19" });
        }

        [Theory]
        [InlineData("/a/{b=15}", "/54/b")]
        [InlineData("/a/{b=15}", "/54/")]
        [InlineData("/a/{b=15}", "/54")]
        [InlineData("/a/{b=15}", "/a//")]
        [InlineData("/a/{b=15}", "/54/43/23")]
        [InlineData("/{a=19}/{b=15}", "/54/b/c")]
        [InlineData("/a/{b=15}/c", "/a/b")] // Intermediate default values don't act like optional segments
        [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a")]
        [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b")]
        [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c")]
        [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d")]
        public virtual async Task NotMatch_DefaultValues(string template, string path)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertNotMatch(httpContext);
        }

        [Theory]
        [InlineData("/{a?}/{b?}/{c?}", "/", null, null)]
        [InlineData("/{a?}/{b?}/{c?}", "/a", new[] { "a", }, new[] { "a", })]
        [InlineData("/{a?}/{b?}/{c?}", "/a/", new[] { "a", }, new[] { "a", })]
        [InlineData("/{a?}/{b?}/{c?}", "/a/b", new[] { "a", "b", }, new[] { "a", "b", })]
        [InlineData("/{a?}/{b?}/{c?}", "/a/b/", new[] { "a", "b", }, new[] { "a", "b", })]
        [InlineData("/{a?}/{b?}/{c?}", "/a/b/c", new[] { "a", "b", "c", }, new[] { "a", "b", "c", })]
        [InlineData("/{a?}/{b?}/{c?}", "/a/b/c/", new[] { "a", "b", "c", }, new[] { "a", "b", "c", })]
        [InlineData("/{c}/{a?}", "/h/i", new[] { "c", "a", }, new[] { "h", "i", })]
        [InlineData("/{c}/{a?}", "/h/", new[] { "c", }, new[] { "h", })]
        [InlineData("/{c}/{a?}", "/h", new[] { "c", }, new[] { "h", })]
        [InlineData("/{c?}/{a?}", "/", null, null)]
        [InlineData("/{c}/{a?}/{id?}", "/h/i/18", new[] { "c", "a", "id", }, new[] { "h", "i", "18", })]
        [InlineData("/{c}/{a?}/{id?}", "/h/i", new[] { "c", "a", }, new[] { "h", "i", })]
        [InlineData("/{c}/{a?}/{id?}", "/h", new[] { "c", }, new[] { "h", })]
        [InlineData("template/{p:int?}", "/template/5", new[] { "p", }, new[] { "5", })]
        [InlineData("template/{p:int?}", "/template", null, null)]
        [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e", new[] { "b", "d", "f" }, new[] { "b", "d", null, })]
        [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e/f", new[] { "b", "d", "f", }, new[] { "b", "d", "f", })]
        public virtual async Task Match_OptionalParameter(string template, string path, string[] keys, string[] values)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, endpoint, keys, values);
        }

        [Theory]
        [InlineData("/{a?}/{b?}/{c?}", "///")]
        [InlineData("/{a?}/{b?}/{c?}", "/a//")]
        [InlineData("/{a?}/{b?}/{c?}", "/a/b//")]
        [InlineData("/{a?}/{b?}/{c?}", "//b//")]
        [InlineData("/{a?}/{b?}/{c?}", "///c")]
        [InlineData("/{a?}/{b?}/{c?}", "///c/")]
        [InlineData("/{a?}/{b?}/{c?}", "/a/b/c/d")]
        [InlineData("/a/{b?}/{c?}", "/")]
        [InlineData("template/{parameter:int?}", "/template/qwer")]
        public virtual async Task NotMatch_OptionalParameter(string template, string path)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertNotMatch(httpContext);
        }

        [Theory]
        [InlineData("/{a}/{*b}", "/a", new[] { "a", "b", }, new[] { "a", null, })]
        [InlineData("/{a}/{*b}", "/a/", new[] { "a", "b", }, new[] { "a", null, })]
        [InlineData("/{a}/{*b=b}", "/a", new[] { "a", "b", }, new[] { "a", "b", })]
        [InlineData("/{a}/{*b=b}", "/a/", new[] { "a", "b", }, new[] { "a", "b", })]
        [InlineData("/{a}/{*b=b}", "/a/hello", new[] { "a", "b", }, new[] { "a", "hello", })]
        [InlineData("/{a}/{*b=b}", "/a/hello/goodbye", new[] { "a", "b", }, new[] { "a", "hello/goodbye", })]
        [InlineData("/{a}/{*b=b}", "/a/b//", new[] { "a", "b", }, new[] { "a", "b//", })]
        [InlineData("/{a}/{*b=b}", "/a/b/c/", new[] { "a", "b", }, new[] { "a", "b/c/", })]
        [InlineData("/{a=1}/{b=2}/{c=3}/{d=4}", "/a/b/c", new[] { "a", "b", "c", "d", }, new[] { "a", "b", "c", "4", })]
        [InlineData("a/{*path:regex(10/20/30)}", "/a/10/20/30", new[] { "path", }, new[] { "10/20/30" })]
        public virtual async Task Match_CatchAllParameter(string template, string path, string[] keys, string[] values)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, endpoint, keys, values);
        }

        // Historically catchall segments don't match an empty segment, but only if it's
        // the first one. So `/a/b//` would match, but `/a//` would not. This is pretty
        // wierd and inconsistent with the intent of using a catch all. The DfaMatcher
        // fixes this issue.
        [Theory]
        [InlineData("/{a}/{*b=b}", "/a///", new[] { "a", "b", }, new[] { "a", "//" })]
        [InlineData("/{a}/{*b=b}", "/a//c/", new[] { "a", "b", }, new[] { "a", "/c/" })]
        public virtual async Task Quirks_CatchAllParameter(string template, string path, string[] keys, string[] values)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertNotMatch(httpContext);

            // Need to access these to prevent a warning from the xUnit analyzer.
            // Some of these tests will match (and process the values) and some will not.
            GC.KeepAlive(keys);
            GC.KeepAlive(values);
        }

        [Theory]
        [InlineData("{p}x{s}", "/xxxxxxxxxx", new[] { "p", "s" }, new[] { "xxxxxxxx", "x", })]
        [InlineData("{p}xyz{s}", "/xxxxyzxyzxxxxxxyz", new[] { "p", "s" }, new[] { "xxxxyz", "xxxxxxyz", })]
        [InlineData("{p}xyz{s}", "/abcxxxxyzxyzxxxxxxyzxx", new[] { "p", "s" }, new[] { "abcxxxxyzxyzxxxxx", "xx", })]
        [InlineData("{p}xyz{s}", "/xyzxyzxyzxyzxyz", new[] { "p", "s" }, new[] { "xyzxyzxyz", "xyz", })]
        [InlineData("{p}xyz{s}", "/xyzxyzxyzxyzxyz1", new[] { "p", "s" }, new[] { "xyzxyzxyzxyz", "1", })]
        [InlineData("{p}xyz{s}", "/xyzxyzxyz", new[] { "p", "s" }, new[] { "xyz", "xyz", })]
        [InlineData("{p}aa{s}", "/aaaaa", new[] { "p", "s" }, new[] { "aa", "a", })]
        [InlineData("{p}aaa{s}", "/aaaaa", new[] { "p", "s" }, new[] { "a", "a", })]
        [InlineData("language/{lang=en}-{region=US}", "/language/xx-yy", new[] { "lang", "region" }, new[] { "xx", "yy", })]
        [InlineData("language/{lang}-{region}", "/language/en-US", new[] { "lang", "region" }, new[] { "en", "US", })]
        [InlineData("language/{lang}-{region}a", "/language/en-USa", new[] { "lang", "region" }, new[] { "en", "US", })]
        [InlineData("language/a{lang}-{region}", "/language/aen-US", new[] { "lang", "region" }, new[] { "en", "US", })]
        [InlineData("language/a{lang}-{region}a", "/language/aen-USa", new[] { "lang", "region" }, new[] { "en", "US", })]
        [InlineData("language/{lang}-", "/language/en-", new[] { "lang", }, new[] { "en", })]
        [InlineData("language/a{lang}", "/language/aen", new[] { "lang", }, new[] { "en", })]
        [InlineData("language/a{lang}a", "/language/aena", new[] { "lang", }, new[] { "en", })]
        public virtual async Task Match_ComplexSegment(string template, string path, string[] keys, string[] values)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, endpoint, keys, values);
        }

        [Theory]
        [InlineData("language/a{lang}-{region}a", "/language/a-USa")]
        [InlineData("language/a{lang}-{region}a", "/language/aen-a")]
        [InlineData("language/{lang=en}-{region=US}", "/language")]
        [InlineData("language/{lang=en}-{region=US}", "/language/-")]
        [InlineData("language/{lang=en}-{region=US}", "/language/xx-")]
        [InlineData("language/{lang=en}-{region=US}", "/language/-xx")]
        public virtual async Task NotMatch_ComplexSegment(string template, string path)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertNotMatch(httpContext);
        }

        [Theory]
        [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", new[] { "p1", "p2", }, new[] { "foo", "bar" })]
        [InlineData("moo/{p1}.{p2?}", "/moo/foo", new[] { "p1", }, new[] { "foo", })]
        [InlineData("moo/{p1}.{p2?}", "/moo/.foo", new[] { "p1", }, new[] { ".foo", })]
        [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", new[] { "p1", "p2", }, new[] { "foo.", "bar" })]
        [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", new[] { "p1", "p2", }, new[] { "foo.moo", "bar" })]
        [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", new[] { "p1", }, new[] { "moo", })]
        [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.foo.bar", new[] { "p1", "p2", }, new[] { "foo", "bar" })]
        [InlineData("moo/.{p2?}", "/moo/.foo", new[] { "p2", }, new[] { "foo", })]
        [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })]
        [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", new[] { "p1", "p2", }, new[] { "foo", "moo" })]
        [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })]
        [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })]
        [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", new[] { "p1", "p3" }, new[] { "foo", "bar" })]
        [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", new[] { "p1", "p3" }, new[] { ".foo", "bar" })]
        [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", new[] { "p1", "p2", "p3" }, new[] { "foo", "bar", "baz" })]
        public virtual async Task Match_OptionalSeparator(string template, string path, string[] keys, string[] values)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, endpoint, keys, values);
        }

        [Theory]
        [InlineData("moo/{p1}.{p2?}", "/moo/foo.")]
        [InlineData("moo/{p1}.{p2?}", "/moo/.")]
        [InlineData("moo/{p1}.{p2}", "/foo.")]
        [InlineData("moo/{p1}.{p2}", "/foo")]
        [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")]
        [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")]
        [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")]
        [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")]
        [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")]
        [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")]
        [InlineData("moo/.{p2?}", "/moo/.")]
        [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")]
        public virtual async Task NotMatch_OptionalSeparator(string template, string path)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertNotMatch(httpContext);
        }

        // Most of are copied from old routing tests that date back to the VS 2010 era. Enjoy!
        [Theory]
        [InlineData("{Controller}.mvc/../{action}", "/Home.mvc/../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })]
        [InlineData("{Controller}.mvc/.../{action}", "/Home.mvc/.../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })]
        [InlineData("{Controller}.mvc/../../../{action}", "/Home.mvc/../../../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })]
        [InlineData("{Controller}.mvc!/{action}", "/Home.mvc!/index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })]
        [InlineData("../{Controller}.mvc", "/../Home.mvc", new string[] { "Controller", }, new string[] { "Home", })]
        [InlineData(@"\{Controller}.mvc", @"/\Home.mvc", new string[] { "Controller", }, new string[] { "Home", })]
        [InlineData(@"{Controller}.mvc\{id}\{Param1}", @"/Home.mvc\123\p1", new string[] { "Controller", "id", "Param1" }, new string[] { "Home", "123", "p1" })]
        [InlineData("(Controller).mvc", "/(Controller).mvc", new string[] { }, new string[] { })]
        [InlineData("Controller.mvc/ ", "/Controller.mvc/ ", new string[] { }, new string[] { })]
        [InlineData("Controller.mvc ", "/Controller.mvc ", new string[] { }, new string[] { })]
        public virtual async Task Match_WierdCharacterCases(string template, string path, string[] keys, string[] values)
        {
            // Arrange
            var (matcher, endpoint) = CreateMatcher(template);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, endpoint, keys, values);
        }

        [Theory]
        [InlineData("template/5", "template/{parameter:int}")]
        [InlineData("template/5", "template/{parameter}")]
        [InlineData("template/5", "template/{*parameter:int}")]
        [InlineData("template/5", "template/{*parameter}")]
        [InlineData("template/{parameter}", "template/{parameter:alpha}")] // constraint doesn't match
        [InlineData("template/{parameter:int}", "template/{parameter}")]
        [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
        [InlineData("template/{parameter:int}", "template/{*parameter}")]
        [InlineData("template/{parameter}", "template/{*parameter:int}")]
        [InlineData("template/{parameter}", "template/{*parameter}")]
        [InlineData("template/{*parameter:int}", "template/{*parameter}")]
        public virtual async Task Match_SelectEndpoint_BasedOnPrecedence(string template1, string template2)
        {
            // Arrange
            var expected = CreateEndpoint(template1);
            var other = CreateEndpoint(template2);
            var path = "/template/5";

            // Arrange
            var matcher = CreateMatcher(other, expected);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true);
        }

        [Theory]
        [InlineData("template/5", "template/{parameter:int}")]
        [InlineData("template/5", "template/{parameter}")]
        [InlineData("template/5", "template/{*parameter:int}")]
        [InlineData("template/5", "template/{*parameter}")]
        [InlineData("template/{parameter:int}", "template/{parameter}")]
        [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
        [InlineData("template/{parameter:int}", "template/{*parameter}")]
        [InlineData("template/{parameter}", "template/{*parameter:int}")]
        [InlineData("template/{parameter}", "template/{*parameter}")]
        [InlineData("template/{*parameter:int}", "template/{*parameter}")]
        [InlineData("template/5", "template/5")]
        [InlineData("template/{parameter:int}", "template/{parameter:int}")]
        [InlineData("template/{parameter}", "template/{parameter}")]
        [InlineData("template/{*parameter:int}", "template/{*parameter:int}")]
        [InlineData("template/{*parameter}", "template/{*parameter}")]
        public virtual async Task Match_SelectEndpoint_BasedOnOrder(string template1, string template2)
        {
            // Arrange
            var expected = CreateEndpoint(template1, order: 0);
            var other = CreateEndpoint(template2, order: 1);
            var path = "/template/5";

            // Arrange
            var matcher = CreateMatcher(other, expected);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true);
        }

        [Theory]
        [InlineData("/", "")]
        [InlineData("/Literal1", "Literal1")]
        [InlineData("/Literal1/Literal2", "Literal1/Literal2")]
        [InlineData("/Literal1/Literal2/Literal3", "Literal1/Literal2/Literal3")]
        [InlineData("/Literal1/Literal2/Literal3/4", "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}")]
        [InlineData("/Literal1/Literal2/Literal3/Literal4", "Literal1/Literal2/Literal3/{*catchAll}")]
        [InlineData("/1", "{constrained1:int}")]
        [InlineData("/1/2", "{constrained1:int}/{constrained2:int}")]
        [InlineData("/1/2/3", "{constrained1:int}/{constrained2:int}/{constrained3:int}")]
        [InlineData("/1/2/3/4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}")]
        [InlineData("/1/2/3/CatchAll4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}")]
        [InlineData("/parameter1", "{parameter1}")]
        [InlineData("/parameter1/parameter2", "{parameter1}/{parameter2}")]
        [InlineData("/parameter1/parameter2/parameter3", "{parameter1}/{parameter2}/{parameter3}")]
        [InlineData("/parameter1/parameter2/parameter3/4", "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}")]
        [InlineData("/parameter1/parameter2/parameter3/CatchAll4", "{parameter1}/{parameter2}/{parameter3}/{*catchAll}")]
        public virtual async Task Match_IntegrationTest_MultipleEndpoints(string path, string expectedTemplate)
        {
            // Arrange
            var templates = new[]
            {
                "",
                "Literal1",
                "Literal1/Literal2",
                "Literal1/Literal2/Literal3",
                "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}",
                "Literal1/Literal2/Literal3/{*catchAll}",
                "{constrained1:int}",
                "{constrained1:int}/{constrained2:int}",
                "{constrained1:int}/{constrained2:int}/{constrained3:int}",
                "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}",
                "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}",
                "{parameter1}",
                "{parameter1}/{parameter2}",
                "{parameter1}/{parameter2}/{parameter3}",
                "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}",
                "{parameter1}/{parameter2}/{parameter3}/{*catchAll}",
            };

            var endpoints = templates.Select((t) => CreateEndpoint(t)).ToArray();
            var expected = endpoints[Array.IndexOf(templates, expectedTemplate)];

            var matcher = CreateMatcher(endpoints);
            var httpContext = CreateContext(path);

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true);
        }

        // https://github.com/dotnet/aspnetcore/issues/16579
        [Fact]
        public virtual async Task Match_Regression_16579_Order1()
        {
            var endpoints = new RouteEndpoint[]
            {
                EndpointFactory.CreateRouteEndpoint(
                    "{controller}/folder/{*path}",
                    order: 0,
                    defaults: new { controller = "File", action = "Folder", },
                    requiredValues: new { controller = "File", }),
                EndpointFactory.CreateRouteEndpoint(
                    "{controller}/{action}/{filename}",
                    order: 1,
                    defaults: new { controller = "File", action = "Index", },
                    requiredValues: new { controller = "File", action = "Index", }),
            };

            var expected = endpoints[0];

            var matcher = CreateMatcher(endpoints);
            var httpContext = CreateContext("/file/folder/abc/abc");

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true);
        }

        // https://github.com/dotnet/aspnetcore/issues/16579
        [Fact]
        public virtual async Task Match_Regression_16579_Order2()
        {
            var endpoints = new RouteEndpoint[]
            {
                EndpointFactory.CreateRouteEndpoint(
                    "{controller}/{action}/{filename}",
                    order: 0,
                    defaults: new { controller = "File", action = "Index", },
                    requiredValues: new { controller = "File", action = "Index", }),

                EndpointFactory.CreateRouteEndpoint(
                    "{controller}/folder/{*path}",
                    order: 1,
                    defaults: new { controller = "File", action = "Folder", },
                    requiredValues: new { controller = "File", }),
            };

            var expected = endpoints[1];

            var matcher = CreateMatcher(endpoints);
            var httpContext = CreateContext("/file/folder/abc/abc");

            // Act
            await matcher.MatchAsync(httpContext);

            // Assert
            MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true);
        }
    }
}
