// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Microsoft.AspNetCore.Analyzers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;

namespace Microsoft.AspNetCore.Mvc.Analyzers;

public class TopLevelParameterNameAnalyzerTest
{
    private static readonly DiagnosticResult Diagnostic = new(DiagnosticDescriptors.MVC1004_ParameterNameCollidesWithTopLevelProperty);

    [Fact]
    public Task DiagnosticsAreReturned_ForControllerActionsWithParametersThatMatchProperties()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class MyController : Controller
    {
        [HttpPost]
        public IActionResult EditPerson(MyModel {|#0:model|}) => null;
    }

    public class MyModel
    {
        public string Model { get; }
    }
}";
        var result = Diagnostic.WithLocation(0)
            .WithArguments("MyModel", "model");

        return VerifyAnalyzerAsync(source, result);
    }

    [Fact]
    public Task DiagnosticsAreReturned_ForModelBoundParameters()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class DiagnosticsAreReturned_ForModelBoundParameters : Controller
    {
        [HttpPost]
        public IActionResult EditPerson(
            [FromBody] DiagnosticsAreReturned_ForModelBoundParametersModel model,
            [FromQuery] DiagnosticsAreReturned_ForModelBoundParametersModel {|#0:value|}) => null;
    }

    public class DiagnosticsAreReturned_ForModelBoundParametersModel
    {
        public string Model { get; }

        public string Value { get; }
    }
}";
        var result = Diagnostic.WithLocation(0)
            .WithArguments("DiagnosticsAreReturned_ForModelBoundParametersModel", "value");

        return VerifyAnalyzerAsync(source, result);
    }

    [Fact]
    public Task DiagnosticsAreReturned_IfModelNameProviderIsUsedToModifyParameterName()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class DiagnosticsAreReturned_IfModelNameProviderIsUsedToModifyParameterName : Controller
    {
        [HttpPost]
        public IActionResult Edit([ModelBinder(Name = ""model"")] TestModel {|#0:parameter|}) => null;
    }

    public class TestModel
    {
        public string Model { get; }

        public string Value { get; }
    }
}
";
        var result = Diagnostic.WithLocation(0)
            .WithArguments("TestModel", "parameter");

        return VerifyAnalyzerAsync(source, result);
    }

    [Fact]
    public Task NoDiagnosticsAreReturnedForApiControllers()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    [ApiController]
    public class NoDiagnosticsAreReturnedForApiControllers : Controller
    {
        [HttpPost]
        public IActionResult EditPerson(NoDiagnosticsAreReturnedForApiControllersModel model) => null;
    }

