using System;
using System.ComponentModel;
using System.Linq;

using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;

using Moq;

using Xunit;

namespace Avalonia.Controls.UnitTests
{
    public class FlyoutTests
    {
        [Fact]
        public void Opening_Raises_Single_Opening_Event()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();
                window.Show();

                int tracker = 0;
                Flyout f = new Flyout();
                f.Opening += (s, e) =>
                {
                    tracker++;
                };
                f.ShowAt(window);

                Assert.Equal(1, tracker);
                Assert.True(f.IsOpen);
            }
        }

        [Fact]
        public void Opening_Raises_Single_Opened_Event()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();
                window.Show();

                int tracker = 0;
                Flyout f = new Flyout();
                f.Opened += (s, e) =>
                {
                    tracker++;
                };
                f.ShowAt(window);

                Assert.Equal(1, tracker);
            }
        }

        [Fact]
        public void Opening_Is_Cancellable()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();
                window.Show();

                int tracker = 0;
                Flyout f = new Flyout();
                f.Opening += (s, e) =>
                {
                    tracker++;
                    if (e is CancelEventArgs cancelEventArgs)
                    {
                        cancelEventArgs.Cancel = true;
                    }
                };
                f.ShowAt(window);

                Assert.Equal(1, tracker);
                Assert.False(f.IsOpen);
            }
        }

        [Fact]
        public void Closing_Raises_Single_Closing_Event()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();
                window.Show();

                int tracker = 0;
                Flyout f = new Flyout();
                f.Closing += (s, e) =>
                {
                    tracker++;
                };
                f.ShowAt(window);
                f.Hide();

                Assert.Equal(1, tracker);
            }
        }

        [Fact]
        public void Closing_Raises_Single_Closed_Event()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();
                window.Show();

                int tracker = 0;
                Flyout f = new Flyout();
                f.Closed += (s, e) =>
                {
                    tracker++;
                };
                f.ShowAt(window);
                f.Hide();

                Assert.Equal(1, tracker);
            }
        }

        [Fact]
        public void Cancel_Closing_Keeps_Flyout_Open()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();
                window.Show();

                var tracker = 0;
                var f = new Flyout();
                f.Closing += (s, e) =>
                {
                    tracker++;
                    e.Cancel = true;
                };
                f.ShowAt(window);
                f.Hide();

                Assert.True(f.IsOpen);
                Assert.Equal(1, tracker);
            }
        }

        [Fact]
        public void Cancel_Light_Dismiss_Closing_Keeps_Flyout_Open()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();
                window.Width = 100;
                window.Height = 100;

                var button = new Button
                {
                    Height = 10,
                    Width = 10,
                    HorizontalAlignment = Layout.HorizontalAlignment.Left,
                    VerticalAlignment = Layout.VerticalAlignment.Top
                };
                window.Content = button;

                window.Show();

                var tracker = 0;
                var f = new Flyout();
                f.Content = new Border { Width = 10, Height = 10 };
                f.Closing += (s, e) =>
                {
                    tracker++;
                    e.Cancel = true;
                };
                f.ShowAt(window);

                var e = CreatePointerPressedEventArgs(window, new Point(90, 90));
                var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
                overlay.RaiseEvent(e);

                Assert.Equal(1, tracker);
                Assert.True(f.IsOpen);
            }
        }

        [Fact]
        public void Light_Dismiss_Closes_Flyout()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();
                window.Width = 100;
                window.Height = 100;

                var button = new Button
                {
                    Height = 10,
                    Width = 10,
                    HorizontalAlignment = Layout.HorizontalAlignment.Left,
                    VerticalAlignment = Layout.VerticalAlignment.Top
                };
                window.Content = button;

                window.Show();

                var f = new Flyout();
                f.Content = new Border { Width = 10, Height = 10 };
                f.ShowAt(window);

                var e = CreatePointerPressedEventArgs(window, new Point(90, 90));
                var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
                overlay.RaiseEvent(e);

                Assert.False(f.IsOpen);
            }
        }

        [Fact]
        public void Flyout_Has_Uncancellable_Close_Before_Showing_On_A_Different_Target()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();
                Button target1 = new Button();
                Button target2 = new Button();

                window.Content = new StackPanel
                {
                    Children =
                    {
                        target1,
                        target2
                    }
                };
                window.Show();

                bool closingFired = false;
                bool closedFired = false;
                Flyout f = new Flyout();
                f.Closing += (s, e) =>
                {
                    closingFired = true; //This shouldn't happen
                };
                f.Closed += (s, e) =>
                {
                    closedFired = true;
                };

                f.ShowAt(target1);

                f.ShowAt(target2);

                Assert.False(closingFired);
                Assert.True(closedFired);
            }
        }

        [Fact]
        public void ShowMode_Standard_Attemps_Focus_Flyout_Content()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();

                var flyoutTextBox = new TextBox();
                var button = new Button
                {
                    Flyout = new Flyout
                    {
                        ShowMode = FlyoutShowMode.Standard,
                        Content = new Panel
                        {
                            Children =
                            {
                                flyoutTextBox
                            }
                        }
                    }
                };

                window.Content = button;
                window.Show();

                button.Focus();
                Assert.True(FocusManager.Instance?.Current == button);
                button.Flyout.ShowAt(button);
                Assert.False(button.IsFocused);
                Assert.True(FocusManager.Instance?.Current == flyoutTextBox);
            }
        }

        [Fact]
        public void ShowMode_Transient_Does_Not_Move_Focus_From_Target()
        {
            using (CreateServicesWithFocus())
            {
                var window = PreparedWindow();

                var flyoutTextBox = new TextBox();
                var button = new Button
                {
                    Flyout = new Flyout
                    {
                        ShowMode = FlyoutShowMode.Transient,
                        Content = new Panel
                        {
                            Children =
                            {
                                flyoutTextBox
                            }
                        }
                    },
                    Content = "Test"
                };

                window.Content = button;
                window.Show();

                FocusManager.Instance?.Focus(button);
                Assert.True(FocusManager.Instance?.Current == button);
                button.Flyout.ShowAt(button);
                Assert.True(FocusManager.Instance?.Current == button);
            }
        }

        [Fact]
        public void ContextRequested_Opens_ContextFlyout()
        {
            using (CreateServicesWithFocus())
            {
                var flyout = new Flyout();
                var target = new Panel
                {
                    ContextFlyout = flyout
                };

                var window = PreparedWindow(target);
                window.Show();

                int openedCount = 0;

                flyout.Opened += (sender, args) =>
                {
                    openedCount++;
                };

                target.RaiseEvent(new ContextRequestedEventArgs());

                Assert.True(flyout.IsOpen);
                Assert.Equal(1, openedCount);
            }
        }

        [Fact]
        public void KeyUp_Raised_On_Target_Opens_ContextFlyout()
        {
            using (CreateServicesWithFocus())
            {
                var flyout = new Flyout();
                var target = new Panel
                {
                    ContextFlyout = flyout
                };
                var contextRequestedCount = 0;
                target.AddHandler(Control.ContextRequestedEvent, (s, a) => contextRequestedCount++, Interactivity.RoutingStrategies.Tunnel);

                var window = PreparedWindow(target);
                window.Show();

                target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window });

                Assert.True(flyout.IsOpen);
                Assert.Equal(1, contextRequestedCount);
            }
        }

        [Fact]
        public void KeyUp_Raised_On_Target_Closes_Opened_ContextFlyout()
        {
            using (CreateServicesWithFocus())
            {
                var flyout = new Flyout();
                var target = new Panel
                {
                    ContextFlyout = flyout
                };

                var window = PreparedWindow(target);
                window.Show();

                target.RaiseEvent(new ContextRequestedEventArgs());

                Assert.True(flyout.IsOpen);

                target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window });

                Assert.False(flyout.IsOpen);
            }
        }

        [Fact]
        public void KeyUp_Raised_On_Flyout_Closes_Opened_ContextFlyout()
        {
            using (CreateServicesWithFocus())
            {
                var flyoutContent = new Button();
                var flyout = new Flyout()
                {
                    Content = flyoutContent
                };
                var target = new Panel
                {
                    ContextFlyout = flyout
                };

                var window = PreparedWindow(target);
                window.Show();

                target.RaiseEvent(new ContextRequestedEventArgs());

                Assert.True(flyout.IsOpen);

                flyoutContent.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window });

                Assert.False(flyout.IsOpen);
            }
        }

        [Fact]
        public void ContextFlyout_Can_Be_Set_In_Styles()
        {
            using (CreateServicesWithFocus())
            {
                var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
    <Window.Styles>
        <Style Selector='TextBlock'>
            <Setter Property='ContextFlyout'>
                <MenuFlyout>
                    <MenuItem>Foo</MenuItem>
                </MenuFlyout>
            </Setter>
        </Style>
	</Window.Styles>

    <StackPanel>
        <TextBlock Name='target1'/>
        <TextBlock Name='target2'/>
    </StackPanel>
</Window>";

                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
                var target1 = window.Find<TextBlock>("target1");
                var target2 = window.Find<TextBlock>("target2");
                var mouse = new MouseTestHelper();

                Assert.NotNull(target1.ContextFlyout);
                Assert.NotNull(target2.ContextFlyout);
                Assert.Same(target1.ContextFlyout, target2.ContextFlyout);

                window.Show();

                var menu = target1.ContextFlyout;
                mouse.Click(target1, MouseButton.Right);
                Assert.True(menu.IsOpen);
                mouse.Click(target2, MouseButton.Right);
                Assert.True(menu.IsOpen);
            }
        }

        [Fact]
        public void Setting_FlyoutPresenterClasses_Sets_Classes_On_FlyoutPresenter()
        {
            using (CreateServicesWithFocus())
            {
                var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
    <Window.Styles>
        <Style Selector='FlyoutPresenter.TestClass'>
            <Setter Property='Background' Value='Red' />
        </Style>
	</Window.Styles>
</Window>";

                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
                var flyoutPanel = new Panel();
                var button = new Button
                {
                    Content = "Test",
                    Flyout = new Flyout
                    {
                        Content = flyoutPanel
                    }
                };
                window.Content = button;
                window.Show();

                (button.Flyout as Flyout).FlyoutPresenterClasses.Add("TestClass");

                button.Flyout.ShowAt(button);

                var presenter = flyoutPanel.GetVisualAncestors().OfType<FlyoutPresenter>().FirstOrDefault();
                Assert.NotNull(presenter);
                Assert.True((presenter.Background as ISolidColorBrush).Color == Colors.Red);
            }
        }

        private IDisposable CreateServicesWithFocus()
        {
            return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
                new MockWindowingPlatform(null,
                    x =>
                    {
                        return MockWindowingPlatform.CreatePopupMock(x).Object;
                    }),
                    focusManager: new FocusManager(),
                    keyboardDevice: () => new KeyboardDevice()));
        }

        private Window PreparedWindow(object content = null)
        {
            var renderer = new Mock<IRenderer>();
            var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();
            var windowImpl = Mock.Get(platform.CreateWindow());
            windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);

            var w = new Window(windowImpl.Object) { Content = content };
            w.ApplyTemplate();
            return w;
        }

        private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p)
        {
            var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
            return new PointerPressedEventArgs(
                source,
                pointer,
                source,
                p,
                0,
                new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
                KeyModifiers.None);
        }
    }
}
