import os
import sys
from urllib.parse import quote

from marionette_driver import By, errors
from marionette_driver.marionette import Alert, WebElement
from marionette_driver.wait import Wait
from marionette_harness import MarionetteTestCase, WindowManagerMixin

# add this directory to the path
sys.path.append(os.path.dirname(__file__))

from chrome_handler_mixin import ChromeHandlerMixin


def inline(doc):
    return "data:text/html;charset=utf-8,{}".format(quote(doc))


elements = inline("<p>foo</p> <p>bar</p>")

shadow_dom = """
    <style>
        custom-checkbox-element {
            display:block; width:20px; height:20px;
        }
    </style>
    <custom-checkbox-element></custom-checkbox-element>
    <script>
        customElements.define('custom-checkbox-element',
            class extends HTMLElement {
                constructor() {
                        super();
                        this.attachShadow({mode: '%s'}).innerHTML = `
                            <div><input type="checkbox"/></div>
                        `;
                    }
            });
    </script>"""


globals = set(
    [
        "atob",
        "Audio",
        "btoa",
        "document",
        "navigator",
        "URL",
        "window",
    ]
)


class TestExecuteContent(MarionetteTestCase):
    def alert_present(self):
        try:
            Alert(self.marionette).text
            return True
        except errors.NoAlertPresentException:
            return False

    def wait_for_alert_closed(self, timeout=None):
        Wait(self.marionette, timeout=timeout).until(lambda _: not self.alert_present())

    def tearDown(self):
        if self.alert_present():
            alert = self.marionette.switch_to_alert()
            alert.dismiss()
            self.wait_for_alert_closed()

    def assert_is_defined(self, property, sandbox="default"):
        self.assertTrue(
            self.marionette.execute_script(
                "return typeof arguments[0] != 'undefined'", [property], sandbox=sandbox
            ),
            "property {} is undefined".format(property),
        )

    def assert_is_web_element(self, element):
        self.assertIsInstance(element, WebElement)

    def test_return_number(self):
        self.assertEqual(1, self.marionette.execute_script("return 1"))
        self.assertEqual(1.5, self.marionette.execute_script("return 1.5"))

    def test_return_boolean(self):
        self.assertTrue(self.marionette.execute_script("return true"))

    def test_return_string(self):
        self.assertEqual("foo", self.marionette.execute_script("return 'foo'"))

    def test_return_array(self):
        self.assertEqual([1, 2], self.marionette.execute_script("return [1, 2]"))
        self.assertEqual(
            [1.25, 1.75], self.marionette.execute_script("return [1.25, 1.75]")
        )
        self.assertEqual(
            [True, False], self.marionette.execute_script("return [true, false]")
        )
        self.assertEqual(
            ["foo", "bar"], self.marionette.execute_script("return ['foo', 'bar']")
        )
        self.assertEqual(
            [1, 1.5, True, "foo"],
            self.marionette.execute_script("return [1, 1.5, true, 'foo']"),
        )
        self.assertEqual([1, [2]], self.marionette.execute_script("return [1, [2]]"))

    def test_return_object(self):
        self.assertEqual({"foo": 1}, self.marionette.execute_script("return {foo: 1}"))
        self.assertEqual(
            {"foo": 1.5}, self.marionette.execute_script("return {foo: 1.5}")
        )
        self.assertEqual(
            {"foo": True}, self.marionette.execute_script("return {foo: true}")
        )
        self.assertEqual(
            {"foo": "bar"}, self.marionette.execute_script("return {foo: 'bar'}")
        )
        self.assertEqual(
            {"foo": [1, 2]}, self.marionette.execute_script("return {foo: [1, 2]}")
        )
        self.assertEqual(
            {"foo": {"bar": [1, 2]}},
            self.marionette.execute_script("return {foo: {bar: [1, 2]}}"),
        )

    def test_no_return_value(self):
        self.assertIsNone(self.marionette.execute_script("true"))

    def test_argument_null(self):
        self.assertIsNone(
            self.marionette.execute_script(
                "return arguments[0]", script_args=(None,), sandbox="default"
            )
        )
        self.assertIsNone(
            self.marionette.execute_script(
                "return arguments[0]", script_args=(None,), sandbox="system"
            )
        )
        self.assertIsNone(
            self.marionette.execute_script(
                "return arguments[0]", script_args=(None,), sandbox=None
            )
        )

    def test_argument_number(self):
        self.assertEqual(1, self.marionette.execute_script("return arguments[0]", (1,)))
        self.assertEqual(
            1.5, self.marionette.execute_script("return arguments[0]", (1.5,))
        )

    def test_argument_boolean(self):
        self.assertTrue(self.marionette.execute_script("return arguments[0]", (True,)))

    def test_argument_string(self):
        self.assertEqual(
            "foo", self.marionette.execute_script("return arguments[0]", ("foo",))
        )

    def test_argument_array(self):
        self.assertEqual(
            [1, 2], self.marionette.execute_script("return arguments[0]", ([1, 2],))
        )

    def test_argument_object(self):
        self.assertEqual(
            {"foo": 1},
            self.marionette.execute_script("return arguments[0]", ({"foo": 1},)),
        )

    def test_argument_shadow_root(self):
        self.marionette.navigate(inline(shadow_dom % "open"))
        elem = self.marionette.find_element(By.TAG_NAME, "custom-checkbox-element")
        shadow_root = elem.shadow_root
        nodeType = self.marionette.execute_script(
            "return arguments[0].nodeType", script_args=(shadow_root,)
        )
        self.assertEqual(nodeType, 11)

    def test_argument_web_element(self):
        self.marionette.navigate(elements)
        elem = self.marionette.find_element(By.TAG_NAME, "p")
        nodeType = self.marionette.execute_script(
            "return arguments[0].nodeType", script_args=(elem,)
        )
        self.assertEqual(nodeType, 1)

    def test_default_sandbox_globals(self):
        for property in globals:
            self.assert_is_defined(property, sandbox="default")

        self.assert_is_defined("Components")
        self.assert_is_defined("window.wrappedJSObject")

    def test_system_globals(self):
        for property in globals:
            self.assert_is_defined(property, sandbox="system")

        self.assert_is_defined("Components", sandbox="system")
        self.assert_is_defined("window.wrappedJSObject", sandbox="system")

    def test_mutable_sandbox_globals(self):
        for property in globals:
            self.assert_is_defined(property, sandbox=None)

        # Components is there, but will be removed soon
        self.assert_is_defined("Components", sandbox=None)
        # wrappedJSObject is always there in sandboxes
        self.assert_is_defined("window.wrappedJSObject", sandbox=None)

    def test_exception(self):
        self.assertRaises(
            errors.JavascriptException, self.marionette.execute_script, "return foo"
        )

    def test_stacktrace(self):
        with self.assertRaises(errors.JavascriptException) as cm:
            self.marionette.execute_script("return b")

        # by default execute_script pass the name of the python file
        self.assertIn(
            os.path.relpath(__file__.replace(".pyc", ".py")), cm.exception.stacktrace
        )
        self.assertIn("b is not defined", str(cm.exception))

    def test_syntaxerror_stack(self):
        with self.assertRaises(errors.JavascriptException) as cm:
            self.marionette.execute_script("notAFunc() {")

        # by default execute_script pass the name of the python file
        self.assertIn(
            os.path.relpath(__file__.replace(".pyc", ".py")), cm.exception.stacktrace
        )
        self.assertIn("SyntaxError: unexpected token: '{'", str(cm.exception))

    def test_permission(self):
        for sandbox in ["default", None]:
            with self.assertRaises(errors.JavascriptException):
                self.marionette.execute_script(
                    "Components.classes['@mozilla.org/preferences-service;1']"
                )

    def test_return_web_element(self):
        self.marionette.navigate(elements)
        expected = self.marionette.find_element(By.TAG_NAME, "p")
        actual = self.marionette.execute_script("return document.querySelector('p')")
        self.assertEqual(expected, actual)

    def test_return_web_element_array(self):
        self.marionette.navigate(elements)
        expected = self.marionette.find_elements(By.TAG_NAME, "p")
        actual = self.marionette.execute_script(
            """
            let els = document.querySelectorAll('p')
            return [els[0], els[1]]"""
        )
        self.assertEqual(expected, actual)

    def test_return_web_element_nested_array(self):
        self.marionette.navigate(elements)
        expected = self.marionette.find_elements(By.TAG_NAME, "p")
        actual = self.marionette.execute_script(
            """
            let els = document.querySelectorAll('p')
            return { els: [els[0], els[1]] }"""
        )
        self.assertEqual(expected, actual["els"])

    def test_return_web_element_nested_dict(self):
        self.marionette.navigate(elements)
        expected = self.marionette.find_element(By.TAG_NAME, "p")
        actual = self.marionette.execute_script(
            """
            let el = document.querySelector('p')
            return { path: { to: { el } } }"""
        )
        self.assertEqual(expected, actual["path"]["to"]["el"])

    # Bug 938228 identifies a problem with unmarshaling NodeList
    # objects from the DOM.  document.querySelectorAll returns this
    # construct.
    def test_return_web_element_nodelist(self):
        self.marionette.navigate(elements)
        expected = self.marionette.find_elements(By.TAG_NAME, "p")
        actual = self.marionette.execute_script("return document.querySelectorAll('p')")
        self.assertEqual(expected, actual)

    def test_sandbox_reuse(self):
        # Sandboxes between `execute_script()` invocations are shared.
        self.marionette.execute_script("this.foobar = [23, 42];")
        self.assertEqual(
            self.marionette.execute_script("return this.foobar;", new_sandbox=False),
            [23, 42],
        )

    def test_sandbox_refresh_arguments(self):
        self.marionette.execute_script(
            "this.foobar = [arguments[0], arguments[1]]", [23, 42]
        )
        self.assertEqual(
            self.marionette.execute_script("return this.foobar", new_sandbox=False),
            [23, 42],
        )

    def test_mutable_sandbox_wrappedjsobject(self):
        self.assert_is_defined("window.wrappedJSObject")
        with self.assertRaises(errors.JavascriptException):
            self.marionette.execute_script(
                "window.wrappedJSObject.foo = 1", sandbox=None
            )

    def test_default_sandbox_wrappedjsobject(self):
        self.assert_is_defined("window.wrappedJSObject", sandbox="default")

        try:
            self.marionette.execute_script(
                "window.wrappedJSObject.foo = 4", sandbox="default"
            )
            self.assertEqual(
                self.marionette.execute_script(
                    "return window.wrappedJSObject.foo", sandbox="default"
                ),
                4,
            )
        finally:
            self.marionette.execute_script(
                "delete window.wrappedJSObject.foo", sandbox="default"
            )

    def test_system_sandbox_wrappedjsobject(self):
        self.assert_is_defined("window.wrappedJSObject", sandbox="system")

        self.marionette.execute_script(
            "window.wrappedJSObject.foo = 4", sandbox="system"
        )
        self.assertEqual(
            self.marionette.execute_script(
                "return window.wrappedJSObject.foo", sandbox="system"
            ),
            4,
        )

    def test_system_dead_object(self):
        self.assert_is_defined("window.wrappedJSObject", sandbox="system")

        self.marionette.execute_script(
            "window.wrappedJSObject.foo = function() { return 'yo' }", sandbox="system"
        )
        self.marionette.execute_script(
            "dump(window.wrappedJSObject.foo)", sandbox="system"
        )

        self.marionette.execute_script(
            "window.wrappedJSObject.foo = function() { return 'yolo' }",
            sandbox="system",
        )
        typ = self.marionette.execute_script(
            "return typeof window.wrappedJSObject.foo", sandbox="system"
        )
        self.assertEqual("function", typ)
        obj = self.marionette.execute_script(
            "return window.wrappedJSObject.foo.toString()", sandbox="system"
        )
        self.assertIn("yolo", obj)

    def test_lasting_side_effects(self):
        def send(script):
            return self.marionette._send_message(
                "WebDriver:ExecuteScript", {"script": script}, key="value"
            )

        send("window.foo = 1")
        foo = send("return window.foo")
        self.assertEqual(1, foo)

        for property in globals:
            exists = send("return typeof {} != 'undefined'".format(property))
            self.assertTrue(exists, "property {} is undefined".format(property))

        self.assertTrue(
            send(
                """
            return (typeof Components == 'undefined') ||
                (typeof Components.utils == 'undefined')
        """
            )
        )
        self.assertTrue(send("return typeof window.wrappedJSObject == 'undefined'"))

    def test_no_callback(self):
        self.assertTrue(
            self.marionette.execute_script("return typeof arguments[0] == 'undefined'")
        )

    def test_window_set_timeout_is_not_cancelled(self):
        def content_timeout_triggered(mn):
            return mn.execute_script("return window.n", sandbox=None) > 0

        # subsequent call to execute_script after this
        # should not cancel the setTimeout event
        self.marionette.navigate(
            inline(
                """
            <script>
            window.n = 0;
            setTimeout(() => ++window.n, 4000);
            </script>"""
            )
        )

        # as debug builds are inherently slow,
        # we need to assert the event did not already fire
        self.assertEqual(
            0,
            self.marionette.execute_script("return window.n", sandbox=None),
            "setTimeout already fired",
        )

        # if event was cancelled, this will time out
        Wait(self.marionette, timeout=8).until(
            content_timeout_triggered,
            message="Scheduled setTimeout event was cancelled by call to execute_script",
        )

    def test_access_chrome_objects_in_event_listeners(self):
        # sandbox.window.addEventListener/removeEventListener
        # is used by Marionette for installing the unloadHandler which
        # is used to return an error when a document is unloaded during
        # script execution.
        #
        # Certain web frameworks, notably Angular, override
        # window.addEventListener/removeEventListener and introspects
        # objects passed to them.  If these objects originates from chrome
        # without having been cloned, a permission denied error is thrown
        # as part of the security precautions put in place by the sandbox.

        # addEventListener is called when script is injected
        self.marionette.navigate(
            inline(
                """
            <script>
            window.addEventListener = (event, listener) => listener.toString();
            </script>
            """
            )
        )
        self.marionette.execute_script("", sandbox=None)

        # removeEventListener is called when sandbox is unloaded
        self.marionette.navigate(
            inline(
                """
            <script>
            window.removeEventListener = (event, listener) => listener.toString();
            </script>
            """
            )
        )
        self.marionette.execute_script("", sandbox=None)

    def test_access_global_objects_from_chrome(self):
        # test inspection of arguments
        self.marionette.execute_script("__webDriverArguments.toString()")

    def test_toJSON(self):
        foo = self.marionette.execute_script(
            """
            return {
              toJSON () {
                return "foo";
              }
            }
        """,
            sandbox=None,
        )
        self.assertEqual("foo", foo)

    def test_unsafe_toJSON(self):
        el = self.marionette.execute_script(
            """
            return {
              toJSON () {
                return document.documentElement;
              }
            }
        """,
            sandbox=None,
        )
        self.assert_is_web_element(el)
        self.assertEqual(el, self.marionette.find_element(By.CSS_SELECTOR, ":root"))

    def test_comment_in_last_line(self):
        self.marionette.execute_script(" // comment ")

    def test_return_value_on_alert(self):
        res = self.marionette.execute_script("alert()")
        self.assertIsNone(res)


