// <copyright file="BasicMouseInterfaceTest.cs" company="Selenium Committers">
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
// </copyright>

using NUnit.Framework;
using OpenQA.Selenium.Environment;
using System;
using System.Drawing;

namespace OpenQA.Selenium.Interactions
{
    [TestFixture]
    public class BasicMouseInterfaceTest : DriverTestFixture
    {
        [SetUp]
        public void SetupTest()
        {
            IActionExecutor actionExecutor = driver as IActionExecutor;
            if (actionExecutor != null)
            {
                actionExecutor.ResetInputState();
            }
        }

        [Test]
        public void ShouldSetActivePointer()
        {
            Actions actionProvider = new Actions(driver);
            actionProvider.SetActivePointer(PointerKind.Mouse, "test mouse");

            PointerInputDevice device = actionProvider.GetActivePointer();

            Assert.That(device.DeviceName, Is.EqualTo("test mouse"));
        }

        [Test]
        public void ShouldAllowDraggingElementWithMouseMovesItToAnotherList()
        {
            PerformDragAndDropWithMouse();
            IWebElement dragInto = driver.FindElement(By.Id("sortable1"));
            Assert.That(dragInto.FindElements(By.TagName("li")), Has.Exactly(6).Items);
        }

        // This test is very similar to DraggingElementWithMouse. The only
        // difference is that this test also verifies the correct events were fired.
        [Test]
        public void DraggingElementWithMouseFiresEvents()
        {
            PerformDragAndDropWithMouse();
            IWebElement dragReporter = driver.FindElement(By.Id("dragging_reports"));
            // This is failing under HtmlUnit. A bug was filed.
            Assert.That(dragReporter.Text, Does.Match("Nothing happened\\. (?:DragOut *)+DropIn RightItem 3"));
        }

        [Test]
        public void ShouldAllowDoubleClickThenNavigate()
        {
            driver.Url = javascriptPage;

            IWebElement toDoubleClick = driver.FindElement(By.Id("doubleClickField"));

            Actions actionProvider = new Actions(driver);
            IAction dblClick = actionProvider.DoubleClick(toDoubleClick).Build();

            dblClick.Perform();
            driver.Url = droppableItems;
        }

        [Test]
        public void ShouldAllowDragAndDrop()
        {
            driver.Url = droppableItems;

            DateTime waitEndTime = DateTime.Now.Add(TimeSpan.FromSeconds(15));

            while (!IsElementAvailable(driver, By.Id("draggable")) && (DateTime.Now < waitEndTime))
            {
                System.Threading.Thread.Sleep(200);
            }

            if (!IsElementAvailable(driver, By.Id("draggable")))
            {
                throw new Exception("Could not find draggable element after 15 seconds.");
            }

            IWebElement toDrag = driver.FindElement(By.Id("draggable"));
            IWebElement dropInto = driver.FindElement(By.Id("droppable"));

            Actions actionProvider = new Actions(driver);

            IAction holdDrag = actionProvider.ClickAndHold(toDrag).Build();

            IAction move = actionProvider.MoveToElement(dropInto).Build();

            IAction drop = actionProvider.Release(dropInto).Build();

            holdDrag.Perform();
            move.Perform();
            drop.Perform();

            dropInto = driver.FindElement(By.Id("droppable"));
            string text = dropInto.FindElement(By.TagName("p")).Text;

            Assert.That(text, Is.EqualTo("Dropped!"));
        }

        [Test]
        public void ShouldAllowDoubleClick()
        {
            driver.Url = javascriptPage;

            IWebElement toDoubleClick = driver.FindElement(By.Id("doubleClickField"));

            Actions actionProvider = new Actions(driver);
            IAction dblClick = actionProvider.DoubleClick(toDoubleClick).Build();

            dblClick.Perform();
            Assert.That(toDoubleClick.GetAttribute("value"), Is.EqualTo("DoubleClicked"));
        }

