"""
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 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, HierarchicRequestParser, TransactionBlock, SetVariables
from bzt.six import parse, string_types, iteritems, text_type, PY2
from bzt.utils import dehumanize_time, ensure_is_dict
from .jmeter_functions import Base64DecodeFunction, UrlEncodeFunction, UuidFunction
from .jmeter_functions import TimeFunction, RandomFunction, RandomStringFunction, Base64EncodeFunction


def ast_attr(fields):
    """ fields is string of attrs (e.g. 'self.call.me.now') or list of ast args"""
    if isinstance(fields, string_types):
        if "." in fields:
            fields_list = fields.split(".")
            return ast.Attribute(attr=fields_list[-1], value=ast_attr(".".join(fields_list[:-1])))

        return ast.Name(id=fields)
    else:
        if len(fields) == 1:
            if isinstance(fields[0], string_types):
                return ast.Name(id=fields[0])
            else:
                return fields[0]

        return ast.Attribute(attr=fields[-1], value=ast_attr(fields[:-1]))  # join ast expressions


def ast_call(func, args=None, keywords=None):
    args = args or []
    if isinstance(func, string_types):
        func = ast.Name(id=func)
    return ast.Call(func=func, args=args, starargs=None, kwargs=None, keywords=keywords or [])


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__)

    @staticmethod
    def gen_var_accessor(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)
            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 ApiritifScriptGenerator(object):
    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"
    }

    # Python AST docs: https://greentreesnakes.readthedocs.io/en/latest/

    IMPORTS = """import os
