// <copyright file="ExecutingAsyncJavascriptTest.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 System;
using System.Collections.ObjectModel;

namespace OpenQA.Selenium;

[TestFixture]
public class ExecutingAsyncJavascriptTest : DriverTestFixture
{
    private IJavaScriptExecutor executor;
    private TimeSpan originalTimeout = TimeSpan.MinValue;

    [SetUp]
    public void SetUpEnvironment()
    {
        if (driver is IJavaScriptExecutor)
        {
            executor = (IJavaScriptExecutor)driver;
        }

        try
        {
            originalTimeout = driver.Manage().Timeouts().AsynchronousJavaScript;
        }
        catch (NotImplementedException)
        {
            // For driver implementations that do not support getting timeouts,
            // just set a default 30-second timeout.
            originalTimeout = TimeSpan.FromSeconds(30);
        }

        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromSeconds(1);
    }

    [TearDown]
    public void TearDownEnvironment()
    {
        driver.Manage().Timeouts().AsynchronousJavaScript = originalTimeout;
    }

    [Test]
    public void ShouldNotTimeoutIfCallbackInvokedImmediately()
    {
        driver.Url = ajaxyPage;
        object result = executor.ExecuteAsyncScript("arguments[arguments.length - 1](123);");
        Assert.That(result, Is.InstanceOf<long>());
        Assert.That((long)result, Is.EqualTo(123));
    }

    [Test]
    public void ShouldBeAbleToReturnJavascriptPrimitivesFromAsyncScripts_NeitherNullNorUndefined()
    {
        driver.Url = ajaxyPage;
        Assert.That((long)executor.ExecuteAsyncScript("arguments[arguments.length - 1](123);"), Is.EqualTo(123));
        driver.Url = ajaxyPage;
        Assert.That(executor.ExecuteAsyncScript("arguments[arguments.length - 1]('abc');").ToString(), Is.EqualTo("abc"));
        driver.Url = ajaxyPage;
        Assert.That((bool)executor.ExecuteAsyncScript("arguments[arguments.length - 1](false);"), Is.False);
        driver.Url = ajaxyPage;
        Assert.That((bool)executor.ExecuteAsyncScript("arguments[arguments.length - 1](true);"), Is.True);
    }

    [Test]
    public void ShouldBeAbleToReturnJavascriptPrimitivesFromAsyncScripts_NullAndUndefined()
    {
        driver.Url = ajaxyPage;
        Assert.That(executor.ExecuteAsyncScript("arguments[arguments.length - 1](null);"), Is.Null);
        Assert.That(executor.ExecuteAsyncScript("arguments[arguments.length - 1]();"), Is.Null);
    }

    [Test]
    public void ShouldBeAbleToReturnAnArrayLiteralFromAnAsyncScript()
    {
        driver.Url = ajaxyPage;

        object result = executor.ExecuteAsyncScript("arguments[arguments.length - 1]([]);");
        Assert.That(result, Is.Not.Null);
        Assert.That(result, Is.InstanceOf<ReadOnlyCollection<object>>());
        Assert.That((ReadOnlyCollection<object>)result, Has.Count.EqualTo(0));
    }

    [Test]
    public void ShouldBeAbleToReturnAnArrayObjectFromAnAsyncScript()
    {
        driver.Url = ajaxyPage;

        object result = executor.ExecuteAsyncScript("arguments[arguments.length - 1](new Array());");
        Assert.That(result, Is.Not.Null);
        Assert.That(result, Is.InstanceOf<ReadOnlyCollection<object>>());
        Assert.That((ReadOnlyCollection<object>)result, Has.Count.EqualTo(0));
    }

    [Test]
    public void ShouldBeAbleToReturnArraysOfPrimitivesFromAsyncScripts()
    {
        driver.Url = ajaxyPage;

        object result = executor.ExecuteAsyncScript("arguments[arguments.length - 1]([null, 123, 'abc', true, false]);");
        Assert.That(result, Is.Not.Null);
        Assert.That(result, Is.InstanceOf<ReadOnlyCollection<object>>());
        ReadOnlyCollection<object> resultList = result as ReadOnlyCollection<object>;
        Assert.That(resultList, Has.Count.EqualTo(5));
        Assert.That(resultList[0], Is.Null);
        Assert.That((long)resultList[1], Is.EqualTo(123));
        Assert.That(resultList[2].ToString(), Is.EqualTo("abc"));
        Assert.That((bool)resultList[3], Is.True);
        Assert.That((bool)resultList[4], Is.False);
    }

    [Test]
    public void ShouldBeAbleToReturnWebElementsFromAsyncScripts()
    {
        driver.Url = ajaxyPage;

        object result = executor.ExecuteAsyncScript("arguments[arguments.length - 1](document.body);");
        Assert.That(result, Is.InstanceOf<IWebElement>());
        Assert.That(((IWebElement)result).TagName.ToLower(), Is.EqualTo("body"));
    }

