import os
import string
import subprocess
from typing import Text, List, Tuple, Dict, Set, NoReturn

import jinja2
from httprunner import exceptions
from httprunner.compat import ensure_testcase_v3_api, ensure_testcase_v3
from httprunner.loader import (
    load_folder_files,
    load_test_file,
    load_testcase,
    load_testsuite,
    load_project_meta,
)
from httprunner.parser import parse_data
from loguru import logger

""" cache converted pytest files, avoid duplicate making
"""
make_files_cache_set: Set = set()

__TEMPLATE__ = jinja2.Template(
    """# NOTICE: Generated By HttpRunner. DO NOT EDIT!
# FROM: {{ testcase_path }}
{% if imports_list %}
import os
import sys

sys.path.insert(0, os.getcwd())
{% endif %}
from httprunner import HttpRunner, TConfig, TStep
{% for import_str in imports_list %}
{{ import_str }}
{% endfor %}

class {{ class_name }}(HttpRunner):
    config = TConfig(**{{ config }})

    teststeps = [
        {% for teststep in teststeps %}
            TStep(**{{ teststep }}),
        {% endfor %}
    ]

if __name__ == "__main__":
    {{ class_name }}().test_start()

"""
)


def __ensure_file_name(path: Text) -> Text:
    """ ensure file name not startswith digit
        testcases/19.json => testcases/T19.json
    """
    filename = os.path.basename(path)
    if filename[0] in string.digits:
        path = os.path.join(os.path.dirname(path), f"T{filename}")

    return path


def __ensure_absolute(path: Text) -> Text:
    project_meta = load_project_meta(path)

    if os.path.isabs(path):
        absolute_path = path
    else:
        absolute_path = os.path.join(project_meta.PWD, path)

    return absolute_path


def __ensure_cwd_relative(path: Text) -> Text:
    """ convert absolute path to relative path, based on os.getcwd()

    Args:
        path: absolute path

    Returns: relative path based on os.getcwd()

    """
    if os.path.isabs(path):
        return path[len(os.getcwd()) + 1 :]
    else:
        return path


def __ensure_testcase_module(path: Text) -> NoReturn:
    """ ensure pytest files are in python module, generate __init__.py on demand
    """
    init_file = os.path.join(os.path.dirname(path), "__init__.py")
    if os.path.isfile(init_file):
        return

    with open(init_file, "w", encoding="utf-8") as f:
        f.write("# NOTICE: Generated By HttpRunner. DO NOT EDIT!")


def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]:
    """convert single YAML/JSON testcase path to python file"""
    if os.path.isdir(testcase_path):
        # folder does not need to convert
        return testcase_path, ""

    testcase_path = __ensure_file_name(testcase_path)
    raw_file_name, file_suffix = os.path.splitext(os.path.basename(testcase_path))

    file_suffix = file_suffix.lower()
    if file_suffix not in [".json", ".yml", ".yaml"]:
        raise exceptions.ParamsError(
            "testcase file should have .yaml/.yml/.json suffix"
        )

    file_name = raw_file_name.replace(" ", "_").replace(".", "_").replace("-", "_")
    testcase_dir = os.path.dirname(testcase_path)
    testcase_python_path = os.path.join(testcase_dir, f"{file_name}_test.py")

    # convert title case, e.g. request_with_variables => RequestWithVariables
    name_in_title_case = file_name.title().replace("_", "")

    return testcase_python_path, name_in_title_case


def __format_pytest_with_black(python_paths: List[Text]) -> NoReturn:
    logger.info("format pytest cases with black ...")
    try:
        subprocess.run(["black", *python_paths])
    except subprocess.CalledProcessError as ex:
        logger.error(ex)


def __make_testcase(testcase: Dict, dir_path: Text = None) -> NoReturn:
    """convert valid testcase dict to pytest file path"""
    # ensure compatibility with testcase format v2
    testcase = ensure_testcase_v3(testcase)

    # validate testcase format
    load_testcase(testcase)

    testcase_path = __ensure_absolute(testcase["config"]["path"])
    logger.info(f"start to make testcase: {testcase_path}")

    testcase_python_path, testcase_cls_name = convert_testcase_path(testcase_path)
    if dir_path:
        testcase_python_path = os.path.join(
            dir_path, os.path.basename(testcase_python_path)
        )

    global make_files_cache_set
    if testcase_python_path in make_files_cache_set:
        return

    config = testcase["config"]
    config["path"] = __ensure_cwd_relative(testcase_python_path)

    # parse config variables
    config.setdefault("variables", {})
    if isinstance(config["variables"], Text):
        # get variables by function, e.g. ${get_variables()}
        project_meta = load_project_meta(testcase_path)
        config["variables"] = parse_data(
            config["variables"], {}, project_meta.functions
        )

    # prepare reference testcase
    imports_list = []
    teststeps = testcase["teststeps"]
    for teststep in teststeps:
        if not teststep.get("testcase"):
            continue

        # make ref testcase pytest file
        ref_testcase_path = __ensure_absolute(teststep["testcase"])
        __make(ref_testcase_path)

        # prepare ref testcase class name
        ref_testcase_python_path, ref_testcase_cls_name = convert_testcase_path(
            ref_testcase_path
        )
        teststep["testcase"] = f"CLS_LB({ref_testcase_cls_name})CLS_RB"

        # prepare import ref testcase
        ref_testcase_python_path = ref_testcase_python_path[len(os.getcwd()) + 1 :]
        ref_module_name, _ = os.path.splitext(ref_testcase_python_path)
        ref_module_name = ref_module_name.replace(os.sep, ".")
        imports_list.append(
            f"from {ref_module_name} import TestCase{ref_testcase_cls_name} as {ref_testcase_cls_name}"
        )

    data = {
        "testcase_path": __ensure_cwd_relative(testcase_path),
        "class_name": f"TestCase{testcase_cls_name}",
        "config": config,
        "teststeps": teststeps,
        "imports_list": imports_list,
    }
    content = __TEMPLATE__.render(data)
    content = content.replace("'CLS_LB(", "").replace(")CLS_RB'", "")

    with open(testcase_python_path, "w", encoding="utf-8") as f:
        f.write(content)

    __ensure_testcase_module(testcase_python_path)

    logger.info(f"generated testcase: {testcase_python_path}")
    make_files_cache_set.add(__ensure_cwd_relative(testcase_python_path))


