import os
import inspect
# noinspection PyPackageRequirements
import pytest
import collections
from time import sleep
# noinspection PyPackageRequirements
from _pytest.python import FixtureRequest
# noinspection PyPackageRequirements
import jinja2
from tlib.base import TestHelper
from tlib.base import FileHelper
from tlib.base.PytestTester import PytestTester
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from selenium.common.exceptions import TimeoutException, NoAlertPresentException, NoSuchElementException
from selenium.webdriver.remote.webelement import WebElement
from TestHelper import Singleton


# noinspection PyMethodParameters
# noinspection PyUnresolvedReferences
class WebDriverTester(PytestTester):

    __metaclass__ = Singleton

    _driver = None

    _folder_dest = None  # destination of report for the current class
    _test_case_id = None
    _test_case_name = None
    _test_params = None

    _jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(TestHelper.tlib_template_folder()),
                                    trim_blocks=True)

    _screenshot_report = None  # List of tests for which there are screenshots

    # Template for generating screenshot report
    _screenshot_report_template = _jinja_env.get_template("screenshot_report.html")

    _screenshots = {}  # Screenshots generated during a test case

    # Template for generating screenshots for a test case
    _tc_screenshot_template = _jinja_env.get_template("testcase_screenshots.html")

    _request = None

    def screenshot_folder(self):
        """
        Returns location of screenshot folder

        @return: str
        """
        screenshot_folder = self._find_screenshot_folder()
        if screenshot_folder is None:
        #Couldn't find screenshot folder
            raise NotImplementedError("Could not find screenshot folder. "
                                      "Class should implement method screenshot_folder")
        else:
            return screenshot_folder

    def _find_screenshot_folder(self):
        """
        Will try to find a folder named "screenshot" starting from the file being executed and up to
        three levels up

        @return: str
        """
        #Get current folder
        curr_folder = os.path.dirname(self._request.fspath.strpath)

        #Go up to three levels to find screenshot folder
        for i in range(1, 4):
            curr_folder = os.path.abspath(os.path.join(curr_folder, os.pardir))

            #Check if there is a folder 'screenshots'
            screenshot_folder = os.path.join(curr_folder, 'screenshots')
            if os.path.exists(screenshot_folder):
                return screenshot_folder

    def _get_unused_report_name(self, tc_id, tc_params):
        """
        Gets a string based on test case id and name, taking into account if test case has already been run or not
        @param tc_id: Test case id
        @type tc_id: str
        @type tc_name: str
        """
        i = 0
        while True:
            if i == 0:
                filename = "%(tc_id)s_%(tc_params)s" % {"tc_id": tc_id, "tc_params": tc_params}
            else:
                filename = "%(tc_id)s_%(tc_params)s [retry %(cnt)d]" % \
                           {"tc_id": tc_id, "tc_params": tc_params, "cnt": i}

            if not filename in self._screenshot_report:
                return filename

            i += 1

    @pytest.fixture(scope='class', autouse=True)
    def initialize_webdriver_class(self, request):
        """
        @type request: FixtureRequest
        """

        #Generates initial HTML page with all test case results for the class
        self._screenshot_report = collections.OrderedDict()
        self._folder_dest = os.path.basename(os.path.dirname(os.path.abspath(inspect.getfile(request.cls))))

        def generate_report():
            if len(self._screenshot_report) > 0:
                #Generate HTML based on template
                html = self._screenshot_report_template.render(test_class=request.cls.__name__,
                                                               files=self._screenshot_report)

                htm_file = "%s.htm" % self._folder_dest
                full_filename = os.path.join(self.screenshot_folder(), htm_file)

                f = open(full_filename, "w")
                try:
                    f.write(html)
                finally:
                    f.close()

        request.addfinalizer(generate_report)

    @pytest.fixture(scope='function', autouse=True)
    def setup_webdriver_test(self, request):
        """
        Goes to homepage before each test, unless marker skipsetup is given

        @type request: FixtureRequest
        """
        #Store an instance of the request object. Required for the generation of screenshots
        self._request = request

        #Initialization required for screenshot generation
        #  noinspection PyProtectedMember
        self._test_params = request.keywords.node._genid
        self._test_case_name = request.keywords.node.name
        self._screenshots = {}

        #Get marker test case name or use a default if there is none
        marker = request.node.get_marker("testcasename")
        if marker is None:
            self.test_logger.warn("Test case doesn't have marker testcasename")
            self._test_case_id = "UNKNOWN_TEST_CASE_ID"
        else:
            self._test_case_id = marker.args[0]

        def generate_report():
            """
            Generates HTML page with all the screenshots for a test case
            Supports generating different files when test cases are run multiple times with the same arguments
            """

            if len(self._screenshots) > 0:
                name = self._get_unused_report_name(self._test_case_id, self._test_params)

                #Generate HTML based on template
                html = self._tc_screenshot_template.render(test_case_id=self._test_case_id,
                                                           test_case_name=self._test_case_name,
                                                           screenshots=self._screenshots)

                #Filename includes a counter in case test case is being run more than once
                htm_file = FileHelper.get_filename_from_string(name) + '.htm'
                relative_filename = os.path.join(self._folder_dest, htm_file)
                full_filename = os.path.join(self.screenshot_folder(), relative_filename)

                self._screenshot_report[name] = {"tc_id": self._test_case_id,
                                                 "tc_name": self._test_case_name,
                                                 "filename": relative_filename}

                f = open(full_filename, "w")
                try:
                    f.write(html)
                finally:
                    f.close()

        request.addfinalizer(generate_report)

    def save_screenshot(self, description):
        """
        Saves screen shot for the current page
        """
        #Waits until page is loaded
        self.tlib_logger.debug("Saving screenshot")
        self.wait_for_page_loaded()

        #Folder containing images for test case
        name = self._get_unused_report_name(self._test_case_id, self._test_params)
        name = FileHelper.get_filename_from_string(name)

        test_folder = os.path.join(self.screenshot_folder(), self._folder_dest, name)

        try:
            os.makedirs(test_folder)
        except WindowsError:
            pass

        #Get number of existing images
        i = len(os.listdir(test_folder)) + 1

        #Get filename
        img_filename = "%02d.png" % i
        full_path = os.path.join(test_folder, img_filename)

        self._screenshots[i] = {"filename": "%s/%s" % (name, img_filename),
                                u"description": description}

        self._driver.get_screenshot_as_file(full_path)

    def wait_for_page_loaded(self, timeout=10):
        raise NotImplementedError("This method should be implemented by derived classes")

    def wait_for_alert(self, timeout=10):
        """
        Waist until an alert is visible
        @type timeout: Integer
        @param timeout: Number of seconds before timing out
        @rtype: bool
        """
        def is_alert_visible():
            try:
                #Try to get alert text to trigger exception if alert is not visible
                alert = self._driver.switch_to_alert().text
                return True
            except NoAlertPresentException as e:
                return False

        condition = lambda *args: is_alert_visible()
        try:
            WebDriverWait(self._driver, timeout).until(condition)
            return self._driver.switch_to_alert()
        except TimeoutException:
            self.test_logger.error('Timeout while waiting for alert to appear')
            pytest.fail('Timeout while waiting for alert to appear')

    def wait_for_element_to_be_visible(self, locator_strategy, locator_string, error_msg=None, timeout=10):
        """
        Wait until an element becomes visible
        @param locator_strategy: Location strategy to use
        @type locator_strategy: By
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        @param timeout: Maximum time in seconds to wait for the element to be visible
        @type timeout: int
        @rtype: WebElement
        """
        try:
            element = WebDriverWait(self._driver, timeout).\
                until(expected_conditions.visibility_of_element_located((locator_strategy, locator_string)))
            return element
        except TimeoutException:
            if error_msg is None:
                error_msg = "Timeout while waiting for element '%s' to be visible" % locator_string
            self.save_screenshot("[ERROR] %s" % error_msg)
            pytest.fail(error_msg)

    def wait_for_element_to_be_clickable(self, locator_strategy, locator_string, error_msg=None, timeout=10):
        """
        Wait until an element cna be clicked
        @param locator_strategy: Location strategy to use
        @type locator_strategy: By
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        @param timeout: Maximum time in seconds to wait for the element to be clickable
        @type timeout: int
        @rtype: WebElement
        """
        try:
            element = WebDriverWait(self._driver, timeout).\
                until(expected_conditions.element_to_be_clickable((locator_strategy, locator_string)))
            return element
        except TimeoutException:
            if error_msg is None:
                error_msg = "Timeout while waiting for element '%s' to be clickable" % locator_string
            self.save_screenshot("[ERROR] %s" % error_msg)
            pytest.fail(error_msg)

    def wait_for_element_to_be_present(self, locator_strategy, locator_string, error_msg=None, timeout=10):
        """
        Wait until an element is present
        @param locator_strategy: Location strategy to use
        @type locator_strategy: By
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        @param timeout: Maximum time in seconds to wait for the element to be present
        @type timeout: int
        @rtype: WebElement
        """
        try:
            element = WebDriverWait(self._driver, timeout).\
                until(expected_conditions.presence_of_element_located((locator_strategy, locator_string)))
            return element
        except TimeoutException:
            if error_msg is None:
                error_msg = "Timeout while waiting for element '%s' to be present" % locator_string
            self.save_screenshot("[ERROR] %s" % error_msg)
            pytest.fail(error_msg)

    def wait_for_element_to_be_selected(self, locator_strategy, locator_string, error_msg=None, timeout=10):
        """
        Wait until an element is selected
        @param locator_strategy: Location strategy to use
        @type locator_strategy: By
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        @param timeout: Maximum time in seconds to wait for the element to be selected
        @type timeout: int
        @rtype: WebElement
        """
        try:
            element = WebDriverWait(self._driver, timeout).\
                until(expected_conditions.element_located_to_be_selected((locator_strategy, locator_string)))
            return element
        except TimeoutException:
            if error_msg is None:
                error_msg = "Timeout while waiting for element '%s' to be selected" % locator_string
            self.save_screenshot("[ERROR] %s" % error_msg)
            pytest.fail(error_msg)

    def wait_for_element_to_be_invisible(self, locator_strategy, locator_string, error_msg=None, timeout=10):
        """
        Wait until an element becomes invisible
        @param locator_strategy: Location strategy to use
        @type locator_strategy: By
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        @param timeout: Maximum time in seconds to wait for the element to be hidden
        @type timeout: int
        @rtype: WebElement
        """
        try:
            element = WebDriverWait(self._driver, timeout).\
                until(expected_conditions.invisibility_of_element_located((locator_strategy, locator_string)))
            return element
        except TimeoutException:
            if error_msg is None:
                error_msg = "Timeout while waiting for element '%s' to be invisible" % locator_string
            self.save_screenshot("[ERROR] %s" % error_msg)
            pytest.fail(error_msg)

    def wait_for_element_to_be_static(self, locator_strategy, locator_string, error_msg=None, timeout=10):
        """
        Wait until an element that moves on the screen stops moving
        @param locator_strategy: Location strategy to use
        @type locator_strategy: By
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        @param timeout: Maximum time in seconds to wait for the element to be visible
        @type timeout: int
        @rtype: WebElement
        """
        try:
            element = WebDriverWait(self._driver, timeout).\
                until(expected_conditions.visibility_of_element_located((locator_strategy, locator_string)))

            #wait until element is not moving or click will fail
            old_location = {'x': 0, 'y': 0}
            while old_location != element.location:
                self.tlib_logger.debug("Pop-up is still moving. Previous position: %s, current position: %s" %
                                       (old_location, element.location))
                old_location = element.location
                sleep(0.1)
                element = self._driver.find_element(locator_strategy, locator_string)

            return element
        except TimeoutException:
            if error_msg is None:
                error_msg = "Timeout while waiting for element '%s' to be visible" % locator_string
            self.save_screenshot("[ERROR] %s" % error_msg)
            pytest.fail(error_msg)

    def wait_for_text_to_be_present_in_element(self, locator_strategy, locator_string, text,
                                               error_msg=None, timeout=10):
        """
        Wait for an element that contains specified text
        @param locator_strategy: Location strategy to use
        @type locator_strategy: By
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        @param timeout: Maximum time in seconds to wait
        @type timeout: int
        """
        try:
            WebDriverWait(self._driver, timeout).\
                until(expected_conditions.text_to_be_present_in_element((locator_strategy, locator_string), text))
        except TimeoutException:
            if error_msg is None:
                error_msg = "Timeout while waiting for text %(text)s to be present in element '%(element)s'" % \
                            {"text": text, "element": locator_string}
            self.save_screenshot("[ERROR] %s" % error_msg)
            pytest.fail(error_msg)

    def wait_for_text_to_be_present_in_element_value(self, locator_strategy, locator_string, text,
                                                     error_msg=None, timeout=10):
        """
        Wait for an element's value to contain some test
        @param locator_strategy: Location strategy to use
        @type locator_strategy: By
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        @param timeout: Maximum time in seconds to wait
        @type timeout: int
        """
        try:
            WebDriverWait(self._driver, timeout).\
                until(expected_conditions.text_to_be_present_in_element_value((locator_strategy, locator_string), text))
        except TimeoutException:
            if error_msg is None:
                error_msg = "Timeout while waiting for text %(text)s to be present " \
                            "in the value of element '%(element)s'" % {"text": text, "element": locator_string}
            self.save_screenshot("[ERROR] %s" % error_msg)
            pytest.fail(error_msg)


    def get_webelement_by_link_text(self, locator_string):
        """
        Get the webelement by link text
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        """
        try:
            return self.browser.find_element_by_link_text(locator_string)
        except NoSuchElementException:
            error_msg="Could not find the link: '%s'"
            self.save_screenshot(error_msg % locator_string)
            pytest.fail(error_msg % locator_string)

    def get_webelement_by_xpath(self, locator_string):
        """
        Get the webelement by xpath
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        """
        try:
            self.wait_for_element_to_be_visible(By.XPATH, locator_string)
            return self.browser.find_element_by_xpath(locator_string)
        except NoSuchElementException:
            error_msg="Could not find the xpath: '%s'"
            self.save_screenshot(error_msg % locator_string)
            pytest.fail(error_msg % locator_string)
    #
    def get_webelement_by_css(self, locator_string):
        """
        Get the webelement by CSS
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        """
        try:
            self.wait_for_element_to_be_visible(By.CSS_SELECTOR, locator_string)
            return self.browser.find_element_by_css_selector(locator_string)
        except NoSuchElementException:
            error_msg="Could not find css: '%s'"
            self.save_screenshot(error_msg % locator_string)
            pytest.fail(error_msg %locator_string)
    #
    def get_webelements_by_xpath(self, locator_string):
        """
        Get the webelement list by xpath
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        """
        try:
            return  self.browser.find_elements_by_xpath(locator_string)
        except NoSuchElementException:
            error_msg="Could not find the link: '%s'"
            self.save_screenshot(error_msg % locator_string)
            pytest.fail(error_msg+ " '%s'" % locator_string)
    #
    def get_webelements_by_css(self, locator_string):
        """
        Get the webelement list by CSS
        @param locator_string: String used to locate element
        @type locator_string: str
        @param error_msg: Error string to show if element is not found
        @type error_msg: str
        """
        try:
            return self.browser.find_elements_by_css_selector(locator_string)
        except NoSuchElementException:
            error_msg="Could not find css: '%s'"
            self.save_screenshot(error_msg % locator_string)
            pytest.fail(error_msg % locator_string)