        [Test]
        public void ShouldAllowContextClick()
        {
            driver.Url = javascriptPage;

            IWebElement toContextClick = driver.FindElement(By.Id("doubleClickField"));

            Actions actionProvider = new Actions(driver);
            IAction contextClick = actionProvider.ContextClick(toContextClick).Build();

            contextClick.Perform();
            Assert.That(toContextClick.GetAttribute("value"), Is.EqualTo("ContextClicked"));
        }

        [Test]
        [IgnoreBrowser(Browser.Remote, "API not implemented in driver")]
        public void ShouldAllowMoveAndClick()
        {
            driver.Url = javascriptPage;

            IWebElement toClick = driver.FindElement(By.Id("clickField"));

            Actions actionProvider = new Actions(driver);
            IAction contextClick = actionProvider.MoveToElement(toClick).Click().Build();

            contextClick.Perform();
            Assert.That(toClick.GetAttribute("value"), Is.EqualTo("Clicked"), "Value should change to Clicked.");
        }

        [Test]
        [IgnoreBrowser(Browser.Chrome, "Not working properly in RBE, works locally with pinned browsers")]
        [IgnoreBrowser(Browser.Edge, "Not working properly in RBE, works locally with pinned browsers")]
        [IgnoreBrowser(Browser.Firefox, "Not working properly in RBE, works locally with pinned browsers")]
        public void ShouldMoveToLocation()
        {
            driver.Url = mouseInteractionPage;

            Actions actionProvider = new Actions(driver);
            actionProvider.MoveToLocation(100, 200).Build().Perform();

            IWebElement location = driver.FindElement(By.Id("absolute-location"));
            var coordinates = location.Text.Split(',');
            Assert.That(coordinates[0].Trim(), Is.EqualTo("100"));
            Assert.That(coordinates[1].Trim(), Is.EqualTo("200"));
        }

        [Test]
        public void ShouldNotMoveToANullLocator()
        {
            driver.Url = javascriptPage;

            Assert.That(() => new Actions(driver).MoveToElement(null).Perform(), Throws.InstanceOf<ArgumentException>());
        }

        [Test]
        [IgnoreBrowser(Browser.Chrome, "Drivers correctly click at current mouse position without another move, preserving mouse position.")]
        [IgnoreBrowser(Browser.Edge, "Drivers correctly click at current mouse position without another move, preserving mouse position.")]
        [IgnoreBrowser(Browser.Firefox, "Drivers correctly click at current mouse position without another move, preserving mouse position.")]
        [IgnoreBrowser(Browser.IE, "Drivers correctly click at current mouse position without another move, preserving mouse position.")]
        [IgnoreBrowser(Browser.Safari, "Drivers correctly click at current mouse position without another move, preserving mouse position.")]
        public void MousePositionIsNotPreservedInActionsChain()
        {
            driver.Url = javascriptPage;
            IWebElement toMoveTo = driver.FindElement(By.Id("clickField"));

            new Actions(driver).MoveToElement(toMoveTo).Perform();
            Assert.That(() => new Actions(driver).Click().Perform(), Throws.InstanceOf<WebDriverException>());
        }

        [Test]
        [IgnoreBrowser(Browser.All, "Behaviour not finalized yet regarding linked images.")]
        public void MovingIntoAnImageEnclosedInALink()
        {
            driver.Url = linkedImage;

            // Note: For some reason, the Accessibility API in Firefox will not be available before we
            // click on something. As a work-around, click on a different element just to get going.
            driver.FindElement(By.Id("linkToAnchorOnThisPage")).Click();

            IWebElement linkElement = driver.FindElement(By.Id("linkWithEnclosedImage"));

            // Image is 644 x 41 - move towards the end.
            // Note: The width of the link element itself is correct - 644 pixels. However,
            // the height is 17 pixels and the rectangle containing it is *underneath* the image.
            // For this reason, this action will fail.
            new Actions(driver).MoveToElement(linkElement, 500, 30).Click().Perform();

            WaitFor(TitleToBe("We Arrive Here"), "Title was not expected value");
        }

