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

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Testing;

namespace Microsoft.AspNetCore.Mvc.IntegrationTests;

public class BinderTypeBasedModelBinderIntegrationTest
{
    [Fact]
    public async Task BindParameter_WithModelBinderType_NullData_ReturnsNull()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo()
            {
                BinderType = typeof(NullModelBinder)
            },

            ParameterType = typeof(string)
        };

        // No data is passed.
        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert

        // ModelBindingResult
        Assert.True(modelBindingResult.IsModelSet);
        Assert.Null(modelBindingResult.Model);

        // ModelState (not set unless inner binder sets it)
        Assert.True(modelState.IsValid);
        Assert.Empty(modelState);
    }

    [Fact]
    public async Task BindParameter_WithModelBinderType_NoData()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo()
            {
                BinderType = typeof(NullModelNotSetModelBinder)
            },

            ParameterType = typeof(string)
        };

        // No data is passed.
        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        Assert.False(modelBindingResult.IsModelSet);

        // ModelState (not set unless inner binder sets it)
        Assert.True(modelState.IsValid);
        Assert.Empty(modelState);
    }

    private class Person2
    {
    }

    // Ensures that prefix is part of the result returned back.
    [Fact]
    [ReplaceCulture]
    public async Task BindParameter_WithData_WithPrefix_GetsBound()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo()
            {
                BinderType = typeof(SuccessModelBinder),
                BinderModelName = "CustomParameter"
            },

            ParameterType = typeof(Person2)
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert

        // ModelBindingResult
        Assert.True(modelBindingResult.IsModelSet);
        Assert.Equal("Success", modelBindingResult.Model);

        // ModelState
        Assert.True(modelState.IsValid);
        var key = Assert.Single(modelState.Keys);
        Assert.Equal("CustomParameter", key);
        Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
        Assert.NotNull(modelState[key].RawValue); // Value is set by test model binder, no need to validate it.
    }

    private class Person
    {
        public Address Address { get; set; }
    }

    [ModelBinder<AddressModelBinder>]
    private class Address
    {
        public string Street { get; set; }
    }

    public static TheoryData<BindingInfo> NullAndEmptyBindingInfo
    {
        get
        {
            return new TheoryData<BindingInfo>
                {
                    null,
                    new BindingInfo(),
                };
        }
    }

    // Make sure the metadata is honored when a [ModelBinder] attribute is associated with an action parameter's
    // type. This should behave identically to such an attribute on an action parameter. (Tests such as
    // BindParameter_WithData_WithPrefix_GetsBound cover associating [ModelBinder] with an action parameter.)
    //
    // This is a regression test for aspnet/Mvc#4652 and aspnet/Mvc#7595
    [Theory]
    [MemberData(nameof(NullAndEmptyBindingInfo))]
    public async Task BinderTypeOnParameterType_WithData_EmptyPrefix_GetsBound(BindingInfo bindingInfo)
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameters = typeof(TestController).GetMethod(nameof(TestController.Action)).GetParameters();
        var parameter = new ControllerParameterDescriptor
        {
            Name = "Parameter1",
            BindingInfo = bindingInfo,
            ParameterInfo = parameters[0],
            ParameterType = typeof(Address),
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        // ModelBindingResult
        Assert.True(modelBindingResult.IsModelSet);

        // Model
        var address = Assert.IsType<Address>(modelBindingResult.Model);
        Assert.Equal("SomeStreet", address.Street);

        // ModelState
        Assert.True(modelState.IsValid);
        var kvp = Assert.Single(modelState);
        Assert.Equal("Street", kvp.Key);
        var entry = kvp.Value;
        Assert.NotNull(entry);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.NotNull(entry.RawValue); // Value is set by test model binder, no need to validate it.
    }

    private class Person3
    {
        [ModelBinder<Address3ModelBinder>]
        public Address3 Address { get; set; }
    }

    private class Address3
    {
        public string Street { get; set; }
    }

    // Make sure the metadata is honored when a [ModelBinder] attribute is associated with a property in the type
    // hierarchy of an action parameter. (Tests such as BindProperty_WithData_EmptyPrefix_GetsBound cover
    // associating [ModelBinder] with a class somewhere in the type hierarchy of an action parameter.)
    [Theory]
    [MemberData(nameof(NullAndEmptyBindingInfo))]
    public async Task BinderTypeOnProperty_WithData_EmptyPrefix_GetsBound(BindingInfo bindingInfo)
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor
        {
            Name = "Parameter1",
            BindingInfo = bindingInfo,
            ParameterType = typeof(Person3),
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert
        // ModelBindingResult
        Assert.True(modelBindingResult.IsModelSet);

        // Model
        var person = Assert.IsType<Person3>(modelBindingResult.Model);
        Assert.NotNull(person.Address);
        Assert.Equal("SomeStreet", person.Address.Street);

        // ModelState
        Assert.True(modelState.IsValid);
        var kvp = Assert.Single(modelState);
        Assert.Equal("Address.Street", kvp.Key);
        var entry = kvp.Value;
        Assert.NotNull(entry);
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.NotNull(entry.RawValue); // Value is set by test model binder, no need to validate it.
    }

    [Fact]
    public async Task BindProperty_WithData_EmptyPrefix_GetsBound()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo(),
            ParameterType = typeof(Person)
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert

        // ModelBindingResult
        Assert.True(modelBindingResult.IsModelSet);

        // Model
        var boundPerson = Assert.IsType<Person>(modelBindingResult.Model);
        Assert.NotNull(boundPerson.Address);
        Assert.Equal("SomeStreet", boundPerson.Address.Street);

        // ModelState
        Assert.True(modelState.IsValid);
        var key = Assert.Single(modelState.Keys);
        Assert.Equal("Address.Street", key);
        Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
        Assert.NotNull(modelState[key].RawValue); // Value is set by test model binder, no need to validate it.
    }

    [Fact]
    public async Task BindProperty_WithData_WithPrefix_GetsBound()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo()
            {
                BinderModelName = "CustomParameter"
            },
            ParameterType = typeof(Person)
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert

        // ModelBindingResult
        Assert.True(modelBindingResult.IsModelSet);

        // Model
        var boundPerson = Assert.IsType<Person>(modelBindingResult.Model);
        Assert.NotNull(boundPerson.Address);
        Assert.Equal("SomeStreet", boundPerson.Address.Street);

        // ModelState
        Assert.True(modelState.IsValid);
        var key = Assert.Single(modelState.Keys);
        Assert.Equal("CustomParameter.Address.Street", key);
        Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
        Assert.NotNull(modelState[key].RawValue); // Value is set by test model binder, no need to validate it.
    }

    private class AddressModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            ArgumentNullException.ThrowIfNull(bindingContext);

            Debug.Assert(bindingContext.Result == ModelBindingResult.Failed());

            if (bindingContext.ModelType != typeof(Address))
            {
                return Task.CompletedTask;
            }

            var address = new Address() { Street = "SomeStreet" };

            bindingContext.ModelState.SetModelValue(
                ModelNames.CreatePropertyModelName(bindingContext.ModelName, "Street"),
                new string[] { address.Street },
                address.Street);

            bindingContext.Result = ModelBindingResult.Success(address);
            return Task.CompletedTask;
        }
    }

    private class Address3ModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            ArgumentNullException.ThrowIfNull(bindingContext);

            Debug.Assert(bindingContext.Result == ModelBindingResult.Failed());

            if (bindingContext.ModelType != typeof(Address3))
            {
                return Task.CompletedTask;
            }

            var address = new Address3 { Street = "SomeStreet" };

            bindingContext.ModelState.SetModelValue(
                ModelNames.CreatePropertyModelName(bindingContext.ModelName, "Street"),
                new string[] { address.Street },
                address.Street);

            bindingContext.Result = ModelBindingResult.Success(address);
            return Task.CompletedTask;
        }
    }

    private class SuccessModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            ArgumentNullException.ThrowIfNull(bindingContext);
            Debug.Assert(bindingContext.Result == ModelBindingResult.Failed());

            var model = "Success";
            bindingContext.ModelState.SetModelValue(
                bindingContext.ModelName,
                new string[] { model },
                model);

            bindingContext.Result = ModelBindingResult.Success(model);
            return Task.CompletedTask;
        }
    }

    private class NullModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            ArgumentNullException.ThrowIfNull(bindingContext);
            Debug.Assert(bindingContext.Result == ModelBindingResult.Failed());

            bindingContext.Result = ModelBindingResult.Success(model: null);
            return Task.CompletedTask;
        }
    }

    private class NullModelNotSetModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            ArgumentNullException.ThrowIfNull(bindingContext);
            Debug.Assert(bindingContext.Result == ModelBindingResult.Failed());

            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        }
    }

    private class TestController
    {
        public void Action(Address address)
        {
        }
    }
}
