import time
from tlib.asset.wpt_batch_lib import wpt_batch_lib
import xml.etree.ElementTree as elementTree

#Constants used for creating the results object
SUMMARY = 'summary'                 #:Link to the summary report
RUNS = 'runs'                       #:How many test runs were executed
HEADERS_DATA = 'headers'            #:Link to raw headers data
PAGE_DATA = 'pageData'              #:Link to raw page data
REQUESTS_DATA = 'requestsData'      #:Link to raw requests data
UTILIZATION_DATA = 'Utilization'    #:Link to raw utilization data
PAGE_SPEED_DATA = 'pageSpeedData'   #:Link to raw page speed data
FIRST_VIEW = 'firstView'            #:Results for first view (with empty cache)
REPEAT_VIEW = 'repeatView'          #:Results for repeat view (with cache populated)
AVERAGE = 'average'                 #:Response time average
STD_DEV = 'standardDeviation'       #:Response time standard deviation
MEDIAN = 'median'                   #:Response time median
TTFB = 'TTFB'                       #:Time to first byte
REQUESTS = 'requests'               #:Total number of requests
REQUESTS_DOC = 'requestsDoc'        #:Number of requests before document is complete
LOAD_TIME = 'loadTime'              #:Page load time until document is complete
FULLY_LOADED = 'fullyLoaded'        #Page load time until there is no traffic for 2 seconds