        [Test]
        [IgnoreBrowser(Browser.Chrome, "Moving outside of view port throws exception in spec-compliant driver")]
        [IgnoreBrowser(Browser.Edge, "Moving outside of view port throws exception in spec-compliant driver")]
        [IgnoreBrowser(Browser.Firefox, "Moving outside of view port throws exception in spec-compliant driver")]
        [IgnoreBrowser(Browser.IE, "Moving outside of view port throws exception in spec-compliant driver")]
        [IgnoreBrowser(Browser.Safari, "Moving outside of view port throws exception in spec-compliant driver")]
        public void MovingMouseBackAndForthPastViewPort()
        {
            driver.Url = EnvironmentManager.Instance.UrlBuilder.WhereIs("veryLargeCanvas.html");

            IWebElement firstTarget = driver.FindElement(By.Id("r1"));
            new Actions(driver).MoveToElement(firstTarget).Click().Perform();

            IWebElement resultArea = driver.FindElement(By.Id("result"));
            String expectedEvents = "First";
            WaitFor(ElementTextToEqual(resultArea, expectedEvents), "Element text did not equal " + expectedEvents);

            // Move to element with id 'r2', at (2500, 50) to (2580, 100)
            new Actions(driver).MoveByOffset(2540 - 150, 75 - 125).Click().Perform();
            expectedEvents += " Second";
            WaitFor(ElementTextToEqual(resultArea, expectedEvents), "Element text did not equal " + expectedEvents);

            // Move to element with id 'r3' at (60, 1500) to (140, 1550)
            new Actions(driver).MoveByOffset(100 - 2540, 1525 - 75).Click().Perform();
            expectedEvents += " Third";
            WaitFor(ElementTextToEqual(resultArea, expectedEvents), "Element text did not equal " + expectedEvents);

            // Move to element with id 'r4' at (220,180) to (320, 230)
            new Actions(driver).MoveByOffset(270 - 100, 205 - 1525).Click().Perform();
            expectedEvents += " Fourth";
            WaitFor(ElementTextToEqual(resultArea, expectedEvents), "Element text did not equal " + expectedEvents);
        }

        [Test]
        public void ShouldClickElementInIFrame()
        {
            driver.Url = clicksPage;
            try
            {
                driver.SwitchTo().Frame("source");
                IWebElement element = driver.FindElement(By.Id("otherframe"));
                new Actions(driver).MoveToElement(element).Click().Perform();
                driver.SwitchTo().DefaultContent().SwitchTo().Frame("target");
                WaitFor(() => { return driver.FindElement(By.Id("span")).Text == "An inline element"; }, "Could not find element with text 'An inline element'");
            }
            finally
            {
                driver.SwitchTo().DefaultContent();
            }
        }

        [Test]
        public void ShouldAllowUsersToHoverOverElements()
        {
            driver.Url = javascriptPage;

            IWebElement element = driver.FindElement(By.Id("menu1"));
            if (!Platform.CurrentPlatform.IsPlatformType(PlatformType.Windows))
            {
                Assert.Ignore("Skipping test: Simulating hover needs native events");
            }

            IWebElement item = driver.FindElement(By.Id("item1"));
            Assert.That(item.Text, Is.EqualTo(""));

            ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].style.background = 'green'", element);
            Actions actionBuilder = new Actions(driver);
            actionBuilder.MoveToElement(element).Perform();

            item = driver.FindElement(By.Id("item1"));
            Assert.That(item.Text, Is.EqualTo("Item 1"));
        }

        [Test]
        public void HoverPersists()
        {
            driver.Url = javascriptPage;
            // Move to a different element to make sure the mouse is not over the
            // element with id 'item1' (from a previous test).
            new Actions(driver).MoveToElement(driver.FindElement(By.Id("dynamo"))).Perform();

            IWebElement element = driver.FindElement(By.Id("menu1"));

            IWebElement item = driver.FindElement(By.Id("item1"));
            Assert.That(item.Text, Is.Empty);

            ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].style.background = 'green'", element);
            new Actions(driver).MoveToElement(element).Perform();

            // Intentionally wait to make sure hover persists.
            System.Threading.Thread.Sleep(2000);