    public class NoDiagnosticsAreReturnedForApiControllersModel
    {
        public string Model { get; }
    }
}";
        return VerifyAnalyzerAsync(source, DiagnosticResult.EmptyDiagnosticResults);
    }

    [Fact]
    public Task NoDiagnosticsAreReturnedIfParameterIsRenamedUsingBindingAttribute()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class NoDiagnosticsAreReturnedIfParameterIsRenamedUsingBindingAttribute : Controller
    {
        [HttpPost]
        public IActionResult EditPerson([FromForm(Name = """")] NoDiagnosticsAreReturnedIfParameterIsRenamedUsingBindingAttributeModel model) => null;
    }

    public class NoDiagnosticsAreReturnedIfParameterIsRenamedUsingBindingAttributeModel
    {
        public string Model { get; }
    }
}";

        return VerifyAnalyzerAsync(source, DiagnosticResult.EmptyDiagnosticResults);
    }

    [Fact]
    public Task NoDiagnosticsAreReturnedForNonActions()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class NoDiagnosticsAreReturnedForNonActions : Controller
    {
        [NonAction]
        public IActionResult EditPerson(NoDiagnosticsAreReturnedForNonActionsModel model) => null;
    }

    public class NoDiagnosticsAreReturnedForNonActionsModel
    {
        public string Model { get; }
    }
}";

        return VerifyAnalyzerAsync(source, DiagnosticResult.EmptyDiagnosticResults);
    }

    [Fact]
    public void IsProblematicParameter_ReturnsTrue_IfParameterNameIsTheSameAsModelProperty()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        public string Model { get; set; }

        public void ActionMethod(TestController model) { }
    }
}";

        var result = IsProblematicParameterTest(source);
        Assert.True(result);
    }

    [Fact]
    public void IsProblematicParameter_ReturnsTrue_IfParameterNameWithBinderAttributeIsTheSameNameAsModelProperty()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        public string Model { get; set; }

        public void ActionMethod([Bind(Prefix = ""model"")] TestController different) { }
    }
}";

        var result = IsProblematicParameterTest(source);
        Assert.True(result);
    }

    [Fact]
    public void IsProblematicParameter_ReturnsTrue_IfPropertyWithModelBindingAttributeHasSameNameAsParameter()
    {
        var source = @"
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        [ModelBinder(typeof(ComplexObjectModelBinder), Name = ""model"")]
        public string Different { get; set; }

        public void ActionMethod(TestController model) { }
    }
}";

        var result = IsProblematicParameterTest(source);
        Assert.True(result);
    }

    [Fact]
    public void IsProblematicParameter_ReturnsTrue_IfModelBinderAttributeIsUsedToRenameParameter()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        public string Model { get; set; }

        public void ActionMethod([ModelBinder(Name = ""model"")] TestController different) { }
    }
}";
        var result = IsProblematicParameterTest(source);
        Assert.True(result);
    }

    [Fact]
    public void IsProblematicParameter_ReturnsFalse_IfBindingSourceAttributeIsUsedToRenameProperty()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        [FromQuery(Name = ""different"")]
        public string Model { get; set; }

        public void ActionMethod(TestController model) { }
    }
}";
        var result = IsProblematicParameterTest(source);
        Assert.False(result);
    }

    [Fact]
    public void IsProblematicParameter_ReturnsFalse_IfBindingSourceAttributeIsUsedToRenameParameter()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        public string Model { get; set; }

        public void ActionMethod([FromRoute(Name = ""id"")] TestController model) { }
    }
}";

        var result = IsProblematicParameterTest(source);
        Assert.False(result);
    }

    [Fact]
    public void IsProblematicParameter_ReturnsFalse_ForFromBodyParameter()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        public string Model { get; set; }

        public void ActionMethod([FromBody] IsProblematicParameter_ReturnsFalse_ForFromBodyParameter model) { }
    }
}";

        var result = IsProblematicParameterTest(source);
        Assert.False(result);
    }

    [Fact]
    public void IsProblematicParameter_ReturnsFalse_ForParametersWithCustomModelBinder()
    {
        var source = @"
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        public string Model { get; set; }

        public void ActionMethod(
            [ModelBinder(typeof(SimpleTypeModelBinder))] TestController model) { }
    }
}";

        var result = IsProblematicParameterTest(source);
        Assert.False(result);
    }

    // Test for https://github.com/dotnet/aspnetcore/issues/6945
    [Fact]
    public void IsProblematicParameter_ReturnsFalse_ForSimpleTypes()
    {
        var source = @"using System;

namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        public void ActionMethod(DateTime date, DateTime? day, Uri absoluteUri, Version majorRevision, DayOfWeek sunday) { }
    }
}";
        var compilation = TestCompilation.Create(source);

        var modelType = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles.TestController");
        var method = (IMethodSymbol)modelType.GetMembers("ActionMethod").First();

        Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache));

        Assert.Collection(
            method.Parameters,
            p => Assert.False(TopLevelParameterNameAnalyzer.IsProblematicParameter(symbolCache, p)),
            p => Assert.False(TopLevelParameterNameAnalyzer.IsProblematicParameter(symbolCache, p)),
            p => Assert.False(TopLevelParameterNameAnalyzer.IsProblematicParameter(symbolCache, p)),
            p => Assert.False(TopLevelParameterNameAnalyzer.IsProblematicParameter(symbolCache, p)),
            p => Assert.False(TopLevelParameterNameAnalyzer.IsProblematicParameter(symbolCache, p)));
    }

    [Fact]
    public void IsProblematicParameter_IgnoresStaticProperties()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        public static string Model { get; set; }

        public void ActionMethod(TestController model) { }
    }
}";

        var result = IsProblematicParameterTest(source);
        Assert.False(result);
    }

    [Fact]
    public void IsProblematicParameter_IgnoresFields()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        public string model;

        public void ActionMethod(TestController model) { }
    }
}
";
        var result = IsProblematicParameterTest(source);
        Assert.False(result);
    }

    [Fact]
    public void IsProblematicParameter_IgnoresMethods()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        public string Item() => null;

        public void ActionMethod(TestController item) { }
    }
}
";

        var result = IsProblematicParameterTest(source);
        Assert.False(result);
    }

    [Fact]
    public void IsProblematicParameter_IgnoresNonPublicProperties()
    {
        var source = @"
namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles
{
    public class TestController
    {
        protected string Model { get; set; }

        public void ActionMethod(TestController model) { }
    }
}";
        var result = IsProblematicParameterTest(source);
        Assert.False(result);
    }

    private bool IsProblematicParameterTest(string source)
    {
        var compilation = TestCompilation.Create(source);

        var modelType = compilation.Assembly.GetTypeByMetadataName("Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFiles.TestController");
        var method = (IMethodSymbol)modelType.GetMembers("ActionMethod").First();
        var parameter = method.Parameters[0];

        Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache));

        return TopLevelParameterNameAnalyzer.IsProblematicParameter(symbolCache, parameter);
    }

    [Fact]
    public void GetName_ReturnsValueFromFirstAttributeWithValue()
    {
        var source = @"
using Microsoft.AspNetCore.Mvc;
namespace TestApp
{
    public class GetNameTests
    {
        public void Action([ModelBinder(Name = ""testModelName"")] int param) { }
    }
}";

        var compilation = TestCompilation.Create(source);
        Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache));

        var type = compilation.GetTypeByMetadataName("TestApp.GetNameTests");
        var method = (IMethodSymbol)type.GetMembers("Action").First();

        var parameter = method.Parameters[0];
        var name = TopLevelParameterNameAnalyzer.GetName(symbolCache, parameter);

        Assert.Equal("testModelName", name);
    }

    [Fact]
    public void GetName_ReturnsName_IfNoAttributesAreSpecified()
    {
        var source = @"
using Microsoft.AspNetCore.Mvc;
namespace TestApp
{
    public class GetNameTests
    {
        public void Action(int param) { }
    }
}";

        var compilation = TestCompilation.Create(source);
        Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache));

        var type = compilation.GetTypeByMetadataName("TestApp.GetNameTests");
        var method = (IMethodSymbol)type.GetMembers("Action").First();

        var parameter = method.Parameters[0];
        var name = TopLevelParameterNameAnalyzer.GetName(symbolCache, parameter);

        Assert.Equal("param", name);
    }

    [Fact]
    public void GetName_ReturnsName_IfAttributeDoesNotSpecifyName()
    {
        var source = @"
using Microsoft.AspNetCore.Mvc;
namespace TestApp
{
    public class GetNameTests
    {
        public void Action([ModelBinder] int param) { }
    }
}";
        var compilation = TestCompilation.Create(source);
        Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache));

        var type = compilation.GetTypeByMetadataName("TestApp.GetNameTests");
        var method = (IMethodSymbol)type.GetMembers("Action").First();

        var parameter = method.Parameters[0];
        var name = TopLevelParameterNameAnalyzer.GetName(symbolCache, parameter);

        Assert.Equal("param", name);
    }

    [Fact]
    public void GetName_ReturnsFirstName_IfMultipleAttributesAreSpecified()
    {
        var source = @"
using Microsoft.AspNetCore.Mvc;
namespace TestApp
{
    public class GetNameTests
    {
        public void Action([ModelBinder(Name = ""name1"")][Bind(Prefix = ""name2"")] int param) { }
    }
}";
        var compilation = TestCompilation.Create(source);
        Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache));

        var type = compilation.GetTypeByMetadataName("TestApp.GetNameTests");
        var method = (IMethodSymbol)type.GetMembers("Action").First();

        var parameter = method.Parameters[0];
        var name = TopLevelParameterNameAnalyzer.GetName(symbolCache, parameter);

        Assert.Equal("name1", name);
    }

    [Fact]
    public void SpecifiesModelType_ReturnsFalse_IfModelBinderDoesNotSpecifyType()
    {
        var source = @"
using Microsoft.AspNetCore.Mvc;
namespace TestApp
{
    public class SpecifiesModelTypeTests
    {
        public void Action([ModelBinder(Name = ""Name"")] object model) { }
    }
}";
        var compilation = TestCompilation.Create(source);
        Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache));

        var type = compilation.GetTypeByMetadataName("TestApp.SpecifiesModelTypeTests");
        var method = (IMethodSymbol)type.GetMembers("Action").First();

        var parameter = method.Parameters[0];
        var result = TopLevelParameterNameAnalyzer.SpecifiesModelType(symbolCache, parameter);
        Assert.False(result);
    }

    [Fact]
    public void SpecifiesModelType_ReturnsTrue_IfModelBinderSpecifiesTypeFromConstructor()
    {
        var source = @"
using Microsoft.AspNetCore.Mvc;
namespace TestApp
{
    public class SpecifiesModelTypeTests
    {
        public void Action([ModelBinder(typeof(SimpleTypeModelBinder))] object model) { }
    }
}";
        var compilation = TestCompilation.Create(source);
        Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache));

        var type = compilation.GetTypeByMetadataName("TestApp.SpecifiesModelTypeTests");
        var method = (IMethodSymbol)type.GetMembers("Action").First();

        var parameter = method.Parameters[0];
        var result = TopLevelParameterNameAnalyzer.SpecifiesModelType(symbolCache, parameter);
        Assert.True(result);
    }

    [Fact]
    public void SpecifiesModelType_ReturnsTrue_IfModelBinderSpecifiesTypeFromProperty()
    {
        var source = @"
using Microsoft.AspNetCore.Mvc;
namespace TestApp
{
    public class SpecifiesModelTypeTests
    {
        public void Action([ModelBinder(BinderType = typeof(SimpleTypeModelBinder))] object model) { }
    }
}";
        var compilation = TestCompilation.Create(source);
        Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache));

        var type = compilation.GetTypeByMetadataName("TestApp.SpecifiesModelTypeTests");
        var method = (IMethodSymbol)type.GetMembers("Action").First();

        var parameter = method.Parameters[0];
        var result = TopLevelParameterNameAnalyzer.SpecifiesModelType(symbolCache, parameter);
        Assert.True(result);
    }

    private static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
    {
        var test = new TopLevelParameterNameCSharpAnalyzerTest(TestReferences.MetadataReferences)
        {
            TestCode = source,
            ReferenceAssemblies = TestReferences.EmptyReferenceAssemblies,
        };

        test.ExpectedDiagnostics.AddRange(expected);
        return test.RunAsync();
    }

    internal sealed class TopLevelParameterNameCSharpAnalyzerTest : CSharpAnalyzerTest<TopLevelParameterNameAnalyzer, XUnitVerifier>
    {
        public TopLevelParameterNameCSharpAnalyzerTest(ImmutableArray<MetadataReference> metadataReferences)
        {
            TestState.AdditionalReferences.AddRange(metadataReferences);
        }

        protected override IEnumerable<DiagnosticAnalyzer> GetDiagnosticAnalyzers() => new[] { new TopLevelParameterNameAnalyzer() };
    }
}
