"""
Copyright 2018 BlazeMeter Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import ast
import json
import math
import re
import string
from collections import OrderedDict

import astunparse

from bzt import TaurusConfigError, TaurusInternalException
from bzt.engine import Scenario
from bzt.requests_model import HTTPRequest
from bzt.six import parse, string_types, iteritems, text_type, etree
from bzt.utils import PythonGenerator, dehumanize_time, ensure_is_dict
from .jmeter_functions import Base64DecodeFunction, UrlEncodeFunction, UuidFunction
from .jmeter_functions import TimeFunction, RandomFunction, RandomStringFunction, Base64EncodeFunction


def normalize_class_name(text):
    allowed_chars = "%s%s%s" % (string.digits, string.ascii_letters, '_')
    split_separator = re.split(r'[\-_]', text)
    return ''.join([capitalize_class_name(part, allowed_chars) for part in split_separator])


def capitalize_class_name(text, allowed_chars):
    return filter_string(text, allowed_chars).capitalize()


def filter_string(text, allowed_chars):
    return ''.join(c for c in text if c in allowed_chars)


def normalize_method_name(text):
    allowed_chars = "%s%s%s" % (string.digits, string.ascii_letters, '- ')
    return filter_string(text, allowed_chars).replace(' ', '_').replace('-', '_')


def create_class_name(label):
    return 'TestAPI' if label.startswith('autogenerated') else 'Test%s' % normalize_class_name(label)


def create_method_name(label):
    return 'test_requests' if label.startswith('autogenerated') else normalize_method_name(label)


class JMeterExprCompiler(object):
    def __init__(self, parent_log):
        self.log = parent_log.getChild(self.__class__.__name__)

    def gen_var_accessor(self, varname, ctx=None):
        if ctx is None:
            ctx = ast.Load()

        return ast.Subscript(
            value=ast.Name(id="self.vars", ctx=ast.Load()),
            slice=ast.Index(value=ast.Str(s=varname)),
            ctx=ctx
        )

    def gen_expr(self, value):
        if isinstance(value, bool):
            return ast.Name(id="True" if value else "False", ctx=ast.Load())
        elif isinstance(value, (int, float)):
            return ast.Num(n=value)
        elif isinstance(value, string_types):
            # if is has interpolation - break it into either a `"".format(args)` form or a Name node
            # otherwise - it's a string literal
            parts = re.split(r'(\$\{.*?\})', value)
            format_args = []
            for item in parts:
                if item:
                    if item.startswith("${") and item.endswith("}"):
                        value = value.replace(item, "{}")
                        compiled = self.translate_jmeter_expr(item[2:-1])
                        format_args.append(compiled)
            if format_args:
                if len(format_args) == 1 and value == "{}":
                    result = format_args[0]
                else:
                    result = ast.Call(
                        func=ast.Attribute(
                            value=ast.Str(s=value),
                            attr='format',
                            ctx=ast.Load(),
                        ),
                        args=format_args,
                        keywords=[],
                        starargs=None,
                        kwargs=None
                    )
            else:
                result = ast.Str(s=value)
            return result
        elif isinstance(value, type(None)):
            return ast.Name(id="None", ctx=ast.Load())
        elif isinstance(value, dict):
            items = sorted(list(iteritems(value)))
            return ast.Dict(keys=[self.gen_expr(k) for k, _ in items],
                            values=[self.gen_expr(v) for _, v in items])
        elif isinstance(value, list):
            return ast.List(elts=[self.gen_expr(val) for val in value], ctx=ast.Load())
        elif isinstance(value, tuple):
            return ast.Tuple(elts=[self.gen_expr(val) for val in value], ctx=ast.Load())
        elif isinstance(value, ast.AST):
            return value
        else:
            return value

    def translate_jmeter_expr(self, expr):
        """
        Translates JMeter expression into Apiritif-based Python expression.
        :type expr: str
        :return:
        """
        self.log.debug("Attempting to translate JMeter expression %r", expr)
        functions = {
            '__time': TimeFunction,
            '__Random': RandomFunction,
            '__RandomString': RandomStringFunction,
            '__base64Encode': Base64EncodeFunction,
            '__base64Decode': Base64DecodeFunction,
            '__urlencode': UrlEncodeFunction,
            '__UUID': UuidFunction,
        }
        regexp = r"(\w+)\((.*?)\)"
        args_re = r'(?<!\\),'
        match = re.match(regexp, expr)
        if match is None:  # doesn't look like JMeter func, translate as a var
            return self.gen_var_accessor(expr)

        varname, arguments = match.groups()

        if arguments is None:  # plain variable
            result = self.gen_var_accessor(varname)
        else:  # function call
            if not arguments:
                args = []
            else:
                # parse arguments: split by ',' but not '\,'
                args = [arg.strip() for arg in re.split(args_re, arguments)]

            if varname not in functions:  # unknown function
                return ast.Name(id=varname, ctx=ast.Load())

            self.log.debug("Translating function %s with arguments %s", varname, arguments)
            func = functions[varname](self)
            result = func.to_python(args)
            if result is None:
                result = ast.Name(id=varname, ctx=ast.Load())
        self.log.debug("Compile: %r -> %r", expr, result)
        return result


class SeleniumScriptBuilder(PythonGenerator):
    """
    :type window_size: tuple[int,int]
    """

    IMPORTS_SELENIUM = """import unittest
import os
import re
from time import sleep, time
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import NoAlertPresentException
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support import expected_conditions as econd
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.keys import Keys

import apiritif
"""
    IMPORTS_APPIUM = """import unittest
import os
import re
from time import sleep, time
from appium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import NoAlertPresentException
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support import expected_conditions as econd
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.keys import Keys