class WptHelper(object):
    """
    Helper class to send jobs to WebPagetest and get job results
    """
    _wpt_server = None
    _wpt_params = {}
    _logger = None

    def __init__(self, logger, wpt_server, wpt_params):
        """
        Constructor for class 

        @param logger: instance of a logging object configured in testing project
        @type logger: logging.Logger
        @param wpt_server: URL of the WebPagetest server used for testing, e.g. http://172.27.108.148:8080
        @type wpt_server: str
        @param wpt_params: Dictionarry with test settings. See https://sites.google.com/a/webpagetest.org/docs/advanced-features/webpagetest-restful-apis for more details
        @type wpt_params: dict
        """
        logger.debug("Initializing WptOrchestrator")
        logger.debug("WebPagetest server: %s" % wpt_server)
        logger.debug("WebPagetest test parameters: %s" % wpt_params)

        self._logger = logger
        self._wpt_server = wpt_server
        self._wpt_params = wpt_params

    def test_urls_and_wait(self, requested_urls):
        """
        Submits a test request for a list of URLs and waits until results are returned

        @param requested_urls: List of URLs to test
        @type requested_urls: list
        @return: Results as generated by WebPagetest
            For each item in the response, URL is the key and a dom object is the value
        @rtype: dict
        """
        self._logger.debug("Requested URLs\n%s" % requested_urls)

        #Submit test request
        id_url_dict = wpt_batch_lib.SubmitBatch(requested_urls, self._wpt_params, self._wpt_server)
        self._logger.debug("SubmitBatch returned\n%s" % id_url_dict)

        #Check all URLs were submitted to test server
        submitted_urls = set(id_url_dict.values())
        self._logger.debug("Submitted URLs\n%(submitted_urls)s" % {"submitted_urls": submitted_urls})
        for url in requested_urls:
            if url not in submitted_urls:
                self._logger.error('URL submission failed: %(url)s' % {"url": url})

        #Wait until all tests are completed
        pending_test_ids = id_url_dict.keys()
        results = []
        while pending_test_ids:
            id_status_dict = wpt_batch_lib.CheckBatchStatus(pending_test_ids, server_url=self._wpt_server)

            #Get list of completed tests
            completed_test_ids = []
            for test_id, test_status in id_status_dict.iteritems():
                # We could get 4 different status codes with different meanings as
                # as follows:
                # 1XX: Test in progress
                # 200: Test complete
                # 4XX: Test request not found
                if int(test_status) >= 400:
                    self._logger.error('Tests failed with status %(test_id)s: %(test_status)s' % {'test_id':test_id,'test_status':test_status})
                    pending_test_ids.remove(test_id)
                    continue

                if int(test_status) == 200:
                    self._logger.debug("Test %(test_id)s completed, removing from list of pending tests" % {'test_id': test_id})
                    pending_test_ids.remove(test_id)
                    completed_test_ids.append(test_id)

                else:
                    self._logger.debug("Test %(test_id)s has not completed, status=%(test_status)s" % {'test_id':test_id,'test_status':test_status})

            #Get results for completed tests
            test_results = wpt_batch_lib.GetXMLResult(completed_test_ids, server_url=self._wpt_server)

            #Check we got one result for each of the completed tests
            result_test_ids = set(test_results.keys())
            for test_id in completed_test_ids:
                if test_id not in result_test_ids:
                    self._logger.error('The XML failed to retrieve: %(test_id)s' % {'test_id': test_id})

            for test_id, dom in test_results.iteritems():
                self._logger.debug("Test id %(test_id)s result:\n%(xml)s" % {'test_id': test_id, 'xml': dom.toxml()})
                test_result = self._get_test_results(dom.toxml())
                results.append(test_result)

            if pending_test_ids:
                time.sleep(5)

        return results

    def test_script_and_wait(self, script):
        """
        Submits a test request with a script and waits until results are returned

        @param script: script
        @type script: str
        @return: Results as generated by WebPagetest
            For each item in the response, URL is the key and a dom object is the value
        @rtype: dict
        """
        self._logger.debug("Script to execute\n%(script)s" % {"script": script})
        #Create a new instance of _wpt_params to not change the original parameters
        test_params = dict(self._wpt_params)
        try:
            self._wpt_params['script'] = script
            result = self.test_urls_and_wait([""])
        finally:
            #Restore WPT params
            self._wpt_params = dict(test_params)

        return result

    def test_url_and_wait(self, url):
        """
        Submits a test request for a URL and waits until results are returned

        @param url: URLs to test
        @type url: str
        @return: Results as generated by WebPagetest
            For each item in the response, URL is the key and a dom object is the value
        @rtype: dict
        """
        self._logger.debug("URL to test\n%(url)s" % {"url": url})
        return self.test_urls_and_wait([url])

    def _get_test_results(self, xml_text):
        """
        Extracts global test metrics from DOM object returned by WPT and returns them as a dictionary

        @param xml_text: XML returned by WPT server
        @type xml_text: str
        @return: Returns a dictionary with the metrics extracted from XML
        @rtype: dict
        """
        root = elementTree.fromstring(xml_text)

        result = dict()
        #Get data node
        result[SUMMARY] = root.find("./data/summary").text

        #Number of test runs
        runs = int(root.find("./data/runs").text)
        result[RUNS] = runs

        #Get fist view and last view aggregated metrics
        #It's possible that RepeatView doesn't exist depending on how test was ran
        for view in [FIRST_VIEW, REPEAT_VIEW]:
            #Get aggegated metrics
            for metric in [AVERAGE, STD_DEV, MEDIAN]:
                view_node = root.find("./data/%(metric)s/%(view)s" % {'metric': metric, 'view': view})
                #Check there is an element "view", otherwise skip
                if view_node is not None:
                    result.setdefault(view, {}).setdefault(LOAD_TIME, {})
                    result[view][LOAD_TIME][metric] = int(view_node.find("./%(LOAD_TIME)s" % {'LOAD_TIME': LOAD_TIME}).text)

                    result.setdefault(view, {}).setdefault(TTFB, {})
                    result[view][TTFB][metric] = int(view_node.find("./%(TTFB)s" % {'TTFB': TTFB}).text)

                    result.setdefault(view, {}).setdefault(REQUESTS, {})
                    result[view][REQUESTS][metric] = int(view_node.find("./%(REQUESTS)s" % {'REQUESTS': REQUESTS}).text)

                    result.setdefault(view, {}).setdefault(REQUESTS_DOC, {})
                    result[view][REQUESTS_DOC][metric] = int(view_node.find("./%(REQUESTS_DOC)s" % {'REQUESTS_DOC': REQUESTS_DOC}).text)

                    result.setdefault(view, {}).setdefault(FULLY_LOADED, {})
                    result[view][FULLY_LOADED][metric] = int(view_node.find("./%(FULLY_LOADED)s" % {'FULLY_LOADED': FULLY_LOADED}).text)

            #If element "view" doesn't exist, skip getting test runs for that "view"
            if view in result:
                #Get results for each run
                run_nodes = root.findall("./data/run")
                for i in range(0, runs):
                    #Create missing keys or there will be an exception when assigning values
                    result[view].setdefault(RUNS, {}).setdefault(i, {})

                    data_node = run_nodes[i].find("./%(view)s" % {'view': view})
                    #If there is no 'run'.'view' element, it means that test run failed for that view
                    if data_node is not None:
                        result[view][RUNS][i][HEADERS_DATA] = data_node.find('./rawData/headers').text
                        result[view][RUNS][i][PAGE_DATA] = data_node.find('./rawData/pageData').text
                        result[view][RUNS][i][REQUESTS_DATA] = data_node.find('./rawData/requestsData').text
                        result[view][RUNS][i][UTILIZATION_DATA] = data_node.find('./rawData/utilization').text
                        result[view][RUNS][i][PAGE_SPEED_DATA] = data_node.find('./rawData/PageSpeedData').text
                    else:
                        result[view][RUNS][i][HEADERS_DATA] = ''
                        result[view][RUNS][i][PAGE_DATA] = ''
                        result[view][RUNS][i][REQUESTS_DATA] = ''
                        result[view][RUNS][i][UTILIZATION_DATA] = ''
                        result[view][RUNS][i][PAGE_SPEED_DATA] = ''

        self._logger.debug('Result:\n%(result)s' % {'result': result})
        return result