// 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.Reflection;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
    public class ApiConventionMatcherTest
    {
        [Theory]
        [InlineData("Method", "method")]
        [InlineData("Method", "ConventionMethod")]
        [InlineData("p", "model")]
        [InlineData("person", "model")]
        public void IsNameMatch_WithAny_AlwaysReturnsTrue(string name, string conventionName)
        {
            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Any);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void IsNameMatch_WithExact_ReturnsFalse_IfNamesDifferInCase()
        {
            // Arrange
            var name = "Name";
            var conventionName = "name";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithExact_ReturnsFalse_IfNamesAreDifferent()
        {
            // Arrange
            var name = "Name";
            var conventionName = "Different";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithExact_ReturnsFalse_IfConventionNameIsSubString()
        {
            // Arrange
            var name = "RegularName";
            var conventionName = "Regular";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithExact_ReturnsFalse_IfConventionNameIsSuperString()
        {
            // Arrange
            var name = "Regular";
            var conventionName = "RegularName";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithExact_ReturnsTrue_IfExactMatch()
        {
            // Arrange
            var name = "parameterName";
            var conventionName = "parameterName";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void IsNameMatch_WithPrefix_ReturnsTrue_IfNamesAreExact()
        {
            // Arrange
            var name = "PostPerson";
            var conventionName = "PostPerson";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void IsNameMatch_WithPrefix_ReturnsTrue_IfNameIsProperPrefix()
        {
            // Arrange
            var name = "PostPerson";
            var conventionName = "Post";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void IsNameMatch_WithPrefix_ReturnsFalse_IfNamesAreDifferent()
        {
            // Arrange
            var name = "GetPerson";
            var conventionName = "Post";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithPrefix_ReturnsFalse_IfNamesDifferInCase()
        {
            // Arrange
            var name = "GetPerson";
            var conventionName = "post";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithPrefix_ReturnsFalse_IfNameIsNotProperPrefix()
        {
            // Arrange
            var name = "Postman";
            var conventionName = "Post";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithPrefix_ReturnsFalse_IfNameIsSuffix()
        {
            // Arrange
            var name = "GoPost";
            var conventionName = "Post";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithSuffix_ReturnsFalse_IfNamesAreDifferent()
        {
            // Arrange
            var name = "name";
            var conventionName = "diff";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithSuffix_ReturnsFalse_IfNameIsNotSuffix()
        {
            // Arrange
            var name = "personId";
            var conventionName = "idx";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithSuffix_ReturnTrue_IfNameIsExact()
        {
            // Arrange
            var name = "test";
            var conventionName = "test";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void IsNameMatch_WithSuffix_ReturnFalse_IfNameDiffersInCase()
        {
            // Arrange
            var name = "test";
            var conventionName = "Test";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsNameMatch_WithSuffix_ReturnTrue_IfNameIsProperSuffix()
        {
            // Arrange
            var name = "personId";
            var conventionName = "id";

            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);

            // Assert
            Assert.True(result);
        }

        [Theory]
        [InlineData("candid", "id")]
        [InlineData("canDid", "id")]
        public void IsNameMatch_WithSuffix_ReturnFalse_IfNameIsNotProperSuffix(string name, string conventionName)
        {
            // Act
            var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);

            // Assert
            Assert.False(result);
        }

        [Theory]
        [InlineData(typeof(object), typeof(object))]
        [InlineData(typeof(int), typeof(void))]
        [InlineData(typeof(string), typeof(DateTime))]
        public void IsTypeMatch_WithAny_ReturnsTrue(Type type, Type conventionType)
        {
            // Act
            var result = ApiConventionMatcher.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.Any);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void IsTypeMatch_WithAssignableFrom_ReturnsTrueForExact()
        {
            // Arrange
            var type = typeof(Base);
            var conventionType = typeof(Base);

            // Act
            var result = ApiConventionMatcher.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void IsTypeMatch_WithAssignableFrom_ReturnsTrueForDerived()
        {
            // Arrange
            var type = typeof(Derived);
            var conventionType = typeof(Base);

            // Act
            var result = ApiConventionMatcher.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void IsTypeMatch_WithAssignableFrom_ReturnsFalseForBaseTypes()
        {
            // Arrange
            var type = typeof(Base);
            var conventionType = typeof(Derived);

            // Act
            var result = ApiConventionMatcher.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsTypeMatch_WithAssignableFrom_ReturnsFalseForUnrelated()
        {
            // Arrange
            var type = typeof(string);
            var conventionType = typeof(Derived);

            // Act
            var result = ApiConventionMatcher.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsMatch_ReturnsFalse_IfMethodNamesDoNotMatch()
        {
            // Arrange
            var method = typeof(TestController).GetMethod(nameof(TestController.Get));
            var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Post));

            // Act
            var result = ApiConventionMatcher.IsMatch(method, conventionMethod);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsMatch_ReturnsFalse_IMethodHasMoreParametersThanConvention()
        {
            // Arrange
            var method = typeof(TestController).GetMethod(nameof(TestController.Get));
            var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetNoArgs));

            // Act
            var result = ApiConventionMatcher.IsMatch(method, conventionMethod);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsMatch_ReturnsFalse_IfMethodHasFewerParametersThanConvention()
        {
            // Arrange
            var method = typeof(TestController).GetMethod(nameof(TestController.Get));
            var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetTwoArgs));

            // Act
            var result = ApiConventionMatcher.IsMatch(method, conventionMethod);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsMatch_ReturnsFalse_IfParametersDoNotMatch()
        {
            // Arrange
            var method = typeof(TestController).GetMethod(nameof(TestController.Get));
            var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetParameterNotMatching));

            // Act
            var result = ApiConventionMatcher.IsMatch(method, conventionMethod);

            // Assert
            Assert.False(result);
        }

        [Fact]
        public void IsMatch_ReturnsTrue_IfMethodNameAndParametersMatches()
        {
            // Arrange
            var method = typeof(TestController).GetMethod(nameof(TestController.Get));
            var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Get));

            // Act
            var result = ApiConventionMatcher.IsMatch(method, conventionMethod);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void IsMatch_ReturnsTrue_IfParamsArrayMatchesRemainingArguments()
        {
            // Arrange
            var method = typeof(TestController).GetMethod(nameof(TestController.Search));
            var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Search));

            // Act
            var result = ApiConventionMatcher.IsMatch(method, conventionMethod);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void IsMatch_WithEmpty_MatchesMethodWithNoParameters()
        {
            // Arrange
            var method = typeof(TestController).GetMethod(nameof(TestController.SearchEmpty));
            var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.SearchWithParams));

            // Act
            var result = ApiConventionMatcher.IsMatch(method, conventionMethod);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void GetNameMatchBehavior_ReturnsExact_WhenNoAttributesArePresent()
        {
            // Arrange
            var expected = ApiConventionNameMatchBehavior.Exact;
            var attributes = new object[0];
            var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);

            // Act
            var result = ApiConventionMatcher.GetNameMatchBehavior(provider);

            // Assert
            Assert.Equal(expected, result);
        }

        [Fact]
        public void GetNameMatchBehavior_ReturnsExact_WhenNoNameMatchBehaviorAttributeIsSpecified()
        {
            // Arrange
            var expected = ApiConventionNameMatchBehavior.Exact;
            var attributes = new object[] { new CLSCompliantAttribute(false), new ProducesResponseTypeAttribute(200) };
            var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);

            // Act
            var result = ApiConventionMatcher.GetNameMatchBehavior(provider);

            // Assert
            Assert.Equal(expected, result);
        }

        [Fact]
        public void GetNameMatchBehavior_ReturnsValueFromAttributes()
        {
            // Arrange
            var expected = ApiConventionNameMatchBehavior.Prefix;
            var attributes = new object[]
            {
                new CLSCompliantAttribute(false),
                new ApiConventionNameMatchAttribute(expected),
                new ProducesResponseTypeAttribute(200) }
            ;
            var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);

            // Act
            var result = ApiConventionMatcher.GetNameMatchBehavior(provider);

            // Assert
            Assert.Equal(expected, result);
        }

        [Fact]
        public void GetTypeMatchBehavior_ReturnsIsAssignableFrom_WhenNoAttributesArePresent()
        {
            // Arrange
            var expected = ApiConventionTypeMatchBehavior.AssignableFrom;
            var attributes = new object[0];
            var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);

            // Act
            var result = ApiConventionMatcher.GetTypeMatchBehavior(provider);

            // Assert
            Assert.Equal(expected, result);
        }

        [Fact]
        public void GetTypeMatchBehavior_ReturnsIsAssignableFrom_WhenNoMatchingAttributesArePresent()
        {
            // Arrange
            var expected = ApiConventionTypeMatchBehavior.AssignableFrom;
            var attributes = new object[] { new CLSCompliantAttribute(false), new ProducesResponseTypeAttribute(200) };
            var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);

            // Act
            var result = ApiConventionMatcher.GetTypeMatchBehavior(provider);

            // Assert
            Assert.Equal(expected, result);
        }

        [Fact]
        public void GetTypeMatchBehavior_ReturnsValueFromAttributes()
        {
            // Arrange
            var expected = ApiConventionTypeMatchBehavior.Any;
            var attributes = new object[]
            {
                new CLSCompliantAttribute(false),
                new ApiConventionTypeMatchAttribute(expected),
                new ProducesResponseTypeAttribute(200) }
            ;
            var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);

            // Act
            var result = ApiConventionMatcher.GetTypeMatchBehavior(provider);

            // Assert
            Assert.Equal(expected, result);
        }

        public class Base { }

        public class Derived : Base { }

        public class TestController
        {
            public IActionResult Get(int id) => null;

            public IActionResult Search(string searchTerm, bool sortDescending, int page) => null;

            public IActionResult SearchEmpty() => null;
        }

        public static class TestConvention
        {
            [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
            public static void Get(int id) { }

            [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
            public static void GetNoArgs() { }

            [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
            public static void GetTwoArgs(int id, string name) { }

            [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
            public static void Post(Derived model) { }

            [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
            public static void GetParameterNotMatching([ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.AssignableFrom)] Derived model) { }

            [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
            public static void Search(
                [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Exact)]
                string searchTerm,
                params object[] others)
            { }

            [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
            public static void SearchWithParams(params object[] others) { }
        }
    }
}
