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

using System.Globalization;
using Microsoft.AspNetCore.Testing;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

public abstract class FloatingPointTypeModelBinderTest<TFloatingPoint> where TFloatingPoint : struct
{
    public static TheoryData<Type> ConvertibleTypeData
    {
        get
        {
            return new TheoryData<Type>
                {
                    typeof(TFloatingPoint),
                    typeof(TFloatingPoint?),
                };
        }
    }

    protected abstract TFloatingPoint Twelve { get; }

    protected abstract TFloatingPoint TwelvePointFive { get; }

    protected abstract TFloatingPoint ThirtyTwoThousand { get; }

    protected abstract TFloatingPoint ThirtyTwoThousandPointOne { get; }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_ReturnsFailure_IfAttemptedValueCannotBeParsed(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider
            {
                { "theModelName", "some-value" }
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.False(bindingContext.Result.IsModelSet);
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_CreatesError_IfAttemptedValueCannotBeParsed(Type destinationType)
    {
        // Arrange
        var message = "The value 'not a number' is not valid.";
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider
            {
                { "theModelName", "not a number" },
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.False(bindingContext.Result.IsModelSet);
        Assert.Null(bindingContext.Result.Model);
        Assert.False(bindingContext.ModelState.IsValid);

        var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
        Assert.Equal(message, error.ErrorMessage);
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_CreatesError_IfAttemptedValueCannotBeCompletelyParsed(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
            {
                { "theModelName", "12_5" }
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.False(bindingContext.Result.IsModelSet);
        Assert.Null(bindingContext.Result.Model);

        var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
        Assert.Equal("The value '12_5' is not valid.", error.ErrorMessage, StringComparer.Ordinal);
        Assert.Null(error.Exception);
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_CreatesError_IfAttemptedValueContainsDisallowedWhitespace(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
            {
                { "theModelName", " 12" }
            };
        var binder = GetBinder(NumberStyles.Float & ~NumberStyles.AllowLeadingWhite);

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.False(bindingContext.Result.IsModelSet);
        Assert.Null(bindingContext.Result.Model);

        var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
        Assert.Equal("The value ' 12' is not valid.", error.ErrorMessage, StringComparer.Ordinal);
        Assert.Null(error.Exception);
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_CreatesError_IfAttemptedValueContainsDisallowedDecimal(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
            {
                { "theModelName", "12.5" }
            };
        var binder = GetBinder(NumberStyles.Float & ~NumberStyles.AllowDecimalPoint);

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.False(bindingContext.Result.IsModelSet);
        Assert.Null(bindingContext.Result.Model);

        var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
        Assert.Equal("The value '12.5' is not valid.", error.ErrorMessage, StringComparer.Ordinal);
        Assert.Null(error.Exception);
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_CreatesError_IfAttemptedValueContainsDisallowedThousandsSeparator(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
            {
                { "theModelName", "32,000" }
            };
        var binder = GetBinder(NumberStyles.Float);

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.False(bindingContext.Result.IsModelSet);
        Assert.Null(bindingContext.Result.Model);

        var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
        Assert.Equal("The value '32,000' is not valid.", error.ErrorMessage, StringComparer.Ordinal);
        Assert.Null(error.Exception);
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_ReturnsFailed_IfValueProviderEmpty(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.Equal(ModelBindingResult.Failed(), bindingContext.Result);
        Assert.Empty(bindingContext.ModelState);
    }

    [Theory]
    [InlineData("")]
    [InlineData(" \t \r\n ")]
    public async Task BindModel_CreatesError_IfTrimmedAttemptedValueIsEmpty_NonNullableDestination(string value)
    {
        // Arrange
        var message = $"The value '{value}' is invalid.";
        var bindingContext = GetBindingContext(typeof(TFloatingPoint));
        bindingContext.ValueProvider = new SimpleValueProvider
            {
                { "theModelName", value },
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.False(bindingContext.Result.IsModelSet);
        Assert.Null(bindingContext.Result.Model);

        var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
        Assert.Equal(message, error.ErrorMessage, StringComparer.Ordinal);
        Assert.Null(error.Exception);
    }

    [Theory]
    [InlineData("")]
    [InlineData(" \t \r\n ")]
    public async Task BindModel_ReturnsNull_IfTrimmedAttemptedValueIsEmpty_NullableDestination(string value)
    {
        // Arrange
        var bindingContext = GetBindingContext(typeof(TFloatingPoint?));
        bindingContext.ValueProvider = new SimpleValueProvider
            {
                { "theModelName", value }
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.Null(bindingContext.Result.Model);
        var entry = Assert.Single(bindingContext.ModelState);
        Assert.Equal("theModelName", entry.Key);
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_Twelve(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider
            {
                { "theModelName", "12" }
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.True(bindingContext.Result.IsModelSet);
        Assert.Equal(Twelve, bindingContext.Result.Model);
        Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    [ReplaceCulture("en-GB", "en-GB")]
    public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_TwelvePointFive(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider
            {
                { "theModelName", "12.5" }
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.True(bindingContext.Result.IsModelSet);
        Assert.Equal(TwelvePointFive, bindingContext.Result.Model);
        Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_FrenchTwelvePointFive(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("fr-FR"))
            {
                { "theModelName", "12,5" }
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.True(bindingContext.Result.IsModelSet);
        Assert.Equal(TwelvePointFive, bindingContext.Result.Model);
        Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_ThirtyTwoThousand(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
            {
                { "theModelName", "32,000" }
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.True(bindingContext.Result.IsModelSet);
        Assert.Equal(ThirtyTwoThousand, bindingContext.Result.Model);
        Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_ThirtyTwoThousandPointOne(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
            {
                { "theModelName", "32,000.1" }
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.True(bindingContext.Result.IsModelSet);
        Assert.Equal(ThirtyTwoThousandPointOne, bindingContext.Result.Model);
        Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
    }

    [Theory]
    [MemberData(nameof(ConvertibleTypeData))]
    public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_FrenchThirtyTwoThousandPointOne(Type destinationType)
    {
        // Arrange
        var bindingContext = GetBindingContext(destinationType);
        bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("fr-FR"))
            {
                { "theModelName", "32000,1" }
            };
        var binder = GetBinder();

        // Act
        await binder.BindModelAsync(bindingContext);

        // Assert
        Assert.True(bindingContext.Result.IsModelSet);
        Assert.Equal(ThirtyTwoThousandPointOne, bindingContext.Result.Model);
        Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
    }

    protected abstract IModelBinder GetBinder(NumberStyles numberStyles);

    private IModelBinder GetBinder()
    {
        return GetBinder(FloatingPointTypeModelBinderProvider.SupportedStyles);
    }

    private static DefaultModelBindingContext GetBindingContext(Type modelType)
    {
        return new DefaultModelBindingContext
        {
            ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(modelType),
            ModelName = "theModelName",
            ModelState = new ModelStateDictionary(),
            ValueProvider = new SimpleValueProvider() // empty
        };
    }

    private sealed class TestClass
    {
    }
}