class TestExecuteChrome(ChromeHandlerMixin, WindowManagerMixin, TestExecuteContent):
    def setUp(self):
        super(TestExecuteChrome, self).setUp()

        self.marionette.set_context("chrome")
        win = self.open_chrome_window(self.chrome_base_url + "test.xhtml")
        self.marionette.switch_to_window(win)

    def tearDown(self):
        self.close_all_windows()

        super(TestExecuteChrome, self).tearDown()

    def test_permission(self):
        self.marionette.execute_script(
            "Components.classes['@mozilla.org/preferences-service;1']"
        )

    def test_unmarshal_element_collection(self):
        expected = self.marionette.find_elements(By.TAG_NAME, "input")
        actual = self.marionette.execute_script(
            "return document.querySelectorAll('input')"
        )
        self.assertTrue(len(expected) > 0)
        self.assertEqual(expected, actual)

    def test_argument_shadow_root(self):
        pass

    def test_argument_web_element(self):
        elem = self.marionette.find_element(By.TAG_NAME, "input")
        nodeType = self.marionette.execute_script(
            "return arguments[0].nodeType", script_args=(elem,)
        )
        self.assertEqual(nodeType, 1)

    def test_async_script_timeout(self):
        with self.assertRaises(errors.ScriptTimeoutException):
            self.marionette.execute_async_script(
                """
                var cb = arguments[arguments.length - 1];
                setTimeout(function() { cb() }, 2500);
                """,
                script_timeout=100,
            )

    def test_lasting_side_effects(self):
        pass

    def test_return_web_element(self):
        pass

    def test_return_web_element_array(self):
        pass

    def test_return_web_element_nested_array(self):
        pass

    def test_return_web_element_nested_dict(self):
        pass

    def test_return_web_element_nodelist(self):
        pass

    def test_window_set_timeout_is_not_cancelled(self):
        pass

    def test_mutable_sandbox_wrappedjsobject(self):
        pass

    def test_default_sandbox_wrappedjsobject(self):
        pass

    def test_system_sandbox_wrappedjsobject(self):
        pass

    def test_access_chrome_objects_in_event_listeners(self):
        pass

    def test_return_value_on_alert(self):
        pass

    def test_syntaxerror_stack(self):
        pass