            WaitFor(ElementTextToNotEqual(item, ""), "Element text was empty after timeout");

            Assert.That(item.Text, Is.EqualTo("Item 1"));
        }

        [Test]
        public void MovingMouseByRelativeOffset()
        {
            driver.Url = mouseTrackerPage;

            IWebElement trackerDiv = driver.FindElement(By.Id("mousetracker"));
            new Actions(driver).MoveToElement(trackerDiv).Build().Perform();

            IWebElement reporter = driver.FindElement(By.Id("status"));

            WaitFor(FuzzyMatchingOfCoordinates(reporter, 50, 200), "Coordinate matching was not within tolerance");

            new Actions(driver).MoveByOffset(10, 20).Build().Perform();

            WaitFor(FuzzyMatchingOfCoordinates(reporter, 60, 220), "Coordinate matching was not within tolerance");
        }

        [Test]
        public void MovingMouseToRelativeElementOffset()
        {
            driver.Url = mouseTrackerPage;

            IWebElement trackerDiv = driver.FindElement(By.Id("mousetracker"));
            Size size = trackerDiv.Size;

            new Actions(driver).MoveToElement(trackerDiv, 95 - size.Width / 2, 195 - size.Height / 2).Build().Perform();

            IWebElement reporter = driver.FindElement(By.Id("status"));

            WaitFor(FuzzyMatchingOfCoordinates(reporter, 95, 195), "Coordinate matching was not within tolerance");
        }

        [Test]
        public void MovingMouseToRelativeZeroElementOffset()
        {
            driver.Url = mouseTrackerPage;

            IWebElement trackerDiv = driver.FindElement(By.Id("mousetracker"));
            Size size = trackerDiv.Size;

            new Actions(driver).MoveToElement(trackerDiv, -size.Width / 2, -size.Height / 2).Perform();

            IWebElement reporter = driver.FindElement(By.Id("status"));

            WaitFor(FuzzyMatchingOfCoordinates(reporter, 0, 0), "Coordinate matching was not within tolerance");
        }

        [Test]
        [NeedsFreshDriver(IsCreatedBeforeTest = true)]
        public void MoveRelativeToBody()
        {
            driver.Url = mouseTrackerPage;

            new Actions(driver).MoveByOffset(50, 100).Build().Perform();

            IWebElement reporter = driver.FindElement(By.Id("status"));

            WaitFor(FuzzyMatchingOfCoordinates(reporter, 40, 20), "Coordinate matching was not within tolerance");
        }

        [Test]
        public void MoveMouseByOffsetOverAndOutOfAnElement()
        {
            driver.Url = mouseOverPage;

            IWebElement greenbox = driver.FindElement(By.Id("greenbox"));
            IWebElement redbox = driver.FindElement(By.Id("redbox"));
            Point greenboxPosition = greenbox.Location;
            Point redboxPosition = redbox.Location;
            int shiftX = redboxPosition.X - greenboxPosition.X;
            int shiftY = redboxPosition.Y - greenboxPosition.Y;

            Size greenBoxSize = greenbox.Size;
            int xOffset = 2 - greenBoxSize.Width / 2;
            int yOffset = 2 - greenBoxSize.Height / 2;

            new Actions(driver).MoveToElement(greenbox, xOffset, yOffset).Perform();
            WaitFor(ElementColorToBe(redbox, Color.Green), "element color was not green");

            new Actions(driver).MoveToElement(greenbox, xOffset, yOffset).MoveByOffset(shiftX, shiftY).Perform();
            WaitFor(ElementColorToBe(redbox, Color.Red), "element color was not red");

            new Actions(driver).MoveToElement(greenbox, xOffset, yOffset).MoveByOffset(shiftX, shiftY).MoveByOffset(-shiftX, -shiftY).Perform();
            WaitFor(ElementColorToBe(redbox, Color.Green), "element color was not red");
        }

        [Test]
        public void CanMouseOverAndOutOfAnElement()
        {
            driver.Url = mouseOverPage;

            IWebElement greenbox = driver.FindElement(By.Id("greenbox"));
            IWebElement redbox = driver.FindElement(By.Id("redbox"));
            Size greenSize = greenbox.Size;
            Size redSize = redbox.Size;

            new Actions(driver).MoveToElement(greenbox, 1 - greenSize.Width / 2, 1 - greenSize.Height / 2).Perform();
            Assert.That(redbox.GetCssValue("background-color"), Is.EqualTo("rgba(0, 128, 0, 1)").Or.EqualTo("rgb(0, 128, 0)"));

            new Actions(driver).MoveToElement(redbox).Perform();
            Assert.That(redbox.GetCssValue("background-color"), Is.EqualTo("rgba(255, 0, 0, 1)").Or.EqualTo("rgb(255, 0, 0)"));

            new Actions(driver).MoveToElement(redbox, redSize.Width / 2 + 2, redSize.Height / 2 + 2).Perform();
            Assert.That(redbox.GetCssValue("background-color"), Is.EqualTo("rgba(0, 128, 0, 1)").Or.EqualTo("rgb(0, 128, 0)"));
        }

        private Func<bool> FuzzyMatchingOfCoordinates(IWebElement element, int x, int y)
        {
            return () =>
            {
                return FuzzyPositionMatching(x, y, element.Text);
            };
        }

        private bool FuzzyPositionMatching(int expectedX, int expectedY, String locationTuple)
        {
            string[] splitString = locationTuple.Split(',');
            int gotX = Convert.ToInt16(Math.Round(Convert.ToDouble(splitString[0].Trim())));
            int gotY = Convert.ToInt16(Math.Round(Convert.ToDouble(splitString[1].Trim())));

            // Everything within 5 pixels range is OK
            const int ALLOWED_DEVIATION = 5;
            return Math.Abs(expectedX - gotX) < ALLOWED_DEVIATION && Math.Abs(expectedY - gotY) < ALLOWED_DEVIATION;
        }

        private void PerformDragAndDropWithMouse()
        {
            driver.Url = draggableLists;

            IWebElement dragReporter = driver.FindElement(By.Id("dragging_reports"));

            IWebElement toDrag = driver.FindElement(By.Id("rightitem-3"));
            IWebElement dragInto = driver.FindElement(By.Id("sortable1"));

            IAction holdItem = new Actions(driver).ClickAndHold(toDrag).Build();

            IAction moveToSpecificItem = new Actions(driver).MoveToElement(driver.FindElement(By.Id("leftitem-4"))).Build();

            IAction moveToOtherList = new Actions(driver).MoveToElement(dragInto).Build();

            IAction drop = new Actions(driver).Release(dragInto).Build();

            Assert.That(dragReporter.Text, Is.EqualTo("Nothing happened."));

            holdItem.Perform();
            moveToSpecificItem.Perform();
            moveToOtherList.Perform();

            Assert.That(dragReporter.Text, Does.Match("Nothing happened\\. (?:DragOut *)+"));
            drop.Perform();
        }

        private bool IsElementAvailable(IWebDriver driver, By locator)
        {
            try
            {
                driver.FindElement(locator);
                return true;
            }
            catch (NoSuchElementException)
            {
                return false;
            }
        }

        private Func<bool> TitleToBe(string desiredTitle)
        {
            return () => driver.Title == desiredTitle;
        }

        private Func<bool> ValueToBe(IWebElement element, string desiredValue)
        {
            return () => element.GetDomProperty("value") == desiredValue;
        }

        private Func<bool> ElementTextToEqual(IWebElement element, string text)
        {
            return () => element.Text == text;
        }

        private Func<bool> ElementTextToNotEqual(IWebElement element, string text)
        {
            return () => element.Text != text;
        }

        private Func<bool> ElementColorToBe(IWebElement element, Color color)
        {
            return () =>
            {
                string rgb = string.Format("rgb({0}, {1}, {2})", color.R, color.G, color.B);
                string rgba = string.Format("rgba({0}, {1}, {2}, 1)", color.R, color.G, color.B);
                string value = element.GetCssValue("background-color");
                return value == rgb || value == rgba;
            };
        }
    }
}
