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

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

namespace Microsoft.AspNetCore.Components;

public class EventCallbackFactoryBinderExtensionsTest
{
    [Fact]
    public async Task CreateBinder_SwallowsConversionException()
    {
        // Arrange
        var value = 17;
        var component = new EventCountingComponent();
        Action<int> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "not-an-integer!", });

        Assert.Equal(17, value); // Setter not called
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_IfConverterThrows_ConvertsEmptyStringToDefault()
    {
        // Arrange
        var value = 17;
        var component = new EventCountingComponent();
        Action<int> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });

        Assert.Equal(0, value); // Calls setter to apply default value for this type
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_ThrowsSetterException()
    {
        // Arrange
        var component = new EventCountingComponent();
        Action<int> setter = (_) => { throw new InvalidTimeZoneException(); };

        var binder = EventCallback.Factory.CreateBinder(component, setter, 17);

        // Act
        await Assert.ThrowsAsync<InvalidTimeZoneException>(() =>
        {
            return binder.InvokeAsync(new ChangeEventArgs() { Value = "18", });
        });

        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_BindsEmpty_DoesNotCallSetter()
    {
        // Arrange
        var value = 17;
        var component = new EventCountingComponent();
        Action<int> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "not-an-integer!", });

        Assert.Equal(17, value); // Setter not called
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_BindsEmpty_CallsSetterForNullable()
    {
        // Arrange
        var value = (int?)17;
        var component = new EventCountingComponent();
        Action<int?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "", });

        Assert.Null(value); // Setter called
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_String()
    {
        // Arrange
        var value = "hi";
        var component = new EventCountingComponent();
        Action<string> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = "bye";

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue, });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_Bool()
    {
        // Arrange
        var value = false;
        var component = new EventCountingComponent();
        Action<bool> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = true;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = true, });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableBool()
    {
        // Arrange
        var value = (bool?)false;
        var component = new EventCountingComponent();
        Action<bool?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = (bool?)true;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = true, });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_Int()
    {
        // Arrange
        var value = 17;
        var component = new EventCountingComponent();
        Action<int> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = 42;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableInt()
    {
        // Arrange
        var value = (int?)17;
        var component = new EventCountingComponent();
        Action<int?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = (int?)42;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_Long()
    {
        // Arrange
        var value = (long)17;
        var component = new EventCountingComponent();
        Action<long> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = (long)42;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableLong()
    {
        // Arrange
        var value = (long?)17;
        var component = new EventCountingComponent();
        Action<long?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = (long?)42;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_Float()
    {
        // Arrange
        var value = (float)17;
        var component = new EventCountingComponent();
        Action<float> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = (float)42;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableFloat()
    {
        // Arrange
        var value = (float?)17;
        var component = new EventCountingComponent();
        Action<float?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = (float?)42;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_Double()
    {
        // Arrange
        var value = (double)17;
        var component = new EventCountingComponent();
        Action<double> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = (double)42;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableDouble()
    {
        // Arrange
        var value = (double?)17;
        var component = new EventCountingComponent();
        Action<double?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = (double?)42;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_Decimal()
    {
        // Arrange
        var value = (decimal)17;
        var component = new EventCountingComponent();
        Action<decimal> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = (decimal)42;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableDecimal()
    {
        // Arrange
        var value = (decimal?)17;
        var component = new EventCountingComponent();
        Action<decimal?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = (decimal?)42;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_Enum()
    {
        // Arrange
        var value = AttributeTargets.All;
        var component = new EventCountingComponent();
        Action<AttributeTargets> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = AttributeTargets.Class;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableEnum()
    {
        // Arrange
        var value = (AttributeTargets?)AttributeTargets.All;
        var component = new EventCountingComponent();
        Action<AttributeTargets?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = AttributeTargets.Class;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_DateTime()
    {
        // Arrange
        var value = DateTime.Now;
        var component = new EventCountingComponent();
        Action<DateTime> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableDateTime()
    {
        // Arrange
        var value = (DateTime?)DateTime.Now;
        var component = new EventCountingComponent();
        Action<DateTime?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_DateTime_Format()
    {
        // Arrange
        var value = DateTime.Now;
        var component = new EventCountingComponent();
        Action<DateTime> setter = (_) => value = _;
        var format = "ddd yyyy-MM-dd";

        var binder = EventCallback.Factory.CreateBinder(component, setter, value, format);

        var expectedValue = new DateTime(2018, 3, 4);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(format, CultureInfo.InvariantCulture), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableDateTime_Format()
    {
        // Arrange
        var value = (DateTime?)DateTime.Now;
        var component = new EventCountingComponent();
        Action<DateTime?> setter = (_) => value = _;
        var format = "ddd yyyy-MM-dd";

        var binder = EventCallback.Factory.CreateBinder(component, setter, value, format);

        var expectedValue = new DateTime(2018, 3, 4);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(format, CultureInfo.InvariantCulture), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_DateTimeOffset()
    {
        // Arrange
        var value = DateTimeOffset.Now;
        var component = new EventCountingComponent();
        Action<DateTimeOffset> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableDateTimeOffset()
    {
        // Arrange
        var value = (DateTimeOffset?)DateTimeOffset.Now;
        var component = new EventCountingComponent();
        Action<DateTimeOffset?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_DateTimeOffset_Format()
    {
        // Arrange
        var value = DateTimeOffset.Now;
        var component = new EventCountingComponent();
        Action<DateTimeOffset> setter = (_) => value = _;
        var format = "ddd yyyy-MM-dd";

        var binder = EventCallback.Factory.CreateBinder(component, setter, value, format);

        var expectedValue = new DateTime(2018, 3, 4);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(format, CultureInfo.InvariantCulture), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_NullableDateTimeOffset_Format()
    {
        // Arrange
        var value = (DateTimeOffset?)DateTimeOffset.Now;
        var component = new EventCountingComponent();
        Action<DateTimeOffset?> setter = (_) => value = _;
        var format = "ddd yyyy-MM-dd";

        var binder = EventCallback.Factory.CreateBinder(component, setter, value, format);

        var expectedValue = new DateTime(2018, 3, 4);

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(format, CultureInfo.InvariantCulture), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    // This uses a type converter
    [Fact]
    public async Task CreateBinder_Guid()
    {
        // Arrange
        var value = Guid.NewGuid();
        var component = new EventCountingComponent();
        Action<Guid> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = Guid.NewGuid();

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    // This uses a type converter
    [Fact]
    public async Task CreateBinder_NullableGuid()
    {
        // Arrange
        var value = (Guid?)Guid.NewGuid();
        var component = new EventCountingComponent();
        Action<Guid?> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = Guid.NewGuid();

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(), });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public async Task CreateBinder_CustomTypeConverter()
    {
        // Arrange
        var value = new SecretMessage() { Message = "A message", };
        var component = new EventCountingComponent();
        Action<SecretMessage> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value);

        var expectedValue = new SecretMessage() { Message = "TypeConverter may be old, but it still works!", };

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(), });

        Assert.Equal(expectedValue.Message, value.Message);
        Assert.Equal(1, component.Count);
    }

    [Fact]
    public void CreateBinder_GenericWithoutTypeConverter_Throws()
    {
        var value = new ClassWithoutTypeConverter();
        var component = new EventCountingComponent();
        Action<ClassWithoutTypeConverter> setter = (_) => value = _;

        var ex = Assert.Throws<InvalidOperationException>(() => EventCallback.Factory.CreateBinder(component, setter, value));

        Assert.Equal(
            $"The type '{typeof(ClassWithoutTypeConverter).FullName}' does not have an associated TypeConverter that supports conversion from a string. " +
            $"Apply 'TypeConverterAttribute' to the type to register a converter.",
            ex.Message);
    }

    [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/30312")]
    [ReplaceCulture("fr-FR", "fr-FR")]
    public async Task CreateBinder_NumericType_WithCurrentCulture()
    {
        // Arrange
        var value = 17_000;
        var component = new EventCountingComponent();
        Action<int> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value, culture: null);

        var expectedValue = 42_000;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42 000,00", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/30312")]
    public async Task CreateBinder_NumericType_WithInvariantCulture()
    {
        // Arrange
        var value = 17_000;
        var component = new EventCountingComponent();
        Action<int> setter = (_) => value = _;

        var binder = EventCallback.Factory.CreateBinder(component, setter, value, CultureInfo.InvariantCulture);

        var expectedValue = 42_000;

        // Act
        await binder.InvokeAsync(new ChangeEventArgs() { Value = "42,000.00", });

        Assert.Equal(expectedValue, value);
        Assert.Equal(1, component.Count);
    }

    private class EventCountingComponent : IComponent, IHandleEvent
    {
        public int Count;

        public Task HandleEventAsync(EventCallbackWorkItem item, object arg)
        {
            Count++;
            return item.InvokeAsync(arg);
        }

        public void Attach(RenderHandle renderHandle)
        {
            throw new System.NotImplementedException();
        }

        public Task SetParametersAsync(ParameterView parameters)
        {
            throw new System.NotImplementedException();
        }
    }

    private class ClassWithoutTypeConverter
    {
    }

    [TypeConverter(typeof(SecretMessageTypeConverter))]
    private class SecretMessage
    {
        public string Message { get; set; }

        public override string ToString()
        {
            return Message;
        }
    }

    private class SecretMessageTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType == typeof(string))
            {
                return true;
            }

            return false;
        }
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value is string message)
            {
                return new SecretMessage() { Message = message, };
            }

            return null;
        }
    }
}
