using System;
using System.ComponentModel;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Markup.Data;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;

namespace Avalonia.Controls.UnitTests.Primitives
{
    public class RangeBaseTests
    {
        [Fact]
        public void Maximum_Should_Be_Coerced_To_Minimum()
        {
            var target = new TestRange
            {
                Minimum = 100,
                Maximum = 50,
            };
            var root = new TestRoot(target);

            Assert.Equal(100, target.Minimum);
            Assert.Equal(100, target.Maximum);
        }

        [Fact]
        public void Value_Should_Be_Coerced_To_Range()
        {
            var target = new TestRange
            {
                Minimum = 0,
                Maximum = 50,
                Value = 100,
            };
            var root = new TestRoot(target);

            Assert.Equal(0, target.Minimum);
            Assert.Equal(50, target.Maximum);
            Assert.Equal(50, target.Value);
        }

        [Fact]
        public void Changing_Minimum_Should_Coerce_Value_And_Maximum()
        {
            var target = new TestRange
            {
                Minimum = 0,
                Maximum = 100,
                Value = 50,
            };
            var root = new TestRoot(target);

            target.Minimum = 200;

            Assert.Equal(200, target.Minimum);
            Assert.Equal(200, target.Maximum);
            Assert.Equal(200, target.Value);
        }

        [Fact]
        public void Changing_Maximum_Should_Coerce_Value()
        {
            var target = new TestRange
            {
                Minimum = 0,
                Maximum = 100,
                Value = 100,
            };
            var root = new TestRoot(target);

            target.Maximum = 50;

            Assert.Equal(0, target.Minimum);
            Assert.Equal(50, target.Maximum);
            Assert.Equal(50, target.Value);
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void SetValue_Should_Not_Cause_StackOverflow(bool useXamlBinding)
        {
            var viewModel = new TestStackOverflowViewModel()
            {
                Value = 50
            };

            Track track = null;

            var target = new TestRange()
            {
                Template = new FuncControlTemplate<RangeBase>((c, scope) =>
                {
                    track = new Track()
                    {
                        Width = 100,
                        Orientation = Orientation.Horizontal,
                        [~~Track.MinimumProperty] = c[~~RangeBase.MinimumProperty],
                        [~~Track.MaximumProperty] = c[~~RangeBase.MaximumProperty],

                        Name = "PART_Track",
                        Thumb = new Thumb()
                    }.RegisterInNameScope(scope);

                    if (useXamlBinding)
                    {
                        track.Bind(Track.ValueProperty, new Binding("Value")
                                                    {
                                                        Mode = BindingMode.TwoWay,
                                                        Source = c,
                                                        Priority = BindingPriority.Style
                                                    });
                    }
                    else
                    {
                        track[~~Track.ValueProperty] = c[~~RangeBase.ValueProperty];
                    }

                    return track;
                }),
                Minimum = 0,
                Maximum = 100,
                DataContext = viewModel
            };

            target.Bind(TestRange.ValueProperty, new Binding("Value") { Mode = BindingMode.TwoWay });

            target.ApplyTemplate();
            track.Measure(new Size(100, 0));
            track.Arrange(new Rect(0, 0, 100, 0));

            Assert.Equal(1, viewModel.SetterInvokedCount);

            // Issues #855 and #824 were causing a StackOverflowException at this point.
            target.Value = 51.001;

            Assert.Equal(2, viewModel.SetterInvokedCount);

            double expected = 51;

            Assert.Equal(expected, viewModel.Value);
            Assert.Equal(expected, target.Value);
            Assert.Equal(expected, track.Value);
        }

        [Fact]
        public void Coercion_Should_Not_Be_Done_During_Initialization()
        {
            var target = new TestRange();

            target.BeginInit();

            var root = new TestRoot(target);
            target.Minimum = 1;
            Assert.Equal(0, target.Value);

            target.Value = 50;
            target.EndInit();

            Assert.Equal(50, target.Value);
        }

        [Fact]
        public void Coercion_Should_Be_Done_After_Initialization()
        {
            var target = new TestRange();

            target.BeginInit();

            var root = new TestRoot(target);
            target.Minimum = 1;

            target.EndInit();

            Assert.Equal(1, target.Value);
        }

        private class TestRange : RangeBase
        {
        }

        private class TestStackOverflowViewModel : INotifyPropertyChanged
        {
            public int SetterInvokedCount { get; private set; }

            public const int MaxInvokedCount = 1000;

            private double _value;

            public event PropertyChangedEventHandler PropertyChanged;

            public double Value
            {
                get { return _value; }
                set
                {
                    if (_value != value)
                    {
                        SetterInvokedCount++;
                        if (SetterInvokedCount < MaxInvokedCount)
                        {
                            _value = (int)value;
                            if (_value > 75) _value = 75;
                            if (_value < 25) _value = 25;
                        }
                        else
                        {
                            _value = value;
                        }

                        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
                    }
                }
            }
        }
    }
}