def __make_testsuite(testsuite: Dict) -> NoReturn:
    """convert valid testsuite dict to pytest folder with testcases"""
    # validate testsuite format
    load_testsuite(testsuite)

    config = testsuite["config"]
    testsuite_path = config["path"]

    testsuite_variables = config.get("variables", {})
    if isinstance(testsuite_variables, Text):
        # get variables by function, e.g. ${get_variables()}
        project_meta = load_project_meta(testsuite_path)
        testsuite_variables = parse_data(
            testsuite_variables, {}, project_meta.functions
        )

    logger.info(f"start to make testsuite: {testsuite_path}")

    # create directory with testsuite file name, put its testcases under this directory
    testsuite_dir = os.path.join(
        os.path.dirname(testsuite_path),
        os.path.basename(testsuite_path).replace(".", "_"),
    )
    os.makedirs(testsuite_dir, exist_ok=True)

    for testcase in testsuite["testcases"]:
        # get referenced testcase content
        testcase_file = testcase["testcase"]
        testcase_path = __ensure_absolute(testcase_file)
        testcase_dict = load_test_file(testcase_path)
        testcase_dict.setdefault("config", {})
        testcase_dict["config"]["path"] = testcase_path

        # override testcase name
        testcase_dict["config"]["name"] = testcase["name"]
        # override base_url
        base_url = testsuite["config"].get("base_url") or testcase.get("base_url")
        if base_url:
            testcase_dict["config"]["base_url"] = base_url
        # override variables
        testcase_dict["config"].setdefault("variables", {})
        testcase_dict["config"]["variables"].update(testcase.get("variables", {}))
        testcase_dict["config"]["variables"].update(testsuite_variables)

        # make testcase
        __make_testcase(testcase_dict, testsuite_dir)


def __make(tests_path: Text) -> NoReturn:
    """ make testcase(s) with testcase/testsuite/folder absolute path
        generated pytest file path will be cached in make_files_cache_set

    Args:
        tests_path: should be in absolute path

    """
    test_files = []
    if os.path.isdir(tests_path):
        files_list = load_folder_files(tests_path)
        test_files.extend(files_list)
    elif os.path.isfile(tests_path):
        test_files.append(tests_path)
    else:
        raise exceptions.TestcaseNotFound(f"Invalid tests path: {tests_path}")

    for test_file in test_files:
        try:
            test_content = load_test_file(test_file)
        except (exceptions.FileNotFound, exceptions.FileFormatError) as ex:
            logger.warning(ex)
            continue

        # api in v2 format, convert to v3 testcase
        if "request" in test_content:
            test_content = ensure_testcase_v3_api(test_content)

        test_content.setdefault("config", {})["path"] = test_file

        # testcase
        if "teststeps" in test_content:
            try:
                __make_testcase(test_content)
            except exceptions.TestCaseFormatError:
                continue

        # testsuite
        elif "testcases" in test_content:
            try:
                __make_testsuite(test_content)
            except exceptions.TestSuiteFormatError:
                continue

        # invalid format
        else:
            raise exceptions.FileFormatError(
                f"test file is neither testcase nor testsuite: {test_file}"
            )


def main_make(tests_paths: List[Text]) -> List[Text]:
    for tests_path in tests_paths:
        if not os.path.isabs(tests_path):
            tests_path = os.path.join(os.getcwd(), tests_path)

        __make(tests_path)

    testcase_path_list = list(make_files_cache_set)
    __format_pytest_with_black(testcase_path_list)
    return testcase_path_list


def init_make_parser(subparsers):
    """ make testcases: parse command line options and run commands.
    """
    parser = subparsers.add_parser(
        "make", help="Convert YAML/JSON testcases to pytest cases.",
    )
    parser.add_argument(
        "testcase_path", nargs="*", help="Specify YAML/JSON testcase file/folder path"
    )

    return parser
