"""

Robot library with often needed keywords.

"""

import datetime
import os
import random
import time

import robot
from SeleniumLibrary import LibraryComponent, JavaScriptKeywords, SelectElementKeywords
from SeleniumLibrary.errors import ElementNotFound
from SeleniumLibrary.utils import is_noney
from dateutil.relativedelta import relativedelta
from robot.api import logger
from robot.api.deco import keyword
from robot.errors import RobotError
from robot.libraries.BuiltIn import BuiltIn
from robot.libraries.Collections import Collections
from robot.utils.asserts import assert_true
from robot.utils.robottime import secs_to_timestr
from robot.utils.robottypes import *
from selenium import webdriver
from selenium.common.exceptions import StaleElementReferenceException


class Error(RuntimeError):
    ROBOT_CONTINUE_ON_FAILURE = True


class FacileToolkit2:
    ROBOT_LIBRARY_SCOPE = "GLOBAL"
    ROBOT_LIBRARY_DOC_FORMAT = "ROBOT"

    @keyword
    def wait(self, keyword, *keywordargs, retry="15x", retry_interval="1s"):
        """Automatically retries the given keywords/arguments 10 times, each 3 seconds
        """
        BuiltIn().wait_until_keyword_succeeds(retry, retry_interval, keyword, *keywordargs)

    @keyword
    def get_element_class(self, locator):
        self.set_selenium_library()
        return self.sl.find_element(locator).get_attribute("class")

    @keyword
    def get_element_css_property(self, locator, property):
        self.set_selenium_library()
        return self.sl.find_element(locator).value_of_css_property(property)

    @keyword
    def element_css_property_should_be(self, locator, property, expected):
        return BuiltIn().should_be_equal_as_strings(self.get_element_css_property(locator, property), expected)

    @keyword
    def element_attribute_should_contain_value(self, locator, attribute, value, message=None):
        self.set_selenium_library()
        current_expected = self.sl.find_element(locator).get_attribute(attribute)
        if value not in current_expected:
            if is_noney(message):
                message = ("Element '%s' attribute should have contained value '%s' but "
                           "its value was '%s'." % (locator, value, current_expected))
            raise AssertionError(message)
        self.lc.info("Element '%s' attribute '%s' contains value '%s'." % (locator, attribute, value))

    @keyword
    def element_attribute_should_not_contain_value(self, locator, attribute, value, message=None):
        self.set_selenium_library()
        current_expected = self.sl.find_element(locator).get_attribute(attribute)
        if value in current_expected:
            if is_noney(message):
                message = ("Element '%s' attribute should not have contained value '%s' but "
                           "its value was '%s'." % (locator, value, current_expected))
            raise AssertionError(message)
        self.lc.info("Element '%s' attribute '%s' does not contain value '%s'." % (locator, attribute, value))

    @keyword
    def clear_session(self):
        """
        This keyword cleans the current session in order to isolate the execution
        """
        self.set_selenium_library()
        self.sl.delete_all_cookies()
        self.sl.reload_page()

    @keyword
    def scroll(self, range="0"):
        """ Scrolls the document.

            Ranges might span between 0 to X where 0 is the beginning of the document.

            default is set to 0.
        """
        self.set_selenium_library()
        self.sl.execute_javascript("document.documentElement.scrollTop = " + range + ";")

    @keyword
    def dom_is_loaded(self, timeout="10s"):
        """
        returns True if DOM readyState equals "complete" before the given timeout.
        """
        self.set_selenium_library()
        self.sl.wait_for_condition("return document.readyState=='complete'", timeout)

    @keyword
    def checkpoint(self, locator, timeout=None, error=None):
        self.set_selenium_library()
        self._wait_until(
            lambda: self.sl.find_element(locator).is_displayed() and self.sl.execute_javascript(
                "return document.readyState=='complete'"),
            "Element '%s' or their parents did not contain class in <TIMEOUT>." % locator, timeout, error
        )

    @keyword
    def select_from_list_random_index(self, locator, start_index=None, end_index=None, timeout=None, error=None):
        """
        Selects a random option from a Select WebElement specified by a locator,
        it is possible to specify a start index for the random selection
        """
        self.set_selenium_library()
        self._wait_until(
            lambda: not is_noney(self.lc.find_element(locator, required=False)) and
                    len(SelectElementKeywords(self.sl)._get_options(locator)),
            "Error selecting list %s random item" % locator,
            timeout, error
        )
        len_options = len(SelectElementKeywords(self.sl)._get_options(locator))
        if start_index is not None:
            assert_true(len_options >= int(start_index), "List length %s is less than start_index %s"
                        % (len_options, start_index))
        end_index = len_options - 1 if is_noney(end_index) or int(end_index) > len_options else int(end_index)
        random_index = self.generate_random_number_as_string(0, end_index)
        self.sl.select_from_list_by_index(locator, random_index)
        self.lc.info("Selected index %s from list %s" % (random_index, locator))

    @keyword
    def select_from_list_random_index_optional(self, *locators, start_index=None, end_index=None, timeout=None):
        """
        Wrapper for "select from list random index" keyword, the random selection
        is performed only if the select element is present on the page
        """
        timeout = self.lc.get_timeout(timeout)
        for locator in locators:
            lista_presente = BuiltIn().run_keyword_and_return_status("Wait Until Element is Visible", locator, timeout)
            if lista_presente:
                self.select_from_list_random_index(locator=locator, start_index=start_index, end_index=end_index,
                                                   timeout=timeout)

    @keyword
    def i_land_on_page(self, url, timeout="60s"):
        """
        Waits for a maximum time, defined by timeout argument (default=60s),
        until the location contains a portion of the url (specified by the parameter 'url')
        """
        self.set_selenium_library()
        try:
            self.sl.log_location()
            self.sl.wait_until_location_contains(url, timeout)
            self.dom_is_loaded(timeout=timeout)
        finally:
            self.sl.capture_page_screenshot(filename="EMBED")
            self.sl.log_location()

    @keyword
    def click_js(self, locator):
        """
        This keyword clicks a WebElement or a locator that represent a WebElement via Javascript
        """
        is_webelement = "webelement" in str(type(locator)).lower()
        if is_webelement:
            self.click_element_js(locator)
        else:
            self.click_locator_js(locator)

    @keyword
    def click_element_js(self, element):
        """
        This keyword clicks a WebElement via Javascript
        """
        self.set_selenium_library()
        self.sl.execute_javascript("arguments[0].click()", "ARGUMENTS", element)

    @keyword
    def click_locator_js(self, locator):
        """
        Clicks a locator that represent a WebElement via Javascript
        """
        self.set_selenium_library()
        element = self.sl.get_webelement(locator)
        self.sl.execute_javascript("arguments[0].click()", "ARGUMENTS", element)

    @keyword
    def i_see(self, *locators):
        """
        Loops over an array of locators and checks
        if the webelements represented by them are visible on the page
        """
        """
        Loops over an array of locators and checks
        if the webelements represented by them are visible on the page
        """
        self.set_selenium_library()
        not_visible_elements = [x for x in locators if
                                is_noney(self.lc.find_element(x, required=False)) or not self.sl.find_element(
                                    x).is_displayed()]
        if not_visible_elements:
            errors = ', '.join(not_visible_elements)
            raise AssertionError("Following elements are not visible: %s" % errors)

    @keyword
    def i_dont_see(self, *locators):
        """
        Loops over an array of locators and checks
        if the webelements represented by them are visible on the page
        """
        self.set_selenium_library()
        visible_elements = [x for x in locators if
                            not is_noney(self.lc.find_element(x, required=False)) and self.sl.find_element(
                                x).is_displayed()]
        if visible_elements:
            errors = ', '.join(visible_elements)
            raise AssertionError("Following elements are visible: %s" % errors)

    @keyword
    def select_first_autocomplete_option(self,
                                         locator_option="css:.autocomplete-suggestions:not([style*='display']):not([style*='none']) strong",
                                         timeout=None, error=None):
        """
        Selects the first autocomplete option for auto-complete fields.
        """
        self.set_selenium_library()
        self.sl.wait_until_element_is_visible(locator_option)
        self.click_js(locator_option)

    @keyword
    def press_keys_on_active_element(self, *keys):
        """
        Inputs the given key to the active element in DOM
        """
        self.set_selenium_library()
        active_element = self.sl.execute_javascript("return document.activeElement;")
        self.sl.press_keys(active_element, *keys)

    @keyword
    def get_text_from_webelements(self, *webelements, timeout=None, error=None):
        """
        Takes in input a WebElements array and
        returns a string array containing the stripped texts retrieved from the webelements
        """
        self.set_selenium_library()
        self._wait_until(
            lambda: [self.sl.get_text(element) for element in webelements],
            "Error retrieving element texts",
            timeout, error
        )
        return [self.sl.get_text(element) for element in webelements]

    @keyword
    def generate_random_number_as_string(self, minimum, maximum):
        """
        Generates a random number between the range given by "min", "max" arguments
        and returns it as string
        """
        minimum = BuiltIn().convert_to_integer(minimum)
        maximum = BuiltIn().convert_to_integer(maximum)
        num = str(random.randint(minimum, maximum))
        return num

    @keyword
    def select_random_item_from_list(self, *in_list):
        """
        Returns a random item from the given list
        """

        if is_list_like(in_list):
            if list:
                return random.choice(*in_list)
            else:
                raise AssertionError("List is empty")
        else:
            raise AssertionError("Argument is %s, not a list" % type(in_list))

    @keyword
    def error_message_should_be(self, message, error_locator="//span[contains(@class, 'err') and "
                                                             "contains(@class, 'wrong') and "
                                                             "normalize-space(.)]", timeout=None,
                                case_insensitive=True, match=True):
        """
        Checks if the given error message is present in DOM
        """
        self.set_selenium_library()
        try:
            self.sl.wait_until_element_is_visible(error_locator, timeout)
            error_msgs = self.sl.get_webelements(error_locator)
            found_errors = [el.text for el in error_msgs]
            Collections().log_list(found_errors)
            if is_truthy(match):
                Collections().should_contain_match(found_errors, message, case_insensitive=is_truthy(case_insensitive))
            else:
                Collections().list_should_contain_value(found_errors, message)
            logger.info("%s is contained in error message" % message)
        except Exception as error_thrown:
            raise Error(error_thrown)

    @keyword
    def wait_ajax(self, timeout="60s", wait_active="400ms"):
        self.set_selenium_library()
        BuiltIn().run_keyword_and_ignore_error("wait_ajax_activation", "400ms")
        self.sl.wait_for_condition('if(typeof(jQuery) == "function") {return jQuery.active == 0} return false', timeout)

    @keyword
    def wait_ajax_activation(self, timeout="400ms"):
        self.set_selenium_library()
        self.sl.wait_for_condition('if(typeof(jQuery) == "function") {return jQuery.active == 1} return false', timeout)

    @keyword
    def field_or_parents_should_contain_error(self, locator, timeout=None, error_class="wrong", error=None):
        self.wait_ultil_element_or_parents_contain_class(locator, error_class, timeout)

    @keyword
    def field_or_parents_should_be_verified(self, locator, timeout=None, verified_class="verified", error=None):
        self.wait_ultil_element_or_parents_contain_class(locator, verified_class, timeout)

    @keyword
    def field_should_contain_error(self, locator, timeout=None, error_class="wrong", error=None):
        self.wait_ultil_element_contains_class(locator, error_class, timeout)

    @keyword
    def field_should_be_verified(self, locator, timeout=None, error_class="verified", error=None):
        self.wait_ultil_element_contains_class(locator, error_class, timeout)

    @keyword
    def element_attribute_value_should_not_be(self, locator, attribute, expected, message=None):
        self.set_selenium_library()
        current_expected = self.sl.find_element(locator).get_attribute(attribute)
        if current_expected == expected:
            if is_noney(message):
                message = ("Element '%s' attribute should not have value '%s'"
                           % (locator, expected))
            raise AssertionError(message)
        self.lc.info("Element '%s' attribute '%s' contains value '%s'." % (locator, attribute, expected))

    @keyword
    def textfield_value_should_not_be(self, locator, expected, message=None):
        """Verifies text field ``locator`` does not have text ``expected``.

        ``message`` can be used to override default error message.

        See the `Locating elements` section for details about the locator
        syntax.
        """
        self.set_selenium_library()
        actual = self.lc.find_element(locator, 'text field').get_attribute('value')
        if actual == expected:
            if is_noney(message):
                message = "Value of text field '%s' should not have been '%s' " \
                          "." % (locator, expected)
            raise AssertionError(message)
        self.lc.info("Content of text field '%s' is '%s'." % (locator, expected))

    @staticmethod
    @keyword
    def get_browser_options(browser, headless=False):
        if browser.lower() == "chrome":
            return FacileToolkit2.get_chrome_options(headless=headless)
        elif browser == "firefox" or "ff":
            return FacileToolkit2.get_ff_options(headless=headless)
        else:
            raise RobotError("{} unexpected browser name.".format(browser))

    @staticmethod
    @keyword
    def get_chrome_options(headless=False):
        chrome_options = webdriver.ChromeOptions()
        chrome_options.add_argument("--disable-popup-blocking")
        chrome_options.add_argument("--disable-extensions")
        chrome_options.add_argument("--disable-infobars")
        chrome_options.add_argument("--ignore-certificate-errors")
        chrome_options.add_argument("--enable-features=NetworkService,NetworkServiceInProcess")
        chrome_options.add_argument("--no-sandbox")
        chrome_options.add_argument("--disable-gpu")
        chrome_options.add_argument("--disable-browser-side-navigation")
        chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
        if headless:
            chrome_options.add_argument("--window-size=1920,960")
            chrome_options.headless = True
        return chrome_options

    @staticmethod
    @keyword
    def get_ff_options(headless=False):
        os.environ['MOZ_HEADLESS_WIDTH'] = '1920'
        os.environ['MOZ_HEADLESS_HEIGHT'] = '960'
        firefox_options = webdriver.FirefoxOptions()
        firefox_options.add_argument("--no-sandbox")
        firefox_options.add_argument("--disable-gpu")
        firefox_options.add_argument("--disable-extensions")
        if headless:
            firefox_options.headless = True
        return firefox_options

    @staticmethod
    @keyword
    def get_chrome_mobile_options(headless=False, device="iPhone X"):
        chrome_options = webdriver.ChromeOptions()
        mobile_emulation = {"deviceName": device}
        chrome_options.add_experimental_option("mobileEmulation", mobile_emulation)
        chrome_options.add_argument("--disable-popup-blocking")
        chrome_options.add_argument("--disable-extensions")
        chrome_options.add_argument("--disable-infobars")
        chrome_options.add_argument("--ignore-certificate-errors")
        chrome_options.add_argument("--enable-features=NetworkService,NetworkServiceInProcess")
        chrome_options.add_argument("--no-sandbox")
        chrome_options.add_argument("--disable-gpu")
        chrome_options.add_argument("--disable-browser-side-navigation")
        chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
        if headless:
            chrome_options.headless = True
        return chrome_options

    @staticmethod
    @keyword
    def add_argument_to_browser_options(options, argument):
        options.add_argument(argument)
        return options

    @staticmethod
    @keyword
    def add_experimental_to_browser_options(options, argument, value):
        options.add_experimental_option(argument, value)
        return options

    @staticmethod
    def get_current_date_decremented_by_time(years=0, months=0, days=0):
        date = datetime.datetime.now() - relativedelta(years=years, months=months, days=days)
        return date.strftime('%Y'), date.strftime('%m'), date.strftime('%d')

    @keyword
    def wait_ultil_element_contains_class(self, locator, clazz, timeout=None, error=None):
        self.set_selenium_library()
        self._wait_until(
            lambda: clazz in self.sl.get_element_attribute(locator, "class"),
            "Element '%s' did not contain class '%s' in <TIMEOUT>." % (locator, clazz),
            timeout, error
        )

    @keyword
    def wait_ultil_element_does_not_contain_class(self, locator, clazz, timeout=None, error=None):
        self.set_selenium_library()
        self._wait_until(
            lambda: clazz not in self.sl.get_element_attribute(locator, "class"),
            "Element '%s' did not contain class '%s' in <TIMEOUT>." % (locator, clazz),
            timeout, error
        )

    def get_element_parents(self, locator):
        self.set_selenium_library()
        get_parents = "let parents = node => (node.parentElement ? parents(node.parentElement) : [])" \
                      ".concat([node]);return parents(arguments[0]);"
        return self.sl.driver.execute_script(get_parents, self.sl.find_element(locator))

    @keyword
    def wait_ultil_element_or_parents_contain_class(self, locator, clazz, timeout=None, error=None):
        elements = self.get_element_parents(locator)
        self._wait_until(
            lambda: any(clazz in element.get_attribute("class") for element in elements),
            "Element '%s' or their parents did not contain class '%s' in <TIMEOUT>." % (locator, clazz),
            timeout, error
        )

    @keyword
    def wait_ultil_element_or_parents_do_not_contain_class(self, locator, clazz, timeout=None, error=None):
        elements = self.get_element_parents(locator)
        self._wait_until(
            lambda: any(clazz not in element.get_attribute("class") for element in elements),
            "Element '%s' or their parents did not contain class '%s' in <TIMEOUT>." % (locator, clazz),
            timeout, error
        )

    @keyword
    def wait_for_condition_extended(self, *condition, timeout=None, error=None):
        self.set_selenium_library()
        jk = JavaScriptKeywords(self.sl)
        js_code, js_args = jk._get_javascript_to_execute(condition)
        if 'return' not in js_code:
            raise ValueError("Condition '%s' did not have mandatory 'return'." % js_code)
        self._wait_until(
            lambda: self.sl.execute_javascript(*condition),
            "Condition '%s' did not become true in <TIMEOUT>." % js_code,
            timeout, error
        )

    def _wait_until(self, condition, error, timeout=None, custom_error=None):
        self.set_selenium_library()
        timeout = self.lc.get_timeout(timeout)
        if is_noney(custom_error):
            error = error.replace('<TIMEOUT>', secs_to_timestr(timeout))
        else:
            error = custom_error
        self._wait_until_worker(condition, timeout, error)

    def _wait_until_worker(self, condition, timeout, error):
        self.set_selenium_library()
        max_time = time.time() + timeout
        not_found = None
        while time.time() < max_time:
            try:
                if condition():
                    return
            except ElementNotFound as err:
                not_found = str(err)
            except StaleElementReferenceException as err:
                self.sl.info('Suppressing StaleElementReferenceException from Selenium.')
                not_found = err
            else:
                not_found = None
            time.sleep(0.2)
        raise AssertionError(not_found or error)

    def set_selenium_library(self):
        self.sl = BuiltIn().get_library_instance('SeleniumLibrary')
        self.lc = LibraryComponent(self.sl)


if __name__ == "__main__":
    robot.run('../atests/keyword_tests.robot')