import apiritif
"""

    TAGS = ("byName", "byID", "byCSS", "byXPath", "byLinkText")

    def __init__(self, scenario, parent_logger, wdlog, utils_file, ignore_unknown_actions=False, generate_markers=None):
        super(SeleniumScriptBuilder, self).__init__(scenario, parent_logger)
        self.label = ''
        self.webdriver_address = None
        self.capabilities_from_outside = {}
        self.window_size = None
        self.wdlog = wdlog
        self.appium = False
        self.utils_file = utils_file
        self.ignore_unknown_actions = ignore_unknown_actions
        self.generate_markers = generate_markers

    def gen_asserts(self, config, indent=None):
        test_method = []
        if "assert" in config:
            test_method.append(self.gen_statement("body = self.driver.page_source", indent=indent))
            for assert_config in config.get("assert"):
                for elm in self.gen_assertion(assert_config, indent=indent):
                    test_method.append(elm)
        return test_method

    def gen_think_time(self, think_time, indent=None):
        test_method = []
        if think_time is not None:
            delay = dehumanize_time(think_time)
            if delay > 0:
                test_method.append(self.gen_statement("sleep(%s)" % dehumanize_time(think_time), indent=indent))
                test_method.append(self.gen_new_line())
        return test_method

    def gen_request(self, req, indent=None):
        default_address = self.scenario.get("default-address")
        transaction_contents = []
        if req.url is not None:
            parsed_url = parse.urlparse(req.url)
            if default_address and not parsed_url.netloc:
                url = default_address + req.url
            else:
                url = req.url
            transaction_contents.append(
                self.gen_statement("self.driver.get(self.template(%r))" % url, indent=indent))
            transaction_contents.append(self.gen_new_line())
        return transaction_contents

    def build_source_code(self):
        self.log.debug("Generating Test Case test methods")

        test_class = self.gen_class_definition("TestRequests", ["unittest.TestCase"])
        test_class.append(self.gen_setup_method())
        test_class.append(self.gen_teardown_method())

        requests = self.scenario.get_requests(require_url=False)
        test_method = self.gen_test_method('test_requests')
        self.gen_setup(test_method)

        for i, req in enumerate(requests, 1):
            self._fill_test_method(req, test_method)
            if i != len(requests):
                test_method.append(self.gen_new_line())

        test_class.append(test_method)

        self.root.append(self.gen_statement("# coding=utf-8", indent=0))
        self.root.append(self.add_imports())
        self.root.append(test_class)
        self.root.append(self.add_utilities())

    def _fill_test_method(self, req, test_method):
        if req.label:
            label = req.label
        elif req.url:
            label = req.url
        else:
            raise TaurusConfigError("You must specify at least 'url' or 'label' for each requests item")

        if self.generate_markers:
            test_method.append(self.gen_statement("try:", indent=self.INDENT_STEP * 2))
            indent = 3
            marker = "self.driver.execute_script('/* FLOW_MARKER test-case-start */', " \
                     "{'testCaseName': %r, 'testSuiteName': %r})" % (label, self.label)
            test_method.append(self.gen_statement(marker, indent=self.INDENT_STEP * indent))
            test_method.append(self.gen_new_line())
        else:
            indent = 2

        test_method.append(self.gen_statement('with apiritif.transaction_logged(self.template(%r)):' % label,
                                              indent=self.INDENT_STEP * indent))
        transaction_contents = []

        transaction_contents.extend(self.gen_request(req, indent=self.INDENT_STEP * (indent + 1)))
        if req.url is not None and req.timeout is not None:
            test_method.append(self.gen_impl_wait(req.timeout, indent=self.INDENT_STEP * (indent + 1)))

        action_append = False
        for action_config in req.config.get("actions", []):
            action = self.gen_action(action_config, indent=self.INDENT_STEP * (indent + 1))
            if action:
                transaction_contents.extend(action)
                action_append = True
        if action_append:
            transaction_contents.append(self.gen_new_line())

        transaction_contents.extend(self.gen_asserts(req.config, indent=self.INDENT_STEP * (indent + 1)))

        if transaction_contents:
            test_method.extend(transaction_contents)
        else:
            test_method.append(self.gen_statement('pass', indent=self.INDENT_STEP * (indent + 1)))
        test_method.append(self.gen_new_line())

        test_method.extend(self.gen_think_time(req.priority_option('think-time'), indent=self.INDENT_STEP * indent))

        if self.generate_markers:
            marker = "self.driver.execute_script('/* FLOW_MARKER test-case-stop */', " \
                     "{'status': %s, 'message': %s})"

            test_method.append(self.gen_statement("except AssertionError as exc:", indent=self.INDENT_STEP * 2))
            test_method.append(self.gen_statement(marker % (repr('failed'), 'str(exc)'), indent=self.INDENT_STEP * 3))
            test_method.append(self.gen_statement("raise", indent=self.INDENT_STEP * 3))

            test_method.append(self.gen_statement("except BaseException as exc:", indent=self.INDENT_STEP * 2))
            test_method.append(self.gen_statement(marker % (repr('broken'), 'str(exc)'), indent=self.INDENT_STEP * 3))
            test_method.append(self.gen_statement("raise", indent=self.INDENT_STEP * 3))

            test_method.append(self.gen_statement("else:", indent=self.INDENT_STEP * 2))
            test_method.append(self.gen_statement(marker % (repr('success'), repr('')), indent=self.INDENT_STEP * 3))

    def add_imports(self):
        imports = super(SeleniumScriptBuilder, self).add_imports()
        if self.appium:
            imports.text = self.IMPORTS_APPIUM
        else:
            imports.text = self.IMPORTS_SELENIUM
        return imports

    def add_utilities(self):
        with open(self.utils_file) as fds:
            utilities_source_lines = fds.read()
        utils = etree.Element("utilities")
        utils.text = "\n" + utilities_source_lines
        return utils

    def gen_global_vars(self):
        variables = self.scenario.get("variables")
        stmts = [
            "self.vars = {}",
            "self.template = Template(self.vars)"
        ]

        for key in sorted(variables.keys()):
            stmts.append("self.vars['%s'] = %r" % (key, variables[key]))
        stmts.append("")
        return [self.gen_statement(stmt) for stmt in stmts]

    def _add_url_request(self, default_address, req, test_method):
        parsed_url = parse.urlparse(req.url)
        if default_address is not None and not parsed_url.netloc:
            url = default_address + req.url
        else:
            url = req.url
        if req.timeout is not None:
            test_method.append(self.gen_impl_wait(req.timeout))
        test_method.append(self.gen_statement("self.driver.get(self.template(%r))" % url))

    def gen_setup(self, test_method):
        timeout = self.scenario.get("timeout", "30s")
        scenario_timeout = dehumanize_time(timeout)
        test_method.append(self.gen_impl_wait(scenario_timeout))
        test_method.append(self.gen_new_line())

    def _check_platform(self):
        inherited_capabilities = [{x: y} for x, y in iteritems(self.capabilities_from_outside)]
        mobile_browsers = ["Chrome", "Safari"]
        mobile_platforms = ["Android", "iOS"]
        remote_executor = self.scenario.get("remote", self.webdriver_address)

        browser = self.scenario.get("browser", None)

        browser_platform = None
        if browser:
            browser_split = browser.split("-")
            browser = browser_split[0]
            browsers = ["Firefox", "Chrome", "Ie", "Opera", "Remote"]
            if browser not in browsers:
                raise TaurusConfigError("Unsupported browser name: %s" % browser)
            if len(browser_split) > 1:
                browser_platform = browser_split[1]

        if remote_executor:
            if browser:
                self.log.warning("Forcing browser to Remote, because of remote webdriver address")
            inherited_capabilities.append({"browser": browser})
            browser = "Remote"
            if self.generate_markers is None:  # if not set by user - set to true
                self.generate_markers = True
        elif browser in mobile_browsers and browser_platform in mobile_platforms:
            self.appium = True
            inherited_capabilities.append({"platform": browser_platform})
            inherited_capabilities.append({"browser": browser})
            browser = "Remote"  # Force to use remote web driver
        elif not browser:
            browser = "Firefox"

        return browser, inherited_capabilities, remote_executor

    def gen_setup_method(self):
        self.log.debug("Generating setUp test method")
        browser, inherited_capabilities, remote_executor = self._check_platform()

        headless = self.scenario.get("headless", False)
        if headless:
            self.log.info("Headless mode works only with Selenium 3.8.0+, be sure to have it installed")

        setup_method_def = self.gen_method_definition("setUp", ["self"])
        setup_method_def.extend(self.gen_global_vars())

        if browser == 'Firefox':
            setup_method_def.append(self.gen_statement("options = webdriver.FirefoxOptions()"))
            if headless:
                setup_method_def.append(self.gen_statement("options.set_headless()"))
            setup_method_def.append(self.gen_statement("profile = webdriver.FirefoxProfile()"))
            statement = "profile.set_preference('webdriver.log.file', %s)" % repr(self.wdlog)
            log_set = self.gen_statement(statement)
            setup_method_def.append(log_set)
            tmpl = "self.driver = webdriver.Firefox(profile, firefox_options=options)"
            setup_method_def.append(self.gen_statement(tmpl))
        elif browser == 'Chrome':
            setup_method_def.append(self.gen_statement("options = webdriver.ChromeOptions()"))
            if headless:
                setup_method_def.append(self.gen_statement("options.set_headless()"))
            statement = "self.driver = webdriver.Chrome(service_log_path=%s, chrome_options=options)"
            setup_method_def.append(self.gen_statement(statement % repr(self.wdlog)))
        elif browser == 'Remote':
            setup_method_def.append(self._gen_remote_driver(inherited_capabilities, remote_executor))
        else:
            if headless:
                self.log.warning("Browser %r doesn't support headless mode")
            setup_method_def.append(self.gen_statement("self.driver = webdriver.%s()" % browser))

        scenario_timeout = self.scenario.get("timeout", "30s")
        setup_method_def.append(self.gen_impl_wait(scenario_timeout))

        setup_method_def.append(self.gen_statement("self.wnd_mng = WindowManager(self.driver)"))
        setup_method_def.append(self.gen_statement("self.frm_mng = FrameManager(self.driver)"))

        if self.window_size:  # FIXME: unused in fact
            statement = self.gen_statement("self.driver.set_window_position(0, 0)")
            setup_method_def.append(statement)

            args = (self.window_size[0], self.window_size[1])
            statement = self.gen_statement("self.driver.set_window_size(%s, %s)" % args)
            setup_method_def.append(statement)
        else:
            pass  # TODO: setup_method_def.append(self.gen_statement("self.driver.maximize_window()"))
            # but maximize_window does not work on virtual displays. Bummer

        setup_method_def.append(self.gen_new_line())
        return setup_method_def

    def _gen_remote_driver(self, inherited_caps, remote_executor):
        desired_caps = {}
        remote_caps = self.scenario.get("capabilities", [])
        if not isinstance(remote_caps, list):
            remote_caps = [remote_caps]
        capabilities = remote_caps + inherited_caps

        for capability in capabilities:
            for cap_key in capability.keys():
                if cap_key == "browser":
                    desired_caps["browserName"] = capability[cap_key]
                elif cap_key == "version":
                    desired_caps["version"] = str(capability[cap_key])
                elif cap_key == "selenium":
                    desired_caps["seleniumVersion"] = str(capability[cap_key])
                elif cap_key == "javascript":
                    desired_caps["javascriptEnabled"] = capability[cap_key]
                elif cap_key == "platform":
                    desired_caps["platformName"] = str(capability[cap_key])
                elif cap_key == "os_version":
                    desired_caps["platformVersion"] = str(capability[cap_key])
                elif cap_key == "device":
                    desired_caps["deviceName"] = str(capability[cap_key])
                else:
                    desired_caps[cap_key] = capability[cap_key]

        tpl = "self.driver = webdriver.Remote(command_executor={command_executor}, desired_capabilities={des_caps})"

        if not remote_executor:
            if self.appium:
                remote_executor = "http://localhost:4723/wd/hub"
            else:
                remote_executor = "http://localhost:4444/wd/hub"

        cmd = tpl.format(command_executor=repr(remote_executor), des_caps=json.dumps(desired_caps, sort_keys=True))

        return self.gen_statement(cmd)

    def gen_impl_wait(self, timeout, indent=None):
        return self.gen_statement("self.driver.implicitly_wait(%s)" % dehumanize_time(timeout), indent=indent)

    def gen_test_method(self, name):
        self.log.debug("Generating test method %s", name)
        test_method = self.gen_method_definition(name, ["self"])
        return test_method

    def gen_teardown_method(self):
        self.log.debug("Generating tearDown test method")
        tear_down_method_def = self.gen_method_definition("tearDown", ["self"])
        tear_down_method_def.append(self.gen_statement("self.driver.quit()"))
        tear_down_method_def.append(self.gen_new_line())
        return tear_down_method_def

    def gen_assertion(self, assertion_config, indent=None):
        self.log.debug("Generating assertion, config: %s", assertion_config)
        assertion_elements = []

        if isinstance(assertion_config, string_types):
            assertion_config = {"contains": [assertion_config]}

        for val in assertion_config["contains"]:
            regexp = assertion_config.get("regexp", True)
            reverse = assertion_config.get("not", False)
            subject = assertion_config.get("subject", "body")
            if subject != "body":
                raise TaurusConfigError("Only 'body' subject supported ")

            assert_message = "'%s' " % val
            if not reverse:
                assert_message += 'not '
            assert_message += 'found in BODY'

            if regexp:
                assert_method = "self.assertEqual" if reverse else "self.assertNotEqual"
                assertion_elements.append(self.gen_statement("re_pattern = re.compile(r'%s')" % val, indent=indent))

                method = '%s(0, len(re.findall(re_pattern, body)), "Assertion: %s")'
                method %= assert_method, assert_message
                assertion_elements.append(self.gen_statement(method, indent=indent))
            else:
                assert_method = "self.assertNotIn" if reverse else "self.assertIn"
                method = '%s("%s", body, "Assertion: %s")'
                method %= assert_method, val, assert_message
                assertion_elements.append(self.gen_statement(method, indent=indent))
        return assertion_elements

    def gen_action(self, action_config, indent=None):
        action = self._parse_action(action_config)
        if action:
            atype, tag, param, selector = action
        else:
            return

        action_elements = []

        bys = {
            'byxpath': "XPATH",
            'bycss': "CSS_SELECTOR",
            'byname': "NAME",
            'byid': "ID",
            'bylinktext': "LINK_TEXT"
        }
        action_chains = {
            'doubleclick': "double_click",
            'mousedown': "click_and_hold",
            'mouseup': "release",
            'mousemove': "move_to_element"
        }

        if tag == "window":
            if atype == "switch":
                cmd = 'self.wnd_mng.switch(self.template(%r))' % selector
                action_elements.append(self.gen_statement(cmd, indent=indent))
            elif atype == "open":
                script = "window.open('%s');" % selector
                cmd = 'self.driver.execute_script(self.template(%r))' % script
                action_elements.append(self.gen_statement(cmd, indent=indent))
            elif atype == "close":
                if selector:
                    cmd = 'self.wnd_mng.close(self.template(%r))' % selector
                else:
                    cmd = 'self.wnd_mng.close()'
                action_elements.append(self.gen_statement(cmd, indent=indent))

        elif atype == "switchframe":
            if tag == "byidx":
                cmd = "self.frm_mng.switch(%r)" % int(selector)
            elif selector.startswith("index=") or selector in ["relative=top", "relative=parent"]:
                cmd = "self.frm_mng.switch(%r)" % selector
            else:
                frame = "self.driver.find_element(By.%s, self.template(%r))" % (bys[tag], selector)
                cmd = "self.frm_mng.switch(%s)" % frame

            action_elements.append(self.gen_statement(cmd, indent=indent))

        elif atype in action_chains:
            tpl = "self.driver.find_element(By.%s, self.template(%r))"
            action = action_chains[atype]
            action_elements.append(self.gen_statement(
                "ActionChains(self.driver).%s(%s).perform()" % (action, (tpl % (bys[tag], selector))),
                indent=indent))
        elif atype == 'drag':
            drop_action = self._parse_action(param)
            if drop_action and drop_action[0] == "element" and not drop_action[2]:
                drop_tag, drop_selector = (drop_action[1], drop_action[3])
                tpl = "self.driver.find_element(By.%s, self.template(%r))"
                action = "drag_and_drop"
                drag_element = tpl % (bys[tag], selector)
                drop_element = tpl % (bys[drop_tag], drop_selector)
                action_elements.append(self.gen_statement(
                    "ActionChains(self.driver).%s(%s, %s).perform()" % (action, drag_element, drop_element),
                    indent=indent))
        elif atype == 'select':
            tpl = "self.driver.find_element(By.%s, self.template(%r))"
            action = "select_by_visible_text(self.template(%r))" % param
            action_elements.append(self.gen_statement("Select(%s).%s" % (tpl % (bys[tag], selector), action),
                                                      indent=indent))
        elif atype.startswith('assert') or atype.startswith('store'):
            if tag == 'title':
                if atype.startswith('assert'):
                    action_elements.append(
                        self.gen_statement("self.assertEqual(self.driver.title, self.template(%r))"
                                           % selector, indent=indent))
                else:
                    action_elements.append(self.gen_statement(
                        "self.vars['%s'] = self.template(self.driver.title)" % param.strip(), indent=indent
                    ))
            elif atype == 'store' and tag == 'string':
                action_elements.append(self.gen_statement(
                    "self.vars['%s'] = self.template('%s')" % (param.strip(), selector.strip()), indent=indent
                ))
            else:
                tpl = "self.driver.find_element(By.%s, self.template(%r)).%s"
                if atype in ['asserttext', 'storetext']:
                    action = "get_attribute('innerText')"
                elif atype in ['assertvalue', 'storevalue']:
                    action = "get_attribute('value')"
                if atype.startswith('assert'):
                    action_elements.append(
                        self.gen_statement("self.assertEqual(self.template(%s).strip(), self.template(%r).strip())" %
                                           (tpl % (bys[tag], selector, action), param),
                                           indent=indent))
                elif atype.startswith('store'):
                    action_elements.append(
                        self.gen_statement("self.vars['%s'] = self.template(%s)" %
                                           (param.strip(), tpl % (bys[tag], selector, action)),
                                           indent=indent))
        elif atype in ('click', 'type', 'keys', 'submit'):
            tpl = "self.driver.find_element(By.%s, self.template(%r)).%s"
            action = None
            if atype == 'click':
                action = "click()"
            elif atype == 'submit':
                action = "submit()"
            elif atype in ['keys', 'type']:
                if atype == 'type':
                    action_elements.append(self.gen_statement(
                        tpl % (bys[tag], selector, "clear()"), indent=indent))
                action = "send_keys(self.template(%r))" % str(param)
                if isinstance(param, (string_types, text_type)) and param.startswith("KEY_"):
                    action = "send_keys(Keys.%s)" % param.split("KEY_")[1]

            action_elements.append(self.gen_statement(tpl % (bys[tag], selector, action), indent=indent))

        elif atype == "script" and tag == "eval":
            cmd = 'self.driver.execute_script(self.template(%r))' % selector
            action_elements.append(self.gen_statement(cmd, indent=indent))
        elif atype == "rawcode":
            lines = param.split('\n')
            for line in lines:
                action_elements.append(self.gen_statement(line, indent=indent))
        elif atype == 'go':
            if selector and not param:
                cmd = "self.driver.get(self.template(%r))" % selector.strip()
                action_elements.append(self.gen_statement(cmd, indent=indent))
        elif atype == "editcontent":
            element = "self.driver.find_element(By.%s, %r)" % (bys[tag], selector)
            editable_error = "The element (By.%s, %r) " \
                             "is not contenteditable element" % (bys[tag], selector)
            editable_script_tpl = "arguments[0].innerHTML = %s;"
            editable_script_tpl_argument = "self.template.str_repr(self.template(%r))" % param.strip()
            editable_script = "%r %% %s" % \
                              (editable_script_tpl, editable_script_tpl_argument)
            action_elements.extend([
                self.gen_statement(
                    "if %s.get_attribute('contenteditable'):" % element,
                    indent=indent),
                self.gen_statement(
                    "self.driver.execute_script(",
                    indent=indent + self.INDENT_STEP),
                self.gen_statement(
                    "%s," % editable_script,
                    indent=indent + self.INDENT_STEP * 2),
                self.gen_statement(
                    element,
                    indent=indent + self.INDENT_STEP * 2),
                self.gen_statement(
                    ")",
                    indent=indent + self.INDENT_STEP),
                self.gen_statement(
                    "else:", indent=indent),
                self.gen_statement(
                    "raise NoSuchElementException(%r)" % editable_error,
                    indent=indent + self.INDENT_STEP)
            ])
        elif atype == 'echo' and tag == 'string':
            if len(selector) > 0 and not param:
                action_elements.append(
                    self.gen_statement("print(self.template(%r))" % selector.strip(), indent=indent))
        elif atype == 'wait':
            tpl = "WebDriverWait(self.driver, %s).until(econd.%s_of_element_located((By.%s, self.template(%r))), %r)"
            mode = "visibility" if param == 'visible' else 'presence'
            exc = TaurusConfigError("wait action requires timeout in scenario: \n%s" % self.scenario)
            timeout = dehumanize_time(self.scenario.get("timeout", exc))
            errmsg = "Element %r failed to appear within %ss" % (selector, timeout)
            action_elements.append(self.gen_statement(tpl % (timeout, mode, bys[tag], selector, errmsg), indent=indent))
        elif atype == 'pause' and tag == 'for':
            tpl = "sleep(%g)"
            action_elements.append(self.gen_statement(tpl % (dehumanize_time(selector),), indent=indent))
        elif atype == 'clear' and tag == 'cookies':
            action_elements.append(self.gen_statement("self.driver.delete_all_cookies()", indent=indent))
        elif atype == 'screenshot':
            if selector:
                filename = selector
                action_elements.append(self.gen_statement('self.driver.save_screenshot(self.template(%r))' % filename,
                                                          indent=indent))
            else:
                filename = "filename = os.path.join(os.getenv('TAURUS_ARTIFACTS_DIR'), " \
                           "'screenshot-%d.png' % (time() * 1000))"
                action_elements.append(self.gen_statement(filename, indent=indent))
                action_elements.append(self.gen_statement('self.driver.save_screenshot(filename)', indent=indent))

        if not action_elements:
            raise TaurusInternalException("Could not build code for action: %s" % action_config)

        return action_elements

    def _parse_action(self, action_config):
        if isinstance(action_config, string_types):
            name = action_config
            param = None
        elif isinstance(action_config, dict):
            name, param = next(iteritems(action_config))
        else:
            raise TaurusConfigError("Unsupported value for action: %s" % action_config)

        actions = "|".join(['click', 'doubleClick', 'mouseDown', 'mouseUp', 'mouseMove', 'select', 'wait', 'keys',
                            'pause', 'clear', 'assert', 'assertText', 'assertValue', 'submit', 'close', 'script',
                            'editcontent', 'switch', 'switchFrame', 'go', 'echo', 'type', 'element', 'drag',
                            'storeText', 'storeValue', 'store', 'open', 'screenshot', 'rawCode'
                            ])

        tag = "|".join(self.TAGS) + "|For|Cookies|Title|Window|Eval|ByIdx|String"
        expr = re.compile("^(%s)(%s)?(\(([\S\s]*)\))?$" % (actions, tag), re.IGNORECASE)
        res = expr.match(name)
        if not res:
            msg = "Unsupported action: %s" % name
            if self.ignore_unknown_actions:
                self.log.warning(msg)
                return
            else:
                raise TaurusConfigError(msg)

        atype = res.group(1).lower()
        tag = res.group(2).lower() if res.group(2) else ""
        selector = res.group(4)

        # hello, reviewer!
        if selector:
            if selector.startswith('"') and selector.endswith('"'):
                selector = selector[1:-1]
            elif selector.startswith("'") and selector.endswith("'"):
                selector = selector[1:-1]
        else:
            selector = ""
        return atype, tag, param, selector


class ApiritifScriptGenerator(PythonGenerator):
    # Python AST docs: https://greentreesnakes.readthedocs.io/en/latest/

    ACCESS_TARGET = 'target'
    ACCESS_PLAIN = 'plain'

    def __init__(self, engine, scenario, label, parent_log):
        super(ApiritifScriptGenerator, self).__init__(scenario, parent_log)
        self.scenario = scenario
        self.engine = engine
        self.data_sources = list(scenario.get_data_sources())
        self.label = label
        self.log = parent_log.getChild(self.__class__.__name__)
        self.tree = None
        self.verbose = False
        self.expr_compiler = JMeterExprCompiler(parent_log=self.log)
        self.stored_vars = []

    @staticmethod
    def _gen_empty_line_stmt():
        return ast.Expr(value=ast.Name(id=""))  # hacky, but works

    def _gen_module(self):
        stmts = [self._gen_imports()]

        if self.verbose:
            stmts.extend(self._gen_logging())

        stmts.extend(self._gen_data_source_readers())
        stmts.extend(self._gen_module_setup())
        stmts.append(self._gen_classdef())
        return ast.Module(body=stmts)

    def _gen_imports(self):
        return [
            ast.Import(names=[ast.alias(name='logging', asname=None)]),
            ast.Import(names=[ast.alias(name='random', asname=None)]),
            ast.Import(names=[ast.alias(name='string', asname=None)]),
            ast.Import(names=[ast.alias(name='sys', asname=None)]),
            ast.Import(names=[ast.alias(name='time', asname=None)]),
            ast.Import(names=[ast.alias(name='unittest', asname=None)]),
            self._gen_empty_line_stmt(),
            ast.Import(names=[ast.alias(name='apiritif', asname=None)]),    # or "from apiritif import http, utils"?
            self._gen_empty_line_stmt()]

    def _gen_module_setup(self):
        target_init = self._gen_target()

        data_sources = [self._gen_default_vars()]
        for idx in range(len(self.data_sources)):
            reader = "reader_%s" % (idx + 1)
            func = ast.Attribute(value=ast.Name(id=reader), attr="read_vars")
            read_vars = ast.Call(func=func, args=[], starargs=None, kwargs=None, keywords=[])
            data_sources.append(ast.Expr(read_vars))

        for idx in range(len(self.data_sources)):
            reader_get_vars = ast.Attribute(value=ast.Name(id="reader_%s" % (idx + 1)), attr="get_vars")
            get_vars = ast.Call(func=reader_get_vars, args=[], starargs=None, kwargs=None,keywords=[])

            update = ast.Attribute(attr="update", value=ast.Name(id="vars"))

            extend_vars = ast.Call(func=update, args=[get_vars], starargs=None, kwargs=None, keywords=[])
            data_sources.append(ast.Expr(extend_vars))

        self.stored_vars = ['vars']
        if target_init:
            self.stored_vars.append('target')

        store_call = ast.Call(
                func=ast.Attribute(attr="put_into_thread_store", value=ast.Name(id="apiritif")),
                args=[ast.Name(id=var) for var in self.stored_vars],
                starargs=None,
                kwargs=None,
                keywords=[])

        store_block = [self._gen_empty_line_stmt(), ast.Expr(store_call)]

        setup = ast.FunctionDef(
            name="setup",
            args=[],
            body=target_init + data_sources + store_block,
            decorator_list=[])
        return [setup, self._gen_empty_line_stmt()]

    def _gen_data_source_readers(self):
        readers = []
        for idx, source in enumerate(self.data_sources, start=1):
            keywords = []

            if "fieldnames" in source:
                fieldnames = ast.keyword()
                fieldnames.arg = "fieldnames"
                str_names = source.get("fieldnames").split(",")
                fieldnames.value = ast.List(elts=[ast.Str(s=fname) for fname in str_names])
                keywords.append(fieldnames)

            if "loop" in source:
                loop = ast.keyword()
                loop.arg = "loop"
                loop.value = ast.Name(id=source.get("loop"))
                keywords.append(loop)

            if "quoted" in source:
                quoted = ast.keyword()
                quoted.arg = "quoted"
                quoted.value = ast.Name(id=source.get("quoted"))
                keywords.append(quoted)

            if "delimiter" in source:
                delimiter = ast.keyword()
                delimiter.arg = "delimiter"
                delimiter.value = ast.Str(s=source.get("delimiter"))
                keywords.append(delimiter)

            csv_file = self.engine.find_file(source["path"])
            reader = ast.Assign(
                targets=[ast.Name(id="reader_%s" % idx)],
                value=ast.Call(
                    func=ast.Name(id="apiritif.CSVReaderPerThread"),
                    args=[ast.Str(s=csv_file)],
                    starargs=None,
                    kwargs=None,
                    keywords=keywords))

            readers.append(reader)

        if readers:
            readers.append(self._gen_empty_line_stmt())

        return readers

    def _gen_classdef(self):
        class_body = [self._gen_class_setup()]
        class_body.extend(self._gen_test_methods())

        class_name = create_class_name(self.label)
        base = ast.Attribute(value=ast.Name(id='unittest', ctx=ast.Load()), attr='TestCase', ctx=ast.Load())
        return ast.ClassDef(
            name=class_name,
            bases=[base],
            body=class_body,
            keywords=[],
            starargs=None,
            kwargs=None,
            decorator_list=[])

    def _gen_class_setup(self):
        fields = ast.Tuple(elts=[ast.Name(id="self.%s" % var) for var in self.stored_vars])
        get_func = ast.Attribute(attr="get_from_thread_store", value=ast.Name(id="apiritif"))
        get_call = ast.Call(func=get_func, args=[], starargs=None, kwargs=None, keywords=[])

        get_expr = ast.Expr(ast.Assign(targets=[fields], value=get_call))
        args = ast.arguments(args=[ast.Name(id="self")], defaults=[], vararg=None, kwarg=None)

        return ast.FunctionDef(name="setUp", args=args, body=[get_expr], decorator_list=[])

    def _gen_test_methods(self):
        requests = self.scenario.get_requests()
        number_of_digits = int(math.log10(len(requests))) + 1
        for index, req in enumerate(requests, start=1):
            if not isinstance(req, HTTPRequest):
                msg = "Apiritif script generator doesn't support '%s' blocks, skipping"
                self.log.warning(msg, req.NAME)
                continue

            if req.label:
                label = req.label
            else:
                label = req.url

            label = create_method_name(label[:40])
            counter = str(index).zfill(number_of_digits)

            # 'test_01_get_posts'
            method_name = 'test_' + counter + '_' + label
            method = ast.FunctionDef(
                name=method_name,
                args=ast.arguments(
                    args=[ast.Name(id='self', ctx=ast.Param())],
                    defaults=[],
                    vararg=None,
                    kwonlyargs=[],
                    kw_defaults=[],
                    kwarg=None,
                    returns=None,
                ),
                body=self.gen_request_lines(req),
                decorator_list=[],
            )

            yield method

    def gen_expr(self, value):
        return self.expr_compiler.gen_expr(value)

    def _gen_target_setup(self, key, value):
        return ast.Expr(value=ast.Call(
            func=ast.Attribute(value=ast.Name(id='target', ctx=ast.Load()), attr=key, ctx=ast.Load()),
            args=[self.gen_expr(value)],
            keywords=[],
            starargs=None,
            kwargs=None
        ))

    def _access_method(self):
        keepalive = self.scenario.get("keepalive", None)
        default_address = self.scenario.get("default-address", None)
        store_cookie = self.scenario.get("store-cookie", None)

        if default_address is not None or keepalive or store_cookie:
            return ApiritifScriptGenerator.ACCESS_TARGET
        else:
            return ApiritifScriptGenerator.ACCESS_PLAIN

    def _gen_target(self):
        keepalive = self.scenario.get("keepalive", None)
        default_address = self.scenario.get("default-address", None)
        base_path = self.scenario.get("base-path", None)
        auto_assert_ok = self.scenario.get("auto-assert-ok", True)
        store_cookie = self.scenario.get("store-cookie", None)
        timeout = self.scenario.get("timeout", None)
        follow_redirects = self.scenario.get("follow-redirects", True)

        if keepalive is None:
            keepalive = True
        if store_cookie is None:
            store_cookie = True

        target = []
        if self._access_method() == ApiritifScriptGenerator.ACCESS_TARGET:
            http = ast.Attribute(value=ast.Name(id='apiritif', ctx=ast.Load()), attr='http', ctx=ast.Load())
            target = [
                ast.Assign(
                    targets=[
                        ast.Name(id="target", ctx=ast.Store()),
                    ],
                    value=ast.Call(
                        func=ast.Attribute(value=http, attr='target', ctx=ast.Load()),
                        args=[self.gen_expr(default_address)],
                        keywords=[],
                        starargs=None,
                        kwargs=None
                    ))]
            target.extend([
                self._gen_target_setup('keep_alive', keepalive),
                self._gen_target_setup('auto_assert_ok', auto_assert_ok),
                self._gen_target_setup('use_cookies', store_cookie),
                self._gen_target_setup('allow_redirects', follow_redirects),
            ])
            if base_path:
                target.append(self._gen_target_setup('base_path', base_path))
            if timeout is not None:
                target.append(self._gen_target_setup('timeout', dehumanize_time(timeout)))
            target.append(self._gen_empty_line_stmt())
        return target

    def _extract_named_args(self, req):
        named_args = OrderedDict()

        no_target = self._access_method() != ApiritifScriptGenerator.ACCESS_TARGET
        if req.timeout is not None:
            named_args['timeout'] = dehumanize_time(req.timeout)
        elif "timeout" in self.scenario and no_target:
            named_args['timeout'] = dehumanize_time(self.scenario.get("timeout"))

        follow_redirects = req.priority_option('follow-redirects', None)
        if follow_redirects is not None:
            named_args['allow_redirects'] = follow_redirects

        headers = {}
        headers.update(self.scenario.get("headers"))
        headers.update(req.headers)

        if headers:
            named_args['headers'] = self.gen_expr(headers)

        merged_headers = dict([(key.lower(), value) for key, value in iteritems(headers)])
        content_type = merged_headers.get("content-type")

        if content_type == 'application/json' and isinstance(req.body, (dict, list)):  # json request body
            named_args['json'] = self.gen_expr(req.body)
        elif req.method.lower() == "get" and isinstance(req.body, dict):  # request URL params (?a=b&c=d)
            named_args['params'] = self.gen_expr(req.body)
        elif isinstance(req.body, dict):  # form data
            named_args['data'] = self.gen_expr(list(iteritems(req.body)))
        elif isinstance(req.body, string_types):
            named_args['data'] = self.gen_expr(req.body)
        elif req.body:
            msg = "Cannot handle 'body' option of type %s: %s"
            raise TaurusConfigError(msg % (type(req.body), req.body))

        return named_args

    def gen_request_lines(self, req):
        apiritif_http = ast.Attribute(value=ast.Name(id='apiritif', ctx=ast.Load()),
                                      attr='http', ctx=ast.Load())
        target = ast.Name(id='self.target', ctx=ast.Load())
        requestor = target if self._access_method() == ApiritifScriptGenerator.ACCESS_TARGET else apiritif_http

        method = req.method.lower()
        think_time = dehumanize_time(req.priority_option('think-time', default=None))
        named_args = self._extract_named_args(req)

        if req.label:
            label = req.label
        else:
            label = req.url

        lines = []

        tran = ast.Attribute(value=ast.Name(id='apiritif', ctx=ast.Load()), attr="transaction", ctx=ast.Load())
        transaction = ast.With(
            context_expr=ast.Call(
                func=tran,
                args=[self.gen_expr(label)],
                keywords=[],
                starargs=None,
                kwargs=None
            ),
            optional_vars=None,
            body=[
                ast.Assign(
                    targets=[
                        ast.Name(id="response", ctx=ast.Store())
                    ],
                    value=ast.Call(
                        func=ast.Attribute(value=requestor, attr=method, ctx=ast.Load()),
                        args=[self.gen_expr(req.url)],
                        keywords=[ast.keyword(arg=name, value=self.gen_expr(value))
                                  for name, value in iteritems(named_args)],
                        starargs=None,
                        kwargs=None
                    ))])
        transaction.body.extend(self._gen_assertions(req))
        transaction.body.extend(self._gen_jsonpath_assertions(req))
        transaction.body.extend(self._gen_xpath_assertions(req))
        lines.append(transaction)

        lines.extend(self._gen_extractors(req))

        if think_time:
            lines.append(
                ast.Expr(
                    ast.Call(
                        func=ast.Attribute(
                            value=ast.Name(id="time", ctx=ast.Load()),
                            attr="sleep",
                            ctx=ast.Load()),
                        args=[self.gen_expr(think_time)],
                        keywords=[],
                        starargs=None,
                        kwargs=None)))

        return lines

    def _gen_default_vars(self):
        variables = self.scenario.get("variables")
        values = ast.Dict(
            keys=[self.expr_compiler.gen_expr(key) for key, _ in iteritems(variables)],
            values=[self.expr_compiler.gen_expr(value) for _, value in iteritems(variables)])

        return ast.Assign(targets=[ast.Name(id='vars', ctx=ast.Store())], value=values)

    def _gen_assertions(self, request):
        stmts = []
        assertions = request.config.get("assert", [])
        for idx, assertion in enumerate(assertions):
            assertion = ensure_is_dict(assertions, idx, "contains")
            if not isinstance(assertion['contains'], list):
                assertion['contains'] = [assertion['contains']]
            subject = assertion.get("subject", Scenario.FIELD_BODY)
            if subject in (Scenario.FIELD_BODY, Scenario.FIELD_HEADERS):
                for member in assertion["contains"]:
                    func_table = {
                        (Scenario.FIELD_BODY, False, False): "assert_in_body",
                        (Scenario.FIELD_BODY, False, True): "assert_not_in_body",
                        (Scenario.FIELD_BODY, True, False): "assert_regex_in_body",
                        (Scenario.FIELD_BODY, True, True): "assert_regex_not_in_body",
                        (Scenario.FIELD_HEADERS, False, False): "assert_in_headers",
                        (Scenario.FIELD_HEADERS, False, True): "assert_not_in_headers",
                        (Scenario.FIELD_HEADERS, True, False): "assert_regex_in_headers",
                        (Scenario.FIELD_HEADERS, True, True): "assert_regex_not_in_headers",
                    }
                    method = func_table[(subject, assertion.get('regexp', True), assertion.get('not', False))]
                    stmts.append(ast.Expr(
                        ast.Call(
                            func=ast.Attribute(
                                value=ast.Name(id="response", ctx=ast.Load()),
                                attr=method,
                                ctx=ast.Load()
                            ),
                            args=[self.gen_expr(member)],
                            keywords=[],
                            starargs=None,
                            kwargs=None
                        )
                    ))
            elif subject == Scenario.FIELD_RESP_CODE:
                for member in assertion["contains"]:
                    method = "assert_status_code" if not assertion.get('not', False) else "assert_not_status_code"
                    stmts.append(ast.Expr(
                        ast.Call(
                            func=ast.Attribute(
                                value=ast.Name(id="response", ctx=ast.Load()),
                                attr=method,
                                ctx=ast.Load()
                            ),
                            args=[self.gen_expr(member)],
                            keywords=[],
                            starargs=None,
                            kwargs=None
                        )
                    ))
        return stmts

    def _gen_jsonpath_assertions(self, request):
        stmts = []
        jpath_assertions = request.config.get("assert-jsonpath", [])
        for idx, assertion in enumerate(jpath_assertions):
            assertion = ensure_is_dict(jpath_assertions, idx, "jsonpath")
            exc = TaurusConfigError('JSON Path not found in assertion: %s' % assertion)
            query = assertion.get('jsonpath', exc)
            expected = assertion.get('expected-value', None)
            method = "assert_not_jsonpath" if assertion.get('invert', False) else "assert_jsonpath"
            stmts.append(ast.Expr(
                ast.Call(
                    func=ast.Attribute(
                        value=ast.Name(id="response", ctx=ast.Load()),
                        attr=method,
                        ctx=ast.Load()
                    ),
                    args=[self.gen_expr(query)],
                    keywords=[ast.keyword(arg="expected_value", value=self.gen_expr(expected))],
                    starargs=None,
                    kwargs=None
                )
            ))

        return stmts

    def _gen_xpath_assertions(self, request):
        stmts = []
        jpath_assertions = request.config.get("assert-xpath", [])
        for idx, assertion in enumerate(jpath_assertions):
            assertion = ensure_is_dict(jpath_assertions, idx, "xpath")
            exc = TaurusConfigError('XPath not found in assertion: %s' % assertion)
            query = assertion.get('xpath', exc)
            parser_type = 'html' if assertion.get('use-tolerant-parser', True) else 'xml'
            validate = assertion.get('validate-xml', False)
            method = "assert_not_xpath" if assertion.get('invert', False) else "assert_xpath"
            stmts.append(ast.Expr(
                ast.Call(
                    func=ast.Attribute(
                        value=ast.Name(id="response", ctx=ast.Load()),
                        attr=method,
                        ctx=ast.Load()
                    ),
                    args=[self.gen_expr(query)],
                    keywords=[ast.keyword(arg="parser_type", value=self.gen_expr(parser_type)),
                              ast.keyword(arg="validate", value=self.gen_expr(validate))],
                    starargs=None,
                    kwargs=None
                )
            ))

        return stmts

    def _gen_extractors(self, request):
        stmts = []
        jextractors = request.config.get("extract-jsonpath")
        for varname in jextractors:
            cfg = ensure_is_dict(jextractors, varname, "jsonpath")
            stmts.append(ast.Assign(
                targets=[self.expr_compiler.gen_var_accessor(varname, ast.Store())],
                value=ast.Call(
                    func=ast.Attribute(
                        value=ast.Name(id="response", ctx=ast.Load()),
                        attr="extract_jsonpath",
                        ctx=ast.Load()
                    ),
                    args=[self.gen_expr(cfg['jsonpath']), self.gen_expr(cfg.get('default', 'NOT_FOUND'))],
                    keywords=[],
                    starargs=None,
                    kwargs=None
                )
            ))

        extractors = request.config.get("extract-regexp")
        for varname in extractors:
            cfg = ensure_is_dict(extractors, varname, "regexp")
            # TODO: support non-'body' value of 'subject'
            stmts.append(ast.Assign(
                targets=[self.expr_compiler.gen_var_accessor(varname, ast.Store())],
                value=ast.Call(
                    func=ast.Attribute(
                        value=ast.Name(id="response", ctx=ast.Load()),
                        attr="extract_regex",
                        ctx=ast.Load()
                    ),
                    args=[self.gen_expr(cfg['regexp']), self.gen_expr(cfg.get('default', 'NOT_FOUND'))],
                    keywords=[],
                    starargs=None,
                    kwargs=None
                )
            ))

        # TODO: css/jquery extractor?

        xpath_extractors = request.config.get("extract-xpath")
        for varname in xpath_extractors:
            cfg = ensure_is_dict(xpath_extractors, varname, "xpath")
            parser_type = 'html' if cfg.get('use-tolerant-parser', True) else 'xml'
            validate = cfg.get('validate-xml', False)
            stmts.append(ast.Assign(
                targets=[self.expr_compiler.gen_var_accessor(varname, ast.Store())],
                value=ast.Call(
                    func=ast.Attribute(
                        value=ast.Name(id="response", ctx=ast.Load()),
                        attr="extract_xpath",
                        ctx=ast.Load()
                    ),
                    args=[self.gen_expr(cfg['xpath'])],
                    keywords=[ast.keyword(arg="default", value=cfg.get('default', 'NOT_FOUND')),
                              ast.keyword(arg="parser_type", value=parser_type),
                              ast.keyword(arg="validate", value=validate)],
                    starargs=None,
                    kwargs=None,
                )
            ))

        return stmts

    def build_tree(self):
        mod = self._gen_module()
        mod.lineno = 0
        mod.col_offset = 0
        mod = ast.fix_missing_locations(mod)
        return mod

    def build_source_code(self):
        self.tree = self.build_tree()

    def save(self, filename):
        with open(filename, 'wt') as fds:
            source = astunparse.unparse(self.tree)
            # because astunparse on Python 2 adds extra comma+space
            class_name = create_class_name(self.label)
            source = source.replace('class %s(unittest.TestCase, )' % class_name,
                                    'class %s(unittest.TestCase)' % class_name)
            fds.write(source)

    def _gen_logging(self):
        set_log = ast.Assign(targets=[ast.Name(id="log")], value=ast.Call(
                func=ast.Attribute(
                    value=ast.Name(id="logging"),
                    attr="getLogger"),
                args=[ast.Str(s="apiritif.http")],
                keywords=[],
                starargs=None,
                kwargs=None))
        add_handler = ast.Call(
            func=ast.Attribute(
                value=ast.Name(id="log"),
                attr="addHandler"),
            args=[ast.Call(
                    func=ast.Attribute(value=ast.Name(id="logging"), attr="StreamHandler"),
                    args=[ast.Attribute(value=ast.Name(id="sys"), attr="stdout")],
                    keywords=[],
                    starargs=None,
                    kwargs=None)],
            keywords=[],
            starargs=None,
            kwargs=None
        )
        set_level = ast.Call(
            func=ast.Attribute(value=ast.Name(id="log"), attr="setLevel"),
            args=[ast.Attribute(value=ast.Name(id="logging"), attr="DEBUG")],
            keywords=[],
            starargs=None,
            kwargs=None)

        return [set_log, self._gen_empty_line_stmt(), add_handler,
                self._gen_empty_line_stmt(), set_level, self._gen_empty_line_stmt()]