    [Test]
    public void ShouldBeAbleToReturnArraysOfWebElementsFromAsyncScripts()
    {
        driver.Url = ajaxyPage;

        object result = executor.ExecuteAsyncScript("arguments[arguments.length - 1]([document.body, document.body]);");
        Assert.That(result, Is.Not.Null);
        Assert.That(result, Is.InstanceOf<ReadOnlyCollection<IWebElement>>());
        ReadOnlyCollection<IWebElement> resultsList = (ReadOnlyCollection<IWebElement>)result;
        Assert.That(resultsList, Has.Count.EqualTo(2));
        Assert.That(resultsList[0], Is.InstanceOf<IWebElement>());
        Assert.That(resultsList[1], Is.InstanceOf<IWebElement>());
        Assert.That(((IWebElement)resultsList[0]).TagName.ToLower(), Is.EqualTo("body"));
        Assert.That(((IWebElement)resultsList[0]), Is.EqualTo((IWebElement)resultsList[1]));
    }

    [Test]
    public void ShouldTimeoutIfScriptDoesNotInvokeCallback()
    {
        driver.Url = ajaxyPage;
        Assert.That(() => executor.ExecuteAsyncScript("return 1 + 2;"), Throws.InstanceOf<WebDriverTimeoutException>());
    }

    [Test]
    public void ShouldTimeoutIfScriptDoesNotInvokeCallbackWithAZeroTimeout()
    {
        driver.Url = ajaxyPage;
        Assert.That(() => executor.ExecuteAsyncScript("window.setTimeout(function() {}, 0);"), Throws.InstanceOf<WebDriverTimeoutException>());
    }

    [Test]
    public void ShouldNotTimeoutIfScriptCallsbackInsideAZeroTimeout()
    {
        driver.Url = ajaxyPage;
        executor.ExecuteAsyncScript(
            "var callback = arguments[arguments.length - 1];" +
            "window.setTimeout(function() { callback(123); }, 0)");
    }

    [Test]
    public void ShouldTimeoutIfScriptDoesNotInvokeCallbackWithLongTimeout()
    {
        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromMilliseconds(500);
        driver.Url = ajaxyPage;
        Assert.That(() => executor.ExecuteAsyncScript(
            "var callback = arguments[arguments.length - 1];" +
            "window.setTimeout(callback, 1500);"), Throws.InstanceOf<WebDriverTimeoutException>());
    }

    [Test]
    public void ShouldDetectPageLoadsWhileWaitingOnAnAsyncScriptAndReturnAnError()
    {
        driver.Url = ajaxyPage;
        Assert.That(() => executor.ExecuteAsyncScript("window.location = '" + dynamicPage + "';"), Throws.InstanceOf<WebDriverException>());
    }

    [Test]
    public void ShouldCatchErrorsWhenExecutingInitialScript()
    {
        driver.Url = ajaxyPage;
        Assert.That(() => executor.ExecuteAsyncScript("throw Error('you should catch this!');"), Throws.InstanceOf<WebDriverException>());
    }

    [Test]
    public void ShouldNotTimeoutWithMultipleCallsTheFirstOneBeingSynchronous()
    {
        driver.Url = ajaxyPage;
        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromMilliseconds(1000);
        Assert.That((bool)executor.ExecuteAsyncScript("arguments[arguments.length - 1](true);"), Is.True);
        Assert.That((bool)executor.ExecuteAsyncScript("var cb = arguments[arguments.length - 1]; window.setTimeout(function(){cb(true);}, 9);"), Is.True);
    }

    [Test]
    [IgnoreBrowser(Browser.Chrome, ".NET language bindings do not properly parse JavaScript stack trace")]
    [IgnoreBrowser(Browser.Edge, ".NET language bindings do not properly parse JavaScript stack trace")]
    [IgnoreBrowser(Browser.Firefox, ".NET language bindings do not properly parse JavaScript stack trace")]
    [IgnoreBrowser(Browser.IE, ".NET language bindings do not properly parse JavaScript stack trace")]
    [IgnoreBrowser(Browser.Safari, ".NET language bindings do not properly parse JavaScript stack trace")]
    public void ShouldCatchErrorsWithMessageAndStacktraceWhenExecutingInitialScript()
    {
        driver.Url = ajaxyPage;
        string js = "function functionB() { throw Error('errormessage'); };"
                    + "function functionA() { functionB(); };"
                    + "functionA();";

        Assert.That(
            () => executor.ExecuteAsyncScript(js),
            Throws.InstanceOf<WebDriverException>()
            .With.Message.Contains("errormessage")
            .And.Property(nameof(WebDriverException.StackTrace)).Contains("functionB"));
    }

    [Test]
    public void ShouldBeAbleToExecuteAsynchronousScripts()
    {
        // Reset the timeout to the 30-second default instead of zero.
        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromSeconds(30);
        driver.Url = ajaxyPage;

        IWebElement typer = driver.FindElement(By.Name("typer"));
        typer.SendKeys("bob");
        Assert.That(typer.GetAttribute("value"), Is.EqualTo("bob"));

        driver.FindElement(By.Id("red")).Click();
        driver.FindElement(By.Name("submit")).Click();

        Assert.That(GetNumberOfDivElements(), Is.EqualTo(1), "There should only be 1 DIV at this point, which is used for the butter message");

        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromSeconds(10);
        string text = (string)executor.ExecuteAsyncScript(
            "var callback = arguments[arguments.length - 1];"
            + "window.registerListener(arguments[arguments.length - 1]);");
        Assert.That(text, Is.EqualTo("bob"));
        Assert.That(typer.GetAttribute("value"), Is.Empty);

        Assert.That(GetNumberOfDivElements(), Is.EqualTo(2), "There should be 1 DIV (for the butter message) + 1 DIV (for the new label)");
    }

