// 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.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using static Microsoft.AspNetCore.Routing.Matching.HttpMethodMatcherPolicy;

namespace Microsoft.AspNetCore.Routing.Matching;

public class HttpMethodMatcherPolicyTest
{
    [Fact]
    public void INodeBuilderPolicy_AppliesToNode_EndpointWithoutMetadata_ReturnsFalse()
    {
        // Arrange
        var endpoints = new[] { CreateEndpoint("/", null), };

        var policy = (INodeBuilderPolicy)CreatePolicy();

        // Act
        var result = policy.AppliesToEndpoints(endpoints);

        // Assert
        Assert.False(result);
    }

    [Fact]
    public void INodeBuilderPolicy_AppliesToNode_EndpointWithoutHttpMethods_ReturnsFalse()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
            };

        var policy = (INodeBuilderPolicy)CreatePolicy();

        // Act
        var result = policy.AppliesToEndpoints(endpoints);

        // Assert
        Assert.False(result);
    }

    [Fact]
    public void INodeBuilderPolicy_AppliesToNode_EndpointHasHttpMethods_ReturnsTrue()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
            };

        var policy = (INodeBuilderPolicy)CreatePolicy();

        // Act
        var result = policy.AppliesToEndpoints(endpoints);

        // Assert
        Assert.True(result);
    }

    [Fact]
    public void INodeBuilderPolicy_AppliesToNode_EndpointIsDynamic_ReturnsFalse()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", }), new DynamicEndpointMetadata()),
            };

        var policy = (INodeBuilderPolicy)CreatePolicy();

        // Act
        var result = policy.AppliesToEndpoints(endpoints);

        // Assert
        Assert.False(result);
    }

    [Fact]
    public void IEndpointSelectorPolicy_AppliesToNode_EndpointWithoutMetadata_ReturnsTrue()
    {
        // Arrange
        var endpoints = new[] { CreateEndpoint("/", null, new DynamicEndpointMetadata()), };

        var policy = (IEndpointSelectorPolicy)CreatePolicy();

        // Act
        var result = policy.AppliesToEndpoints(endpoints);

        // Assert
        Assert.True(result);
    }

    [Fact]
    public void IEndpointSelectorPolicy_AppliesToNode_EndpointWithoutHttpMethods_ReturnsTrue()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>()), new DynamicEndpointMetadata()),
            };

        var policy = (IEndpointSelectorPolicy)CreatePolicy();

        // Act
        var result = policy.AppliesToEndpoints(endpoints);

        // Assert
        Assert.True(result);
    }

    [Fact]
    public void IEndpointSelectorPolicy_AppliesToNode_EndpointHasHttpMethods_ReturnsTrue()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>()), new DynamicEndpointMetadata()),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
            };

        var policy = (IEndpointSelectorPolicy)CreatePolicy();

        // Act
        var result = policy.AppliesToEndpoints(endpoints);

        // Assert
        Assert.True(result);
    }

    [Fact]
    public void IEndpointSelectorPolicy_AppliesToNode_EndpointIsNotDynamic_ReturnsFalse()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
            };

        var policy = (IEndpointSelectorPolicy)CreatePolicy();

        // Act
        var result = policy.AppliesToEndpoints(endpoints);

        // Assert
        Assert.False(result);
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    public async Task IEndpointSelectorPolicy_ApplyAsync_ProcessesInvalidCandidate(int candidateNum)
    {
        var policy = (IEndpointSelectorPolicy)CreatePolicy();

        var endpoints = new RouteEndpoint[candidateNum];
        for (int i = 0; i < candidateNum; i++)
        {
            endpoints[i] = CreateEndpoint("/", new HttpMethodMetadata(new[] { "DEL" }));
        }

        var candidates = new CandidateSet(endpoints, new RouteValueDictionary[endpoints.Length], Enumerable.Repeat<int>(-1, candidateNum).ToArray());
        var httpContext = new DefaultHttpContext();

        await policy.ApplyAsync(httpContext, candidates);

        Assert.Equal(httpContext.GetEndpoint().Metadata, EndpointMetadataCollection.Empty);
        Assert.True(string.Equals(httpContext.GetEndpoint().DisplayName, Http405EndpointDisplayName, StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public void GetEdges_GroupsByHttpMethod()
    {
        // Arrange
        var endpoints = new[]
        {
                // These are arrange in an order that we won't actually see in a product scenario. It's done
                // this way so we can verify that ordering is preserved by GetEdges.
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
                CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" })),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
                CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
            };

        var policy = CreatePolicy();

        // Act
        var edges = policy.GetEdges(endpoints);

        // Assert
        Assert.Collection(
            edges.OrderBy(e => e.State),
            e =>
            {
                Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
            });
    }

    [Fact]
    public void GetEdges_GroupsByHttpMethod_Cors()
    {
        // Arrange
        var endpoints = new[]
        {
                // These are arrange in an order that we won't actually see in a product scenario. It's done
                // this way so we can verify that ordering is preserved by GetEdges.
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
                CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" }, acceptCorsPreflight: true)),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
                CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>(), acceptCorsPreflight: true)),
            };

        var policy = CreatePolicy();

        // Act
        var edges = policy.GetEdges(endpoints);

        // Assert
        Assert.Collection(
            edges.OrderBy(e => e.State),
            e =>
            {
                Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: true), e.State);
                Assert.Equal(new[] { endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: true), e.State);
                Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: true), e.State);
                Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: true), e.State);
                Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
            });
    }

    [Fact] // See explanation in GetEdges for how this case is different
    public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint()
    {
        // Arrange
        var endpoints = new[]
        {
                // These are arrange in an order that we won't actually see in a product scenario. It's done
                // this way so we can verify that ordering is preserved by GetEdges.
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" })),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
            };

        var policy = CreatePolicy();

        // Act
        var edges = policy.GetEdges(endpoints);

        // Assert
        Assert.Collection(
            edges.OrderBy(e => e.State),
            e =>
            {
                Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
                Assert.Equal(Http405EndpointDisplayName, e.Endpoints.Single().DisplayName);
            },
            e =>
            {
                Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
            });

    }

    [Fact] // See explanation in GetEdges for how this case is different
    public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint_CORS()
    {
        // Arrange
        var endpoints = new[]
        {
                // These are arrange in an order that we won't actually see in a product scenario. It's done
                // this way so we can verify that ordering is preserved by GetEdges.
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" }, acceptCorsPreflight: true)),
                CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
            };

        var policy = CreatePolicy();

        // Act
        var edges = policy.GetEdges(endpoints);

        // Assert
        Assert.Collection(
            edges.OrderBy(e => e.State),
            e =>
            {
                Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
                Assert.Equal(Http405EndpointDisplayName, e.Endpoints.Single().DisplayName);
            },
            e =>
            {
                Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: true), e.State);
                Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: true), e.State);
                Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: true), e.State);
                Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray());
            });
    }

    private static RouteEndpoint CreateEndpoint(string template, HttpMethodMetadata httpMethodMetadata, params object[] more)
    {
        var metadata = new List<object>();
        if (httpMethodMetadata != null)
        {
            metadata.Add(httpMethodMetadata);
        }

        if (more != null)
        {
            metadata.AddRange(more);
        }

        return new RouteEndpoint(
            TestConstants.EmptyRequestDelegate,
            RoutePatternFactory.Parse(template),
            0,
            new EndpointMetadataCollection(metadata),
            $"test: {template}");
    }

    private static HttpMethodMatcherPolicy CreatePolicy()
    {
        return new HttpMethodMatcherPolicy();
    }

    private class DynamicEndpointMetadata : IDynamicEndpointMetadata
    {
        public bool IsDynamic => true;
    }
}