import re
from %s import webdriver
from selenium.common.exceptions import NoSuchElementException
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
"""

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

    ACCESS_TARGET = 'target'
    ACCESS_PLAIN = 'plain'
    SUPPORTED_BLOCKS = (HTTPRequest, TransactionBlock, SetVariables)

    def __init__(self, engine, scenario, label, parent_log,
                 wdlog=None, utils_file=None,
                 ignore_unknown_actions=False, generate_markers=None,
                 capabilities=None, wd_addr=None, test_mode="selenium"):
        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 = []
        self.service_methods = []

        self.remote_address = wd_addr
        self.capabilities = capabilities or {}
        self.window_size = None
        self.wdlog = wdlog
        self.browser = None
        self.appium = False
        self.utils_file = utils_file
        self.ignore_unknown_actions = ignore_unknown_actions
        self.generate_markers = generate_markers
        self.test_mode = test_mode

    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

    def _gen_locator(self, tag, selector):
        return ast_call(
            func=ast_attr("self.driver.find_element"),
            args=[
                ast_attr("By.%s" % self.BYS[tag]),
                self._gen_expr(selector)])

    @staticmethod
    def _gen_store(name, value):
        return ast.Assign(
            targets=[ast.Subscript(
                value=ast_attr("self.vars"),
                slice=ast.Str(name))],
            value=value)

    def _gen_window_mngr(self, atype, selector):
        elements = []
        if atype == "switch":
            elements.append(ast_call(
                func=ast_attr("self.wnd_mng.switch"),
                args=[self._gen_expr(selector)]))
        elif atype == "open":
            elements.append(ast_call(
                func=ast_attr("self.driver.execute_script"),
                args=[self._gen_expr("window.open('%s');" % selector)]))
        elif atype == "close":
            args = []
            if selector:
                args.append(self._gen_expr(selector))
            elements.append(ast_call(
                func=ast_attr("self.wnd_mng.close"),
                args=args))
        return elements

    def _gen_frame_mngr(self, tag, selector):
        elements = []  # todo: byid/byidx disambiguation?
        if tag == "byidx" or selector.startswith("index=") or selector in ["relative=top", "relative=parent"]:
            if tag == "byidx":
                selector = "index=%s" % selector

            elements.append(ast_call(
                func=ast_attr("self.frm_mng.switch"),
                args=[ast.Str(selector)]))
        else:
            elements.append(ast_call(
                func=ast_attr("self.frm_mng.switch"),
                args=[self._gen_locator(tag, selector)]))
        return elements

    def _gen_chain_mngr(self, atype, tag, param, selector):
        elements = []
        if atype in self.ACTION_CHAINS:
            operator = ast_attr(fields=(
                ast_call(func="ActionChains", args=[ast_attr("self.driver")]),
                self.ACTION_CHAINS[atype]))
            elements.append(ast_call(
                func=ast_attr(
                    fields=(
                        ast_call(
                            func=operator,
                            args=[self._gen_locator(tag, selector)]),
                        "perform"))))
        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])
                operator = ast_attr(
                    fields=(
                        ast_call(
                            func="ActionChains",
                            args=[ast_attr("self.driver")]),
                        "drag_and_drop"))
                elements.append(ast_call(
                    func=ast_attr(
                        fields=(
                            ast_call(
                                func=operator,
                                args=[self._gen_locator(tag, selector),
                                      self._gen_locator(drop_tag, drop_selector)]),
                            "perform"))))
        return elements

    def _gen_assert_store_mngr(self, atype, tag, param, selector):
        elements = []
        if tag == 'title':
            if atype.startswith('assert'):
                elements.append(ast_call(
                    func=ast_attr("self.assertEqual"),
                    args=[ast_attr("self.driver.title"), self._gen_expr(selector)]))
            else:
                elements.append(self._gen_store(
                    name=param.strip(),
                    value=self._gen_expr(ast_attr("self.driver.title"))))
        elif atype == 'store' and tag == 'string':
            elements.append(self._gen_store(
                name=param.strip(),
                value=self._gen_expr(selector.strip())))
        else:
            target = None

            if atype in ["asserttext", "storetext"]:
                target = "innerText"
            elif atype in ["assertvalue", "storevalue"]:
                target = "value"

            if target:
                locator_attr = ast_call(
                    func=ast_attr(
                        fields=(
                            self._gen_locator(tag, selector),
                            "get_attribute")),
                    args=[ast.Str(target)])

                if atype.startswith("assert"):
                    elements.append(ast_call(
                        func=ast_attr(fields="self.assertEqual"),
                        args=[
                            ast_call(
                                func=ast_attr(
                                    fields=(
                                        self._gen_expr(locator_attr),
                                        "strip"))),
                            ast_call(
                                func=ast_attr(
                                    fields=(
                                        self._gen_expr(param),
                                        "strip")))]))
                elif atype.startswith('store'):
                    elements.append(self._gen_store(
                        name=param.strip(),
                        value=self._gen_expr(locator_attr)))

        return elements

    def _gen_keys_mngr(self, atype, tag, param, selector):
        elements = []
        args = []
        action = None

        if atype == "click":
            action = "click"
        elif atype == "submit":
            action = "submit"
        elif atype in ["keys", "type"]:
            if atype == "type":
                elements.append(ast_call(
                    func=ast_attr(
                        fields=(
                            self._gen_locator(tag, selector),
                            "clear"))))
            action = "send_keys"
            if isinstance(param, (string_types, text_type)) and param.startswith("KEY_"):
                args = [ast_attr("Keys.%s" % param.split("KEY_")[1])]
            else:
                args = [self._gen_expr(str(param))]

        if action:
            elements.append(ast_call(
                func=ast_attr(
                    fields=(
                        self._gen_locator(tag, selector),
                        action)),
                args=args))
        return elements

    def _gen_edit_mngr(self, tag, param, selector):
        msg = "The element (By.%s, %r) is not contenteditable element"
        exc_type = ast_call(
            func="NoSuchElementException",
            args=[ast.Str(msg % (self.BYS[tag], selector))])

        if PY2:
            raise_kwargs = {
                "type": exc_type,
                "inst": None,
                "tback": None
            }
        else:
            raise_kwargs = {
                "exc": exc_type,
                "cause": None}

        short_locator = ast_call(
            func=ast_attr("self.driver.find_element"),
            args=[
                ast_attr("By.%s" % self.BYS[tag]),
                ast.Str(selector)])

        element = ast.If(
            test=ast_call(
                func=ast_attr(
                    fields=(short_locator, "get_attribute")),
                args=[ast.Str("contenteditable")]),
            body=[  # self.driver.execute_script(editable_script)
                ast.Expr(ast_call(func=ast_attr("self.driver.execute_script"),
                                  args=[
                                      ast.BinOp(
                                          left=ast.Str("arguments[0].innerHTML = %s;"),
                                          op=ast.Mod(),
                                          right=self._gen_expr(param.strip())),
                                      short_locator]))],
            orelse=[
                ast.Raise(**raise_kwargs)])

        return [element]

    def _gen_screenshot_mngr(self, selector):
        elements = []
        if selector:
            elements.append(ast_call(
                func=ast_attr("self.driver.save_screenshot"),
                args=[self._gen_expr(selector)]))
        else:
            elements.append(ast.Assign(
                targets=[ast.Name(id="filename")],
                value=ast_call(
                    func=ast_attr("os.path.join"),
                    args=[
                        ast_call(
                            func=ast_attr("os.getenv"),
                            args=[ast.Str('TAURUS_ARTIFACTS_DIR')]),
                        ast.BinOp(
                            left=ast.Str('screenshot-%d.png'),
                            op=ast.Mod(),
                            right=ast.BinOp(
                                left=ast_call(func="time"),
                                op=ast.Mult(),
                                right=ast.Num(1000)))])))
            elements.append(ast_call(
                func=ast_attr("self.driver.save_screenshot"),
                args=[ast.Name(id="filename")]))
        return elements

    def _gen_wait_sleep_mngr(self, atype, tag, param, selector):
        elements = []
        mode = "visibility" if param == 'visible' else 'presence'

        if atype == 'wait':
            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)
            elements.append(ast_call(
                func=ast_attr(
                    fields=(
                        ast_call(
                            func="WebDriverWait",
                            args=[
                                ast_attr("self.driver"),
                                ast.Num(timeout)]),
                        "until")),
                args=[
                    ast_call(
                        func=ast_attr("econd.%s_of_element_located" % mode),
                        args=[
                            ast.Tuple(
                                elts=[
                                    ast_attr("By.%s" % self.BYS[tag]),
                                    self._gen_expr(selector)])]),
                    ast.Str(errmsg)]))

        elif atype == 'pause' and tag == 'for':
            elements.append(ast_call(
                func="sleep",
                args=[ast.Num(dehumanize_time(selector))]))

        return elements

    def _gen_select_mngr(self, tag, param, selector):
        element = (ast_call(
            func=ast_attr(
                fields=(
                    ast_call(func="Select", args=[self._gen_locator(tag, selector)]),
                    "select_by_visible_text")),
            args=[self._gen_expr(param)]))
        return [element]

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

        action_elements = []

        if tag == "window":
            action_elements.extend(self._gen_window_mngr(atype, selector))
        elif atype == "switchframe":
            action_elements.extend(self._gen_frame_mngr(tag, selector))
        elif atype in self.ACTION_CHAINS or atype == "drag":
            action_elements.extend(self._gen_chain_mngr(atype, tag, param, selector))
        elif atype == "select":
            action_elements.extend(self._gen_select_mngr(tag, param, selector))
        elif atype.startswith("assert") or atype.startswith("store"):
            action_elements.extend(self._gen_assert_store_mngr(atype, tag, param, selector))

        elif atype in ("click", "type", "keys", "submit"):
            action_elements.extend(self._gen_keys_mngr(atype, tag, param, selector))

        elif atype == 'echo' and tag == 'string':
            if len(selector) > 0 and not param:
                action_elements.append(ast_call(
                    func="print",
                    args=[self._gen_expr(selector.strip())]))

        elif atype == "script" and tag == "eval":
            action_elements.append(ast_call(func=ast_attr("self.driver.execute_script"),
                                            args=[self._gen_expr(selector)]))
        elif atype == "rawcode":
            action_elements.append(ast.parse(param))
        elif atype == 'go':
            if selector and not param:
                action_elements.append(ast_call(func=ast_attr("self.driver.get"),
                                                args=[self._gen_expr(selector.strip())]))
        elif atype == "editcontent":  # todo: check it functionally (possibly broken)
            action_elements.extend(self._gen_edit_mngr(tag, param, selector))
        elif atype in ('wait', 'pause'):
            action_elements.extend(self._gen_wait_sleep_mngr(atype, tag, param, selector))
        elif atype == 'clear' and tag == 'cookies':
            action_elements.append(ast_call(
                func=ast_attr("self.driver.delete_all_cookies")))
        elif atype == 'screenshot':
            action_elements.extend(self._gen_screenshot_mngr(selector))

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

        return [ast.Expr(element) for element in action_elements]

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

    def _check_platform(self):
        mobile_browsers = ["chrome", "safari"]
        mobile_platforms = ["android", "ios"]

        browser = self.capabilities.get("browserName", "")
        browser = self.scenario.get("browser", browser)
        browser = browser.lower()  # todo: whether we should take browser as is? (without lower case)

        browser_platform = None
        if browser:
            browser_split = browser.split("-")
            browser = browser_split[0]
            browsers = ["firefox", "chrome", "ie", "opera"] + mobile_browsers
            if browser not in browsers:
                raise TaurusConfigError("Unsupported browser name: %s" % browser)
            if len(browser_split) > 1:
                browser_platform = browser_split[1]

        if self.remote_address:
            if browser and browser != "remote":
                msg = "Forcing browser to Remote, because of remote WebDriver address, use '%s' as browserName"
                self.log.warning(msg % browser)
                self.capabilities["browserName"] = 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
            self.remote_address = "http://localhost:4723/wd/hub"
            self.capabilities["platformName"] = browser_platform
            self.capabilities["browserName"] = browser
            browser = "remote"  # Force to use remote web driver
        elif not browser:
            browser = "firefox"

        return browser

    def _gen_webdriver(self):
        self.log.debug("Generating setUp test method")
        browser = self._check_platform()

        headless = self.scenario.get("headless", False)
        headless_setup = []
        if headless:
            self.log.info("Headless mode works only with Selenium 3.8.0+, be sure to have it installed")
            headless_setup = [ast.Expr(
                ast_call(func=ast_attr("options.set_headless")))]

        body = []

        if browser == 'firefox':
            body.append(ast.Assign(
                targets=[ast.Name(id="options")],
                value=ast_call(
                    func=ast_attr("webdriver.FirefoxOptions"))))
            body.extend(headless_setup)
            body.append(ast.Assign(
                targets=[ast.Name(id="profile")],
                value=ast_call(func=ast_attr("webdriver.FirefoxProfile"))))
            body.append(ast.Expr(
                ast_call(
                    func=ast_attr("profile.set_preference"),
                    args=[ast.Str("webdriver.log.file"), ast.Str(self.wdlog)])))

            body.append(ast.Assign(
                targets=[ast.Name(id="driver")],
                value=ast_call(
                    func=ast_attr("webdriver.Firefox"),
                    args=[ast.Name(id="profile")],
                    keywords=[ast.keyword(
                        arg="firefox_options",
                        value=ast.Name(id="options"))])))
        elif browser == 'chrome':
            body.append(ast.Assign(
                targets=[ast.Name(id="options")],
                value=ast_call(
                    func=ast_attr("webdriver.ChromeOptions"))))
            body.extend(headless_setup)
            body.append(ast.Assign(
                targets=[ast.Name(id="driver")],
                value=ast_call(
                    func=ast_attr("webdriver.Chrome"),
                    keywords=[
                        ast.keyword(
                            arg="service_log_path",
                            value=ast.Str(self.wdlog)),
                        ast.keyword(
                            arg="chrome_options",
                            value=ast.Name(id="options"))])))
        elif browser == 'remote':
            keys = sorted(self.capabilities.keys())
            values = [str(self.capabilities[key]) for key in keys]
            body.append(ast.Assign(
                targets=[ast.Name(id="driver")],
                value=ast_call(
                    func=ast_attr("webdriver.Remote"),
                    keywords=[
                        ast.keyword(
                            arg="command_executor",
                            value=ast.Str(self.remote_address)),
                        ast.keyword(
                            arg="desired_capabilities",
                            value=ast.Dict(
                                keys=[ast.Str(key) for key in keys],
                                values=[ast.Str(value) for value in values]))])))
        else:
            if headless:
                self.log.warning("Browser %r doesn't support headless mode" % browser)

            body.append(ast.Assign(
                targets=[ast.Name(id="driver")],
                value=ast_call(
                    func=ast_attr("webdriver.%s" % browser))))  # todo bring 'browser' to correct case

        scenario_timeout = self.scenario.get("timeout", "30s")
        body.append(ast.Expr(
            ast_call(
                func=ast_attr("driver.implicitly_wait"),
                args=[ast.Num(dehumanize_time(scenario_timeout))])))

        body.append(ast.Assign(
            targets=[ast.Name(id="wnd_mng")],
            value=ast_call(
                func=ast.Name(id="WindowManager"),
                args=[ast.Name(id="driver")])))
        body.append(ast.Assign(
            targets=[ast.Name(id="frm_mng")],
            value=ast_call(
                func=ast.Name(id="FrameManager"),
                args=[ast.Name(id="driver")])))

        if self.window_size:  # FIXME: unused in fact ?
            body.append(ast.Expr(
                ast_call(
                    func=ast_attr("target.set_window_position"),
                    args=[ast.Num(0), ast.Num(0)])))

            body.append(ast.Expr(
                ast_call(
                    func=ast_attr("target.set_window_position"),
                    args=[ast.Num(self.window_size[0]), ast.Num(self.window_size[1])])))

        else:
            pass  # TODO: setup_method_def.append(self.gen_statement("self.driver.maximize_window()"))
            # but maximize_window does not work on virtual displays. Bummer

        return body

    @staticmethod
    def _gen_impl_wait(timeout):
        return ast.Expr(
            ast_call(
                func=ast_attr("self.driver.implicitly_wait"),
                args=[ast.Num(dehumanize_time(timeout))]))

    def _gen_module(self):
        stmts = []

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

        stmts.extend(self._gen_data_source_readers())
        stmts.extend(self._gen_module_setup())

        if self.test_mode == "selenium":
            stmts.append(self._gen_module_teardown())

        stmts.append(self._gen_classdef())

        stmts = self._gen_imports() + stmts     # todo: order is important (with classdef) because of self.appium setup

        if self.test_mode == "selenium":
            with open(self.utils_file) as fds:
                utilities_source_lines = fds.read()
            stmts.append(ast.parse(utilities_source_lines))

        return ast.Module(body=stmts)

    def _gen_imports(self):
        imports = [
            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='unittest', asname=None)]),
            ast.ImportFrom(
                module="time",
                names=[
                    ast.alias(name="time", asname=None),
                    ast.alias(name="sleep", asname=None)],
                level=0),
            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()]

        if self.test_mode == "selenium":
            if self.appium:
                source = "appium"
            else:
                source = "selenium"

            imports.append(ast.parse(self.IMPORTS % source).body)

        return imports

    def _gen_module_setup(self):
        if self.test_mode == "apiritif":
            target_init = self._gen_api_target()
        else:
            target_init = self._gen_webdriver()

        data_sources = [self._gen_default_vars()]
        for idx in range(len(self.data_sources)):
            data_sources.append(ast.Expr(ast_call(func=ast_attr("reader_%s.read_vars" % (idx + 1)))))

        for idx in range(len(self.data_sources)):
            extend_vars = ast_call(
                func=ast_attr("vars.update"),
                args=[ast_call(
                    func=ast_attr("reader_%s.get_vars" % (idx + 1)))])
            data_sources.append(ast.Expr(extend_vars))

        self.stored_vars = ['vars']
        if target_init:
            if self.test_mode == "apiritif":
                self.stored_vars.append("target")
            else:
                self.stored_vars.extend(["driver", "wnd_mng", "frm_mng"])

        store_call = ast_call(
            func=ast_attr("apiritif.put_into_thread_store"),
            args=[ast.Name(id=var) for var in self.stored_vars])

        store_block = [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_attr("apiritif.CSVReaderPerThread"),
                    args=[ast.Str(s=csv_file)],
                    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())

        return ast.ClassDef(
            name=create_class_name(self.label),
            bases=[ast_attr("unittest.TestCase")],
            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])

        body = [ast.Assign(
            targets=[fields],
            value=ast_call(func=ast_attr("apiritif.get_from_thread_store")))]

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

    def _gen_module_teardown(self):
        fields = []
        for var in self.stored_vars:
            if var == "driver":
                fields.append(var)
            else:
                fields.append("_")

        ast_fields = ast.Tuple(elts=[ast.Name(id="%s" % var) for var in fields])

        body = [ast.Assign(
            targets=[ast_fields],
            value=ast_call(func=ast_attr("apiritif.get_from_thread_store")))]

        body.append(ast.Expr(ast_call(func=ast_attr("driver.quit"))))
        return ast.FunctionDef(name="teardown", args=[], body=body, decorator_list=[])

    def _add_markers(self, body, label):
        def marker(case=None, suite=None, status=None, exc_msg=None):
            if case and suite:
                marker_msg = "/* FLOW_MARKER test-case-start */"
                keys = [ast.Str("testCaseName"), ast.Str("testSuiteName")]
                values = [ast.Str(case), ast.Str(suite)]
            else:
                marker_msg = "/* FLOW_MARKER test-case-stop */"
                if exc_msg is None:
                    exc_msg = ast_call(func="str", args=[ast.Name(id="exc")])
                else:
                    exc_msg = ast.Str(exc_msg)

                keys = [ast.Str("status"), ast.Str("message")]
                values = [ast.Str(status), exc_msg]

            return ast.Expr(ast_call(
                func=ast_attr("self.driver.execute_script"),
                args=[
                    ast.Str(marker_msg),
                    ast.Dict(keys=keys, values=values)]))

        start_marker = marker(case=label, suite=self.label)
        kwargs = {"body": [start_marker] + body}

        if PY2:
            ast_try = ast.TryExcept
            name = ast.Name(id="exc")
            reraise = ast.Raise(type=None, inst=None, tback=None)
        else:
            ast_try = ast.Try
            name = "exc"
            reraise = ast.Raise(exc=None, cause=None)
            kwargs["finalbody"] = []

        kwargs["handlers"] = [
            ast.ExceptHandler(
                type=ast.Name(id="AssertionError"),
                name=name,
                body=[marker(status="failed"), reraise]),
            ast.ExceptHandler(
                type=ast.Name(id="BaseException"),
                name=name,
                body=[marker(status="broken"), reraise])]

        kwargs["orelse"] = [marker(status="success", exc_msg="")]

        return ast_try(**kwargs)

    def _gen_test_methods(self):
        requests = self.scenario.get_requests(parser=HierarchicRequestParser, require_url=False)

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

            # convert top-level http request to transaction
            if isinstance(request, HTTPRequest):
                request = TransactionBlock(
                    name=request.label,
                    requests=[request],
                    include_timers=[],
                    config=request.config,
                    scenario=request.scenario)

            if isinstance(request, TransactionBlock):
                body = [self._gen_transaction(request)]
                label = create_method_name(request.label[:40])
                if self.generate_markers:
                    body = self._add_markers(body=body, label=request.label)
            elif isinstance(request, SetVariables):
                body = self._gen_set_vars(request)
                label = request.config.get("label", "set_variables")
            else:
                return

            counter = str(index).zfill(number_of_digits)
            method_name = 'test_' + counter + '_' + label

            if isinstance(request, SetVariables):
                self.service_methods.append(method_name)  # for sample excluding

            yield self._gen_test_method(method_name, body)

    def _gen_set_vars(self, request):
        res = []
        for name in sorted(request.mapping.keys()):
            res.append(ast.Assign(
                targets=[self._gen_expr("${%s}" % name)],
                value=ast.Str(s="%s" % request.mapping[name])))

        return res

    @staticmethod
    def _gen_test_method(name, body):
        # 'test_01_get_posts'
        return ast.FunctionDef(
            name=name,
            args=[ast.Name(id='self', ctx=ast.Param())],
            body=body,
            decorator_list=[])

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

    def _gen_target_setup(self, key, value):
        return ast.Expr(ast_call(
            func=ast_attr("target.%s" % key),
            args=[self._gen_expr(value)]))

    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_api_target(self):
        keepalive = self.scenario.get("keepalive", 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:

            target.extend([
                self._init_target(),
                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 _init_target(self):
        # todo: allow empty address in apiritif (HTTPTarget.__init__)
        default_address = self.scenario.get("default-address", "")

        target_call = ast_call(
            func=ast_attr("apiritif.http.target"),
            args=[self._gen_expr(default_address)])

        target = ast.Assign(
            targets=[ast.Name(id="target", ctx=ast.Store())],
            value=target_call)

        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

    # generate transactions recursively
    def _gen_transaction(self, trans_conf):
        body = []
        for request in trans_conf.requests:
            if isinstance(request, TransactionBlock):
                body.append(self._gen_transaction(request))
            else:
                body.append(self._gen_http_request(request))

        transaction_class = "apiritif.transaction"
        if self.test_mode == "selenium":
            transaction_class += "_logged"

        transaction = ast.With(
            context_expr=ast_call(
                func=ast_attr(transaction_class),
                args=[self._gen_expr(trans_conf.label)]),
            optional_vars=None,
            body=body)

        return transaction

    def _gen_http_request(self, req):
        lines = []
        think_time = dehumanize_time(req.get_think_time())

        if req.url:
            if self.test_mode == "selenium":
                if req.timeout:
                    lines.append(self._gen_impl_wait(req.timeout))
                default_address = self.scenario.get("default-address")
                parsed_url = parse.urlparse(req.url)
                if default_address and not parsed_url.netloc:
                    url = default_address + req.url
                else:
                    url = req.url

                lines.append(ast.Expr(
                    ast_call(
                        func=ast_attr("self.driver.get"),
                        args=[self._gen_expr(url)])))

            else:
                method = req.method.lower()
                named_args = self._extract_named_args(req)

                if self._access_method() == ApiritifScriptGenerator.ACCESS_TARGET:
                    requestor = ast_attr("self.target")
                else:
                    requestor = ast_attr("apiritif.http")

                keywords = [ast.keyword(
                    arg=name,
                    value=self._gen_expr(value)) for name, value in iteritems(named_args)]

                lines.append(ast.Assign(
                    targets=[ast.Name(id="response")],
                    value=ast_call(
                        func=ast_attr((requestor, method)),
                        args=[self._gen_expr(req.url)],
                        keywords=keywords)))

        elif "actions" not in req.config:
            self.log.warning("'url' and/or 'actions' are mandatory for request but not found: '%s'", req.config)
            return [ast.Pass()]

        if self.test_mode == "selenium":
            for action in req.config.get("actions"):
                lines.extend(self._gen_action(action))

            if "assert" in req.config:
                lines.append(ast.Assign(
                    targets=[ast.Name(id="body")],
                    value=ast_attr("self.driver.page_source")))
                for assert_config in req.config.get("assert"):
                    lines.extend(self._gen_sel_assertion(assert_config))

        else:
            lines.extend(self._gen_assertions(req))
            lines.extend(self._gen_jsonpath_assertions(req))
            lines.extend(self._gen_xpath_assertions(req))

        lines.extend(self._gen_extractors(req))

        if think_time:
            lines.append(ast.Expr(
                ast_call(
                    func=ast_attr("sleep"),
                    args=[self._gen_expr(think_time)])))

        return lines

    def _gen_sel_assertion(self, assertion_config):
        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:
                if reverse:
                    method = "self.assertEqual"
                else:
                    method = "self.assertNotEqual"
                assertion_elements.append(
                    ast.Assign(
                        targets=[ast.Name(id="re_pattern")],
                        value=ast_call(
                            func=ast_attr("re.compile"),
                            args=[ast.Str(val)])))

                assertion_elements.append(ast.Expr(
                    ast_call(
                        func=ast_attr(method),
                        args=[
                            ast.Num(0),
                            ast_call(
                                func=ast.Name(id="len"),
                                args=[ast_call(
                                    func=ast_attr("re.findall"),
                                    args=[ast.Name(id="re_pattern"), ast.Name(id="body")])]),
                            ast.Str("Assertion: %s" % assert_message)])))

            else:
                if reverse:
                    method = "self.assertNotIn"
                else:
                    method = "self.assertIn"
                assertion_elements.append(
                    ast.Expr(
                        ast_call(
                            func=ast_attr(method),
                            args=[
                                ast.Str(val),
                                ast.Name(id="body"),
                                ast.Str("Assertion: %s" % assert_message)])))

        return assertion_elements

    def _gen_default_vars(self):
        variables = self.scenario.get("variables")
        names = sorted(variables.keys())
        values = [variables[name] for name in names]

        return ast.Assign(
            targets=[ast.Name(id='vars', ctx=ast.Store())],
            value=ast.Dict(
                keys=[self._gen_expr(name) for name in names],
                values=[self._gen_expr(val) for val in 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_attr("response.%s" % method),
                            args=[self._gen_expr(member)])))

            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_attr("response.%s" % method),
                            args=[self._gen_expr(member)])))
        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_attr("response.%s" % method),
                    args=[self._gen_expr(query)],
                    keywords=[ast.keyword(arg="expected_value", value=self._gen_expr(expected))])))

        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_attr("response.%s" % method),
                    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))])))
        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_attr("response.extract_jsonpath"),
                    args=[self._gen_expr(cfg['jsonpath']), self._gen_expr(cfg.get('default', 'NOT_FOUND'))])))

        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_attr("response.extract_regex"),
                    args=[self._gen_expr(cfg['regexp']), self._gen_expr(cfg.get('default', 'NOT_FOUND'))])))

        # 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_attr("response.extract_xpath"),
                    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)])))
        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:
            fds.write("# coding=utf-8\n")
            fds.write(astunparse.unparse(self.tree))

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

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