    [Test]
    public void ShouldBeAbleToPassMultipleArgumentsToAsyncScripts()
    {
        driver.Url = ajaxyPage;
        long result = (long)executor.ExecuteAsyncScript("arguments[arguments.length - 1](arguments[0] + arguments[1]);", 1, 2);
        Assert.That(result, Is.EqualTo(3));
    }

    [Test]
    public void ShouldBeAbleToMakeXMLHttpRequestsAndWaitForTheResponse()
    {
        string script =
            "var url = arguments[0];" +
            "var callback = arguments[arguments.length - 1];" +
            // Adapted from http://www.quirksmode.org/js/xmlhttp.html
            "var XMLHttpFactories = [" +
            "  function () {return new XMLHttpRequest()}," +
            "  function () {return new ActiveXObject('Msxml2.XMLHTTP')}," +
            "  function () {return new ActiveXObject('Msxml3.XMLHTTP')}," +
            "  function () {return new ActiveXObject('Microsoft.XMLHTTP')}" +
            "];" +
            "var xhr = false;" +
            "while (!xhr && XMLHttpFactories.length) {" +
            "  try {" +
            "    xhr = XMLHttpFactories.shift().call();" +
            "  } catch (e) {}" +
            "}" +
            "if (!xhr) throw Error('unable to create XHR object');" +
            "xhr.open('GET', url, true);" +
            "xhr.onreadystatechange = function() {" +
            "  if (xhr.readyState == 4) callback(xhr.responseText);" +
            "};" +
            "xhr.send();";

        driver.Url = ajaxyPage;
        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromSeconds(3);
        string response = (string)executor.ExecuteAsyncScript(script, sleepingPage + "?time=2");
        Assert.That(response.Trim(), Is.EqualTo("<html><head><title>Done</title></head><body>Slept for 2s</body></html>"));
    }

    [Test]
    public void ThrowsIfScriptTriggersAlert()
    {
        driver.Url = simpleTestPage;
        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromSeconds(5);
        ((IJavaScriptExecutor)driver).ExecuteAsyncScript(
            "setTimeout(arguments[0], 200) ; setTimeout(function() { window.alert('Look! An alert!'); }, 50);");
        Assert.That(() => driver.Title, Throws.InstanceOf<UnhandledAlertException>());

        string title = driver.Title;
    }

    [Test]
    public void ThrowsIfAlertHappensDuringScript()
    {
        driver.Url = slowLoadingAlertPage;
        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromSeconds(5);
        ((IJavaScriptExecutor)driver).ExecuteAsyncScript("setTimeout(arguments[0], 1000);");
        Assert.That(() => driver.Title, Throws.InstanceOf<UnhandledAlertException>());

        // Shouldn't throw
        string title = driver.Title;
    }

    [Test]
    public void ThrowsIfScriptTriggersAlertWhichTimesOut()
    {
        driver.Url = simpleTestPage;
        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromSeconds(5);
        ((IJavaScriptExecutor)driver)
            .ExecuteAsyncScript("setTimeout(function() { window.alert('Look! An alert!'); }, 50);");
        Assert.That(() => driver.Title, Throws.InstanceOf<UnhandledAlertException>());

        // Shouldn't throw
        string title = driver.Title;
    }

    [Test]
    public void ThrowsIfAlertHappensDuringScriptWhichTimesOut()
    {
        driver.Url = slowLoadingAlertPage;
        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromSeconds(5);
        ((IJavaScriptExecutor)driver).ExecuteAsyncScript("");
        Assert.That(() => driver.Title, Throws.InstanceOf<UnhandledAlertException>());

        // Shouldn't throw
        string title = driver.Title;
    }

    [Test]
    [IgnoreBrowser(Browser.Firefox, "Driver chooses not to return text from unhandled alert")]
    public void IncludesAlertTextInUnhandledAlertException()
    {
        driver.Manage().Timeouts().AsynchronousJavaScript = TimeSpan.FromSeconds(5);
        string alertText = "Look! An alert!";
        ((IJavaScriptExecutor)driver).ExecuteAsyncScript(
            "setTimeout(arguments[0], 200) ; setTimeout(function() { window.alert('" + alertText
            + "'); }, 50);");
        Assert.That(() => driver.Title, Throws.InstanceOf<UnhandledAlertException>().With.Property("AlertText").EqualTo(alertText));
    }

    private long GetNumberOfDivElements()
    {
        IJavaScriptExecutor jsExecutor = driver as IJavaScriptExecutor;
        // Selenium does not support "findElements" yet, so we have to do this through a script.
        return (long)jsExecutor.ExecuteScript("return document.getElementsByTagName('div').length;");
    }
}
