using System;
using Bunit;
using Microsoft.AspNetCore.Components.Web;
using Newtonsoft.Json.Linq;
using Xunit;

namespace Radzen.Blazor.Tests
{
    public class NumericTests
    {
        [Fact]
        public void Numeric_Renders_CssClasses()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            component.Render();

            Assert.Contains(@$"rz-numeric", component.Markup);
            Assert.Contains(@$"rz-numeric-up", component.Markup);
            Assert.Contains(@$"rz-numeric-down", component.Markup);
        }

        [Fact]
        public void Numeric_Renders_ValueParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var value = 3.5;

            component.SetParametersAndRender(parameters => parameters.Add<double>(p => p.Value, value));

            Assert.Contains(@$"value=""{value}""", component.Markup);
        }

        [Fact]
        public void Numeric_Respect_MinParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var minValue = 2;
            var raised = false;

            component.SetParametersAndRender(parameters =>
            {
                component.SetParametersAndRender(parameters => parameters.Add(p => p.Change, args => { raised = true; }));
                parameters.Add<double>(p => p.Value, minValue);
                parameters.Add<decimal?>(p => p.Min, minValue);
            });

            component.Find(".rz-numeric-down").Click();

            Assert.False(raised, $"Numeric value should Change event if value is less than min value.");
        }

        [Fact]
        public void Numeric_Respect_Nullable_With_MinParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<double?>>();

            var raised = false;
            var value = 3.5;
            object newValue = null;

            component.SetParametersAndRender(parameters =>
            {
                parameters.Add(p => p.Change, args => { raised = true; newValue = args; });
                parameters.Add<decimal?>(p => p.Min, 1);
            });

            component.Find("input").Change(value);

            Assert.True(raised);
            Assert.True(object.Equals(value, newValue));

            component.Find("input").Change("");

            Assert.True(raised);
            Assert.True(object.Equals(null, newValue));
        }

        [Fact]
        public void Numeric_Respect_MaxParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var maxValue = 10;
            var raised = false;

            component.SetParametersAndRender(parameters =>
            {
                component.SetParametersAndRender(parameters => parameters.Add(p => p.Change, args => { raised = true; }));
                parameters.Add<double>(p => p.Value, maxValue);
                parameters.Add<decimal?>(p => p.Max, maxValue);
            });

            component.Find(".rz-numeric-up").Click();

            Assert.False(raised, $"Numeric value should Change event if value is less than min value.");
        }

        [Fact]
        public void Numeric_Renders_StyleParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var value = "width:20px";

            component.SetParametersAndRender(parameters => parameters.Add(p => p.Style, value));

            Assert.Contains(@$"style=""{value}""", component.Markup);
        }

        [Fact]
        public void Numeric_Renders_UnmatchedParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            component.SetParametersAndRender(parameters => parameters.AddUnmatched("autofocus", ""));

            Assert.Contains(@$"autofocus", component.Markup);
        }


        [Fact]
        public void Numeric_Renders_NameParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var value = "Test";

            component.SetParametersAndRender(parameters => parameters.Add(p => p.Name, value));

            Assert.Contains(@$"name=""{value}""", component.Markup);
        }

        [Fact]
        public void Numeric_Renders_TabIndexParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var value = 1;

            component.SetParametersAndRender(parameters => parameters.Add<int>(p => p.TabIndex, value));

            Assert.Contains(@$"tabindex=""{value}""", component.Markup);
        }

        [Fact]
        public void Numeric_Renders_PlaceholderParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var value = "Test";

            component.SetParametersAndRender(parameters => parameters.Add(p => p.Placeholder, value));

            Assert.Contains(@$"placeholder=""{value}""", component.Markup);
        }

        [Fact]
        public void Numeric_Renders_DisabledParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.Disabled, true));

            Assert.Contains(@$"disabled", component.Markup);
            Assert.Contains(@$"rz-state-disabled", component.Markup);
        }

        [Fact]
        public void Numeric_Renders_ReadOnlyParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var value = true;

            component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.ReadOnly, value));

            Assert.Contains(@$"readonly", component.Markup);
        }

        [Fact]
        public void Numeric_Renders_AutoCompleteParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));

            Assert.Contains(@$"autocomplete=""off""", component.Markup);
            Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);

            component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));

            Assert.Contains(@$"autocomplete=""on""", component.Markup);
            Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);

            component.SetParametersAndRender(parameters => parameters.AddUnmatched("autocomplete", "custom"));

            Assert.Contains(@$"autocomplete=""custom""", component.Markup);
            Assert.DoesNotContain(@$"aria-autocomplete", component.Markup);

            component.Instance.DefaultAutoCompleteAttribute = "autocomplete-custom";
            component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));

            Assert.Contains(@$"autocomplete=""autocomplete-custom""", component.Markup);
            Assert.Contains(@$"aria-autocomplete=""none""", component.Markup);
        }

        [Fact]
        public void Numeric_Renders_TypedAutoCompleteParameter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", false));
            component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.On));

            Assert.Contains(@$"autocomplete=""off""", component.Markup);

            component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
            component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.Off));

            Assert.Contains(@$"autocomplete=""off""", component.Markup);

            component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
            component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.BdayMonth));

            Assert.Contains(@$"autocomplete=""{AutoCompleteType.BdayMonth.GetAutoCompleteValue()}""", component.Markup);

            component.SetParametersAndRender(parameters => parameters.AddUnmatched("AutoComplete", true));
            component.SetParametersAndRender(parameters => parameters.Add<AutoCompleteType>(p => p.AutoCompleteType, AutoCompleteType.BdayYear));

            Assert.Contains(@$"autocomplete=""{AutoCompleteType.BdayYear.GetAutoCompleteValue()}""", component.Markup);
        }

        [Fact]
        public void Numeric_Raises_ChangedEvent()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var raised = false;
            var value = 3.5;
            object newValue = null;

            component.SetParametersAndRender(parameters => parameters.Add(p => p.Change, args => { raised = true; newValue = args; }));

            component.Find("input").Change(value);

            Assert.True(raised);
            Assert.True(object.Equals(value, newValue));
        }

        [Fact]
        public void Numeric_Raises_ChangeEvent_OnBackspace_When_Immediate()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<double?>>(parameters =>
                parameters.Add(p => p.Immediate, true).Add(p => p.Value, 5));

            var raised = false;
            object newValue = 1;

            ctx.JSInterop.Setup<string>("Radzen.getInputValue", _ => true).SetResult("");

            component.SetParametersAndRender(parameters => parameters.Add(p => p.Change, args => { raised = true; newValue = args; }));

            component.Find("input").KeyDown(new KeyboardEventArgs { Key = "Backspace", Code = "Backspace" });

            Assert.True(raised);
            Assert.Null(newValue);
        }

        [Fact]
        public void Numeric_Raises_ChangeEvent_OnDelete_When_Immediate()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<double?>>(parameters =>
                parameters.Add(p => p.Immediate, true).Add(p => p.Value, 5));

            var raised = false;
            object newValue = 1;

            ctx.JSInterop.Setup<string>("Radzen.getInputValue", _ => true).SetResult("");

            component.SetParametersAndRender(parameters => parameters.Add(p => p.Change, args => { raised = true; newValue = args; }));

            component.Find("input").KeyDown(new KeyboardEventArgs { Key = "Delete", Code = "Delete" });

            Assert.True(raised);
            Assert.Null(newValue);
        }

        [Fact]
        public void Numeric_Restores_Cursor_After_Immediate_KeyDown()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            ctx.JSInterop.Setup<int[]>("Radzen.getSelectionRange", _ => true).SetResult(new[] { 3, 3 });
            ctx.JSInterop.Setup<string>("Radzen.getInputValue", _ => true).SetResult("21345.00");

            var component = ctx.RenderComponent<RadzenNumeric<decimal?>>(parameters =>
                parameters.Add(p => p.Immediate, true).Add(p => p.Format, "#.00").Add(p => p.Value, 2345m));

            component.Find("input").KeyDown(new KeyboardEventArgs { Key = "1", Code = "Digit1" });

            var invocations = ctx.JSInterop.Invocations["Radzen.setSelectionRange"];
            Assert.NotEmpty(invocations);
            var invocation = invocations[invocations.Count - 1];
            Assert.Equal(3, invocation.Arguments[1]);
            Assert.Equal(3, invocation.Arguments[2]);
        }

        [Fact]
        public void Numeric_Raises_ValueChangedEvent()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var raised = false;
            var value = 3.5;
            object newValue = null;

            component.SetParametersAndRender(parameters => parameters.Add(p => p.ValueChanged, args => { raised = true; newValue = args; }));

            component.Find("input").Change(value);

            Assert.True(raised);
            Assert.True(object.Equals(value, newValue));
        }

        [Fact]
        public void Numeric_Raises_ChangedAndValueChangedEventOnStepUp()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var raised = false;
            var value = 3.5;
            var expectedValue = 5.5;
            object newValue = null;

            component.SetParametersAndRender(parameters =>
            {
                parameters.Add<double>(p => p.Value, value);
                parameters.Add(p => p.Step, "2");
                parameters.Add(p => p.Change, args => { raised = true; newValue = args; });
            });

            component.Find(".rz-numeric-up").Click();

            Assert.True(raised, "Numeric Change should be raised on step up");
            Assert.True(object.Equals(expectedValue, newValue), $"Numeric value should be incremented on step up. Expected value: {expectedValue}, value: {newValue}");

            raised = false;

            component.SetParametersAndRender(parameters => parameters.Add(p => p.ValueChanged, args => { raised = true; }));

            component.Find(".rz-numeric-up").Click();

            Assert.True(raised, "Numeric ValueChanged should be raised on step up");
        }

        [Fact]
        public void Numeric_Raises_ChangedAndValueChangedEventOnStepDown()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            var raised = false;
            var value = 3.5;
            var expectedValue = 1.5;
            object newValue = null;

            component.SetParametersAndRender(parameters =>
            {
                parameters.Add<double>(p => p.Value, value);
                parameters.Add(p => p.Step, "2");
                parameters.Add(p => p.Change, args => { raised = true; newValue = args; });
            });

            component.Find(".rz-numeric-down").Click();

            Assert.True(raised, "Numeric Change should be raised on step up");
            Assert.True(object.Equals(expectedValue, newValue), $"Numeric value should be incremented on step up. Expected value: {expectedValue}, value: {newValue}");

            raised = false;

            component.SetParametersAndRender(parameters => parameters.Add(p => p.ValueChanged, args => { raised = true; }));

            component.Find(".rz-numeric-down").Click();

            Assert.True(raised, "Numeric ValueChanged should be raised on step up");
        }

        [Fact]
        public void Numeric_UpDown_Rendered()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>();

            component.Render();

            Assert.Contains(@$"rz-numeric-button-icon", component.Markup);
            Assert.Contains(@$"rz-numeric-up", component.Markup);
            Assert.Contains(@$"rz-numeric-down", component.Markup);
        }

        [Fact]
        public void Numeric_UpDown_NotRenderedIfHidden()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var component = ctx.RenderComponent<RadzenNumeric<double>>(ComponentParameter.CreateParameter(nameof(RadzenNumeric<double>.ShowUpDown), false));

            component.Render();

            Assert.DoesNotContain(@$"rz-numeric-button-icon", component.Markup);
            Assert.DoesNotContain(@$"rz-numeric-up", component.Markup);
            Assert.DoesNotContain(@$"rz-numeric-down", component.Markup);
        }

        [Fact]
        public void Numeric_Formatted()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            double valueToTest = 100.234;
            string format = "0.00";

            var component = ctx.RenderComponent<RadzenNumeric<double>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<double>.Format), format),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<double>.Value), valueToTest)
            );

            component.Render();

            Assert.Contains($" value=\"{valueToTest.ToString(format)}\"", component.Markup);
        }
        
        public static TheoryData<decimal, decimal> NumericFormatterPreservesLeadingZerosData =>
            new()
            {
                { 10.000m, 100.000m },
                { 100.000m, 10.000m }
            };
        
        [Theory]
        [MemberData(nameof(NumericFormatterPreservesLeadingZerosData))]
        public void Numeric_Formatter_PreservesLeadingZeros(decimal oldValue, decimal newValue)
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            string format = "0.000";

            var component = ctx.RenderComponent<RadzenNumeric<decimal>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<decimal>.Format), format),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<decimal>.Value), oldValue)
            );

            component.Render();
            
            Assert.Contains($" value=\"{oldValue.ToString(format)}\"", component.Markup);

            component.Find("input").Change(newValue);
            
            Assert.Contains($" value=\"{newValue.ToString(format)}\"", component.Markup);
        }

        [Fact]
        public void Numeric_Uses_ConvertValue()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var value = new Dollars(11m);
            Dollars? ConvertFunc(string s) => decimal.TryParse(s, System.Globalization.CultureInfo.InvariantCulture, out var val) ? new Dollars(val) : null;
            var component = ctx.RenderComponent<RadzenNumeric<Dollars?>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars?>.ConvertValue), (Func<string, Dollars?>)ConvertFunc),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars?>.Value), value),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.InvariantCulture)
            );

            component.Render();
            
            Assert.Contains($" value=\"{value.ToString()}\"", component.Markup);

            var newValue = new Dollars(13.53m);
            component.Find("input").Change("13.53");

            Assert.Contains($" value=\"{newValue.ToString()}\"", component.Markup);
        }

        [Fact]
        public void Numeric_Supports_TypeConverter()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var valueToTest = new Dollars(100.234m);
            string format = "0.00";

            var component = ctx.RenderComponent<RadzenNumeric<Dollars>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Format), format),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Value), valueToTest)
            );

            component.Render();

            Assert.Contains($" value=\"{valueToTest.ToString(format, System.Globalization.CultureInfo.CurrentCulture)}\"", component.Markup);
        }
        [Fact]
        public void Numeric_Supports_TypeConverterWithCulture()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var valueToTest = new Dollars(100.234m);
            string format = "0.00";

            var component = ctx.RenderComponent<RadzenNumeric<Dollars>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Format), format),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Value), valueToTest),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.InvariantCulture)
            );

            component.Render();

            Assert.Contains($" value=\"{valueToTest.ToString(format, System.Globalization.CultureInfo.InvariantCulture)}\"", component.Markup);
        }

        [Fact]
        public void Numeric_Supports_EmptyString()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var valueToTest = "";
            string format = "0.00";

            var component = ctx.RenderComponent<RadzenNumeric<string>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Format), format),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Value), valueToTest),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.InvariantCulture)
            );

            component.Render();

            Assert.Contains($" value=\"0.00\"", component.Markup);
        }

        [Fact]
        public void Numeric_Supports_ValueString()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var valueToTest = "12.50";
            string format = "0.00";

            var component = ctx.RenderComponent<RadzenNumeric<string>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Format), format),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Value), valueToTest),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.InvariantCulture)
            );

            component.Render();

            Assert.Contains($" value=\"{valueToTest}\"", component.Markup);
        }

        [Fact]
        public void Numeric_Supports_ValueStringEsCLCulture()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var valueToTest = "12,50";
            string format = "0.00";

            var component = ctx.RenderComponent<RadzenNumeric<string>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Format), format),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Value), valueToTest),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.GetCultureInfo("es-CL"))
            );

            component.Render();

            Assert.Contains($" value=\"{valueToTest}\"", component.Markup);
        }

        [Fact]
        public void Numeric_Supports_ValueStringEnUSCulture()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var valueToTest = "12.50";
            string format = "0.00";

            var component = ctx.RenderComponent<RadzenNumeric<string>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Format), format),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<string>.Value), valueToTest),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Dollars>.Culture), System.Globalization.CultureInfo.GetCultureInfo("en-US"))
            );

            component.Render();

            Assert.Contains($" value=\"{valueToTest}\"", component.Markup);
        }

        [Fact]
        public void Numeric_Supports_IComparable()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<Dollars>>();

            var maxValue = 2;

            component.SetParametersAndRender(parameters =>
            {
                component.SetParametersAndRender(parameters =>
                {
                    parameters.Add(p => p.Value, new Dollars(1m));
                    parameters.Add(p => p.Max, maxValue);
                });
            });
            
            component.Find("input").Change(13.53);

            var maxDollars = new Dollars(maxValue);
            Assert.Contains($" value=\"{maxDollars}\"", component.Markup);
            Assert.Equal(component.Instance.Value, maxDollars);
        }

        [Fact]
        public void Numeric_Format_WithOptionalDecimals_PreservesValue()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<float>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<float>.Format), "0.##"),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<float>.Culture), System.Globalization.CultureInfo.InvariantCulture)
            );

            object newValue = null;
            component.SetParametersAndRender(parameters =>
                parameters.Add(p => p.Change, args => { newValue = args; }));

            component.Find("input").Change("10.55");

            Assert.NotNull(newValue);
            Assert.Equal(10.55f, (float)newValue, 2);
        }

        [Fact]
        public void Numeric_Format_WithOptionalDecimals_PreservesValue_CommaCulture()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var culture = System.Globalization.CultureInfo.GetCultureInfo("de-DE");

            var component = ctx.RenderComponent<RadzenNumeric<float>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<float>.Format), "0.##"),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<float>.Culture), culture)
            );

            object newValue = null;
            component.SetParametersAndRender(parameters =>
                parameters.Add(p => p.Change, args => { newValue = args; }));

            component.Find("input").Change("10,55");

            Assert.NotNull(newValue);
            Assert.Equal(10.55f, (float)newValue, 2);
        }

        [Fact]
        public void Numeric_Supports_IFormattable()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;

            var valueToTest = new Temperature(60.23m);
            const string format = "F";

            var component = ctx.RenderComponent<RadzenNumeric<Temperature>>(
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Temperature>.Format), format),
                ComponentParameter.CreateParameter(nameof(RadzenNumeric<Temperature>.Value), valueToTest)
            );

            component.Render();

            var input = component.Find("input").GetAttribute("value");
            input.MarkupMatches(valueToTest.ToString(format));
        }

        [Fact]
        public void Numeric_Raises_KeyDownEvent()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<int>>(parameters => parameters.Add(p => p.Value, 5));

            NumericKeyboardEventArgs raised = null;

            component.SetParametersAndRender(parameters => parameters.Add(p => p.KeyDown, args => { raised = args; }));

            component.Find("input").KeyDown(new KeyboardEventArgs { Key = "ArrowUp", Code = "ArrowUp" });

            Assert.NotNull(raised);
            Assert.Equal("ArrowUp", raised.OriginalEvent.Code);
            Assert.False(raised.IsDefaultPrevented);
        }

        [Fact]
        public void Numeric_KeyDown_PreventDefault_Suppresses_ArrowUp_Increment()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<int>>(parameters => parameters.Add(p => p.Value, 5));

            var changeRaised = false;

            component.SetParametersAndRender(parameters => parameters
                .Add(p => p.KeyDown, args => args.PreventDefault())
                .Add(p => p.Change, _ => changeRaised = true));

            component.Find("input").KeyDown(new KeyboardEventArgs { Key = "ArrowUp", Code = "ArrowUp" });

            Assert.False(changeRaised);
            Assert.Equal(5, component.Instance.Value);
        }

        [Fact]
        public void Numeric_KeyDown_PreventDefault_Suppresses_ArrowDown_Decrement()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<int>>(parameters => parameters.Add(p => p.Value, 5));

            var changeRaised = false;

            component.SetParametersAndRender(parameters => parameters
                .Add(p => p.KeyDown, args => args.PreventDefault())
                .Add(p => p.Change, _ => changeRaised = true));

            component.Find("input").KeyDown(new KeyboardEventArgs { Key = "ArrowDown", Code = "ArrowDown" });

            Assert.False(changeRaised);
            Assert.Equal(5, component.Instance.Value);
        }

        [Fact]
        public void Numeric_KeyDown_Without_PreventDefault_Still_Increments_On_ArrowUp()
        {
            using var ctx = new TestContext();
            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");

            var component = ctx.RenderComponent<RadzenNumeric<int>>(parameters => parameters.Add(p => p.Value, 5));

            var changeRaised = false;

            component.SetParametersAndRender(parameters => parameters
                .Add(p => p.KeyDown, _ => { })
                .Add(p => p.Change, _ => changeRaised = true));

            component.Find("input").KeyDown(new KeyboardEventArgs { Key = "ArrowUp", Code = "ArrowUp" });

            Assert.True(changeRaised);
            Assert.Equal(6, component.Instance.Value);
        }
    }
}
