import os
import re
import inspect
# noinspection PyPackageRequirements
import pytest
import collections
from time import sleep
# noinspection PyPackageRequirements
from selenium import webdriver
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
from selenium.webdriver.remote.webelement import WebElement
import logging


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

    _homepage = None
    _test_logger = None # Reference to test logger
    _tlib_logger = None # Reference to tlib logger
    _browser = 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
    _screenshot_report_template = _jinja_env.get_template("screenshot_report.html") # Template for generating screenshot report

    _screenshots = {} # Screenshots generated during a test case
    _tc_screenshot_template = _jinja_env.get_template("testcase_screenshots.html") # Template for generating screenshots for a test case


    def _get_browser(self):
        """
        Returns Instance of the WebDriver browser
        @rtype: webdriver.Remote
        """
        return self.__class__._browser


    def _set_browser(self, browser):
        """
        Instance of the WebDriver browser
        @type browser: webdriver.Remote
        """
        self.__class__._browser = browser

    browser = property(_get_browser, _set_browser)

    def _get_homepage(self):
        """
        Instance of the WebDriver homepage
        """
        return self.__class__._homepage


    def _set_homepage(self, homepage):
        """
        Instance of the WebDriver homepage
        @type homepage: webdriver.Remote
        """
        self.__class__._homepage = homepage

    homepage = property(_get_homepage, _set_homepage)


    @property
    def test_logger(self):
        """
        Logger object to log test information
        """
        return self.__class__._test_logger


    @staticmethod
    def screenshot_folder():
        """
        Returns project's screenshot folder
        This method must be defined in subclasses
        """
        raise NotImplementedError("Subclasses should implement this!")


    @pytest.fixture(scope='class', autouse=True)
    def initialize_class(self, test_logger, tlib_logger, browser, base_url):
        """
        @type test_logger: logging
        @type browser: webdriver.Remote
        Stores an instance of browser and test_logger
        """
        self.__class__._test_logger = test_logger
        self.__class__._tlib_logger = tlib_logger
        self.browser = browser
        # Only initialize homepage if it wasn't already initialized
        if self.homepage is None:
            self.homepage = base_url


    def get_unused_report_name(self, tc_id, tc_name):
        """
        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
        @param tc_name: Test case name
        @type tc_name: str
        """
        i = 0
        while True:
            if i == 0:
                # filename = "%(tc_id)s - %(tc_name)s" % { "tc_id": tc_id, "tc_name": tc_name}
                filename = "%(tc_id)s" % { "tc_id": tc_id}
            else:
                # filename = "%(tc_id)s - %(tc_name)s[retry %(cnt)d]" % { "tc_id": tc_id, "tc_name": tc_name, "cnt": i}
                filename = "%(tc_id)s [retry %(cnt)d]" % { "tc_id": tc_id, "cnt": i}

            if not filename in self.__class__._screenshot_report:
                return filename

            i += 1


    @pytest.fixture(scope='class', autouse=True)
    def generate_class_report(self, request):
        """
        Generates HTML page with all test case results for the class
        @type request: FixtureRequest
        """
        self.__class__._screenshot_report = collections.OrderedDict()
        self.__class__._folder_dest = re.sub('\.py$', '', os.path.relpath(inspect.getfile(request.cls)))

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

                htm_file = "%s.htm" % self.__class__._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 generate_test_report(self, request):
        """
        Generates HTML page with all the screenshots for a test case
        Supports generating different When test cases are run multiple times with the same arguments
        @type request: FixtureRequest
        """
        # noinspection PyProtectedMember
        self.__class__._test_params = request.keywords.node._genid
        self.__class__._test_case_name = request.keywords.node.name
        self.__class__._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.__class__._test_case_id = "UNKNOWN_TEST_CASE_ID"
        else:
            self.__class__._test_case_id = marker.args[0]


        def generate_report():

            if len(self.__class__._screenshots) > 0:
                name = self.get_unused_report_name(self.__class__._test_case_id, self.__class__._test_case_name)

                #Generate HTML based on template
                html = self.__class__._tc_screenshot_template.render(test_case_id=self.__class__._test_case_id,
                            test_case_name=self.__class__._test_case_name,
                            screenshots=self.__class__._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.__class__._folder_dest, htm_file)
                full_filename = os.path.join(self.screenshot_folder(), relative_filename)

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

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


        request.addfinalizer(generate_report)


    def navigate(self, page, save_screenshot=True):
        """
        Navigate to "page"
        """
        self.test_logger.info(u"Navigating to %s" % page)
        self.browser.get(page)
        if save_screenshot:
            self.save_screenshot(u"Navigate to %s" % page)


    def go_home(self, save_screenshot=True):
        """
        Navigate to home page
        """
        self.navigate(self.homepage, save_screenshot=False)
        if save_screenshot:
            self.save_screenshot("Homepage")


    def save_screenshot(self, description):
        """
        Saves screen shot for the current page
        """

        def get_params():
            if self.__class__._test_params is None:
                return ""
            else:
                return self.__class__._test_params

        #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.__class__._test_case_id, self.__class__._test_case_name)
        name = FileHelper.get_filename_from_string(name)

        test_folder = os.path.join(self.screenshot_folder(), self.__class__._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.__class__._screenshots[i] = {"filename": "%s/%s" % (name, img_filename),
                                          u"description": description}

        self.browser.get_screenshot_as_file(full_path)


    def wait_for_page_loaded(self, timeout=10):
        """
        Waist until document.readyState is equal to complete
        @type timeout: Integer
        @param timeout: Number of seconds before timing out
        """
        if self.browser.execute_script("return document.readyState") == "complete":
            return
        else:
            self._tlib_logger.debug("Waiting for '%s' to load" % self.browser.current_url)

        condition = lambda *args: self.browser.execute_script("return document.readyState") == "complete"
        try:
            WebDriverWait(self.browser, timeout).until(condition)
        except TimeoutException:
            self.test_logger.error('Timeout while waiting for page to load')
            pytest.fail('Timeout while waiting for page to load')

        self._tlib_logger.debug("Page '%s' finished loading" % self.browser.current_url)

    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.browser.switch_to_alert().text
                return True
            except NoAlertPresentException as e:
                return False

        condition = lambda *args: is_alert_visible()
        try:
            WebDriverWait(self.browser, timeout).until(condition)
            return self.browser.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')

    @pytest.fixture(scope='function', autouse=True)
    def setup_test(self, request, test_logger, browser):
        """
        Goes to homepage before each test, unless marker skipsetup is given
        """
        #Don't go to home is skipsetup marker was given
        if not request.node.function.__dict__.has_key('skipsetup'):
            #Before clearing cookies, need to go to home screen, to ensure it's the current domain
            self.go_home(save_screenshot=False)
            browser.delete_all_cookies()
            self.go_home()
        else:
            test_logger.info("Skipping setup")


    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.browser, 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.browser, 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.browser, timeout).until(expected_conditions.alert_is_present((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.browser, 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.browser, 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.browser, 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.browser.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.browser, 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.browser, 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)
