""" unit tests for aedev.git_repo_manager portion. """
import contextlib
import os
import shutil
import sys
import tempfile
from collections import OrderedDict
from unittest.mock import patch

# noinspection PyPackageRequirements
import pytest
from ae.base import (
    BUILD_CONFIG_FILE, DOCS_FOLDER, PY_EXT, PY_INIT, TEMPLATES_FOLDER, TESTS_FOLDER,
    norm_name, norm_path, project_main_file, read_file, write_file)
from ae.paths import path_items
from aedev.setup_project import (
    APP_PRJ, DJANGO_PRJ, MODULE_PRJ, NO_PRJ, PACKAGE_PRJ, PARENT_FOLDERS, PARENT_PRJ, REQ_DEV_FILE_NAME, ROOT_PRJ,
    code_file_version)

import aedev.git_repo_manager
# noinspection PyProtectedMember
from aedev.git_repo_manager import (
    ANY_PRJ_TYPE, ARG_ALL, COMMIT_MSG_FILE_NAME, GIT_FOLDER_NAME, NULL_VERSION, LOCK_EXT, MAIN_BRANCH,
    OUTSOURCED_FILE_NAME_PREFIX, OUTSOURCED_MARKER, PACKAGE_VERSION_SEP, REGISTERED_ACTIONS,
    REGISTERED_TPL_PROJECTS, REMOTE_CLASS_NAMES, SKIP_PRJ_TYPE_FILE_NAME_PREFIX,
    TEMPLATE_INCLUDE_FILE_PLACEHOLDER_ID, TEMPLATE_PLACEHOLDER_ARGS_SUFFIX, TEMPLATE_PLACEHOLDER_ID_PREFIX,
    TEMPLATE_PLACEHOLDER_ID_SUFFIX,
    TPL_FILE_NAME_PREFIX, TPL_IMPORT_NAME_PREFIX, TPL_PACKAGES, TPL_STOP_CNV_PREFIX,
    __version__ as grm_version, GitlabCom, PdvType, cae,
    _act_callable, _action, _available_actions, _cl, _debug_or_verbose, _exit_error, _expected_args,
    _get_branch, _get_user,
    _git_add, _git_checkout, _git_clone, _git_commit, _git_current_branch, _git_diff, _git_init_if_needed, _git_status,
    _git_uncommitted, _in_prj_dir_venv,
    _init_act_args_check, _init_act_exec_args, _init_children_pdv_args, _init_children_presets,
    _patch_outsourced, _print_pdv, _renew_prj_dir, _renew_project,
    _template_projects, _register_template, _template_version_option,
    _wait,
    activate_venv, active_venv,
    bump_file_version, deploy_template, editable_project_path, find_extra_modules, increment_version,
    in_venv, main_file_path, next_package_version, package_version, patch_string, pdv_str, project_dev_vars,
    pypi_versions, refresh_templates, replace_file_version, replace_with_file_content_or_default, venv_bin_path,
    add_children_file, bump_version, check_children_integrity, check_integrity, clone_children, clone_project,
    commit_children, commit_project, delete_children_file, install_children_editable, install_editable,
    new_app, new_children, new_django, new_module, new_namespace_root, new_package, new_project, on_ci_host,
    prepare_children_commit, prepare_commit, refresh_children_outsourced, rename_children_file, run_children_command,
    show_actions, show_children_status, show_children_versions, show_status, update_children,
)
from conftest import skip_gitlab_ci


tst_ns_name = 'nsn'
tst_ns_por_name = 'tst_por'
tst_pkg_name = tst_ns_name + '_' + tst_ns_por_name
tst_root_name = tst_ns_name + '_' + tst_ns_name
tst_namespaces_roots = ['aedev.aedev', tst_ns_name + '.' + tst_ns_name]

LOCAL_VENV = 'ae39'     # optional local virtual pyenv to test *venv*-functions completely


@contextlib.contextmanager
def _init_parent():
    with tempfile.TemporaryDirectory() as tmp_path:
        yield os.path.join(tmp_path, PARENT_FOLDERS[0])


@contextlib.contextmanager
def _init_repo(pkg_name: str = ""):
    with _init_parent() as parent_path:
        project_path = os.path.join(parent_path, pkg_name or tst_pkg_name)
        os.makedirs(project_path)
        write_file(os.path.join(project_path, ".gitignore"), read_file(".gitignore"))
        with _in_prj_dir_venv(project_path):
            _cl(963, "git init", exit_on_err=False)  # exit_on_err=False needed to prevent get_option call from _chk_if
            _cl(963, "git config", extra_args=("user.email", "test@test.tst"), exit_on_err=False)
            _cl(963, "git config", extra_args=("user.name", "TestUserName"), exit_on_err=False)
            _cl(963, "git checkout", extra_args=("-b", MAIN_BRANCH))
            _cl(963, "git commit", extra_args=("-v", "--allow-empty", "-m", "unit tst repo init"))
        yield project_path


@pytest.fixture
def changed_repo_path():
    """ provide a git repository with uncommitted changes, yielding the project's temporary working tree root path. """
    with _init_repo() as project_path:
        with _in_prj_dir_venv(project_path):
            write_file(os.path.join(project_path, 'delete.txt'), "--will be deleted")
            write_file(os.path.join(project_path, 'changed.py'), "# will be changed")
            _cl(969, "git add", extra_args=["-A"], exit_on_err=False)
            _cl(969, "git commit", extra_args=["-m", "git commit message"], exit_on_err=False)

            write_file(os.path.join(project_path, 'added.cfg'), "# added/staged to repo")
            os.remove(os.path.join(project_path, 'delete.txt'))
            write_file(os.path.join(project_path, 'changed.py'), "# got changed")

        yield project_path


@pytest.fixture
def empty_repo_path():
    """ provide an empty git repository, yielding the path of the project's temporary working tree root. """
    with _init_repo() as project_path:
        yield project_path


@pytest.fixture
def gitlab_remote(mocked_app_options):
    """ provide a connected Gitlab remote repository api. """
    mocked_app_options['group'] = "aedev-group"
    mocked_app_options['namespace'] = ""
    mocked_app_options['package'] = "aedev_git_repo_manager"
    remote_project = GitlabCom()
    remote_project.connect(project_dev_vars(), cae.get_variable('gitToken'))
    yield remote_project


@pytest.fixture
def mocked_app_options():
    """ prevent argument parsing via cae.get_option('domain') called by _get_domain(). """
    ori_get_arg = cae.get_argument
    ori_get_opt = cae.get_option
    ori_set_opt = cae.set_option

    mocked_options = {_template_version_option(import_name): "" for import_name in tst_namespaces_roots + TPL_PACKAGES}

    cae.get_argument = cae.get_option = lambda opt: mocked_options.get(opt, f"{opt}_mocked_app_option_value")
    cae.set_option = lambda opt, val, save_to_config = True: mocked_options.__setitem__(opt, val)

    yield mocked_options

    cae.get_argument = ori_get_arg
    cae.get_option = ori_get_opt
    cae.set_option = ori_set_opt


@pytest.fixture
def module_repo_path():
    """ minimal/empty test namespace module project. """
    with _init_repo() as project_path:
        with _in_prj_dir_venv(project_path):
            os.makedirs(tst_ns_name)
            write_file(os.path.join(tst_ns_name, tst_ns_por_name + PY_EXT),
                       f"\"\"\" {tst_ns_name} namespace module portion tst docstr \"\"\"\n\n__version__ = '33.6.969'\n")
        yield project_path


@pytest.fixture
def patched_exit_call_wrapper():
    """ count _exit_error() calls and ensure return of function to be tested. """
    exit_called = 0

    class _ExitCaller(Exception):
        """ exception to recognize and simulate app exit for function to be tested. """

    def _exit_(*_args, **_kwargs):
        nonlocal exit_called
        exit_called += 1
        raise _ExitCaller

    def _call_wrapper(fun, *args, **kwargs):
        try:
            ret = fun(*args, **kwargs)
        except _ExitCaller:
            ret = None
        return ret

    # noinspection PyProtectedMember
    ori_fun = aedev.git_repo_manager._exit_error
    aedev.git_repo_manager._exit_error = _exit_

    yield _call_wrapper

    # noinspection PyProtectedMember
    aedev.git_repo_manager._exit_error = ori_fun
    assert exit_called


@pytest.fixture
def root_repo_path():
    """ minimal/empty test namespace root project. """
    with _init_repo(tst_root_name) as project_path:
        with _in_prj_dir_venv(project_path):
            os.makedirs(os.path.join(tst_ns_name, tst_ns_name))
            write_file(os.path.join(tst_ns_name, tst_ns_name, PY_INIT),
                       f"\"\"\" {tst_ns_name} namespace root docstr \"\"\"\n\n__version__ = '333.69.96'\n")
        yield project_path


def setup_module():
    """ clone template projects to speedup tests, printing warning messages if tested module is not setup correctly. """
    print(f"##### setup_module BEG {__file__} - registered template projects={REGISTERED_TPL_PROJECTS}")

    ori_get_opt = cae.get_option
    cae.get_option = lambda _opt: ""
    for import_name in tst_namespaces_roots + TPL_PACKAGES:
        tpl_prj = _register_template(import_name, [], True, [])
        print(f"    ->{tpl_prj}")
    cae.get_option = ori_get_opt

    print(f"##### setup_module END {__file__} - registered template projects={REGISTERED_TPL_PROJECTS}")


def teardown_module():
    """ check if tested module is still setup correctly at the end of this test module. """
    print(f"##### teardown_module {__file__} - registered template projects={REGISTERED_TPL_PROJECTS}")


def test_module_setup_init_of_constants():
    assert REGISTERED_ACTIONS
    assert REMOTE_CLASS_NAMES
    assert TPL_PACKAGES
    assert len(tst_namespaces_roots) + len(TPL_PACKAGES) == len(REGISTERED_TPL_PROJECTS)


@skip_gitlab_ci  # skip on gitlab because of missing remote repository user account token
class TestGitlabActions:
    def test_clean_releases(self, gitlab_remote, mocked_app_options, module_repo_path):
        mocked_app_options['gitToken'] = cae.get_variable('gitToken')
        mocked_app_options['path'] = module_repo_path
        mocked_app_options['verbose'] = True
        gitlab_remote.clean_releases(project_dev_vars(project_path=module_repo_path))

    def test_show_repo(self, capsys, gitlab_remote):
        gitlab_remote.show_repo(project_dev_vars())
        output = capsys.readouterr().out
        assert "default_branch = develop" in output
        assert "name = aedev_git_repo_manager" in output
        assert "visibility = public" in output


class TestLocalActions:
    def test_add_children_file(self, empty_repo_path, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = ""
        mocked_app_options['path'] = module_repo_path
        mocked_app_options['verbose'] = True
        mocked_app_options[_template_version_option(tst_ns_name + '.' + tst_ns_name)] = ""
        file_name = "add_tst_file.abc"
        file_content = "file content"
        file_src_path = os.path.join(module_repo_path, file_name)
        write_file(file_src_path, file_content)
        file_dst_path = os.path.join(module_repo_path, tst_ns_name, file_name)
        tpl_name = OUTSOURCED_FILE_NAME_PREFIX + "add_tpl_file.xyz"
        tpl_content = "tpl content"
        tpl_src_path = os.path.join(module_repo_path, tpl_name)
        write_file(tpl_src_path, tpl_content)
        tpl_dst_path = os.path.join(module_repo_path, tst_ns_name, tpl_name[len(OUTSOURCED_FILE_NAME_PREFIX):])
        mod_pdv = project_dev_vars(project_path=module_repo_path)

        assert not add_children_file(mod_pdv, "not_existing_file.xxx", tst_ns_name, mod_pdv)

        new_pdv = project_dev_vars(project_path=empty_repo_path)
        new_dst_path = os.path.join(empty_repo_path, tst_ns_name, file_name)
        assert not add_children_file(new_pdv, file_src_path, tst_ns_name, new_pdv)  # fail because no tst_ns_name dir

        os.makedirs(os.path.join(empty_repo_path, tst_ns_name))
        assert not os.path.exists(new_dst_path)
        mod_pdv['project_type'] = ROOT_PRJ
        assert add_children_file(mod_pdv, file_src_path, tst_ns_name, new_pdv)
        assert os.path.exists(file_dst_path)
        assert file_content == read_file(file_dst_path)
        assert os.path.exists(new_dst_path)
        assert file_content == read_file(new_dst_path)

        os.remove(file_dst_path)

        assert not os.path.exists(file_dst_path)
        assert not add_children_file(mod_pdv, file_src_path, tst_ns_name, mod_pdv)  # False: added to parent + child
        assert os.path.exists(file_dst_path)
        assert file_content == read_file(file_dst_path)

        assert not os.path.exists(tpl_dst_path)
        assert not add_children_file(mod_pdv, tpl_src_path, tst_ns_name, mod_pdv)
        assert os.path.exists(tpl_dst_path)
        assert tpl_content in read_file(tpl_dst_path)
        assert OUTSOURCED_MARKER in read_file(tpl_dst_path)

    def test_bump_version(self, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = ""        # or tst_ns_name
        mocked_app_options['path'] = module_repo_path
        mocked_app_options['verbose'] = True
        mocked_app_options['versionIncrementPart'] = 3
        import_name = '.'.join(os.path.basename(module_repo_path).split('_', maxsplit=1))
        mocked_app_options[_template_version_option(import_name)] = ""

        version = code_file_version(project_main_file(import_name, project_path=module_repo_path))

        bump_version(project_dev_vars(project_path=module_repo_path))
        assert code_file_version(project_main_file(import_name, project_path=module_repo_path)) > version

    def test_bump_version_disabled(self, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = tst_ns_name   # or ""
        mocked_app_options['path'] = module_repo_path
        mocked_app_options['verbose'] = True
        mocked_app_options['versionIncrementPart'] = 0
        import_name = '.'.join(os.path.basename(module_repo_path).split('_', maxsplit=1))
        mocked_app_options[_template_version_option(import_name)] = ""

        version = code_file_version(project_main_file(import_name, project_path=module_repo_path))

        bump_version(project_dev_vars(project_path=module_repo_path))
        assert code_file_version(project_main_file(import_name, project_path=module_repo_path)) == version

    def test_check_integrity(self, capsys, changed_repo_path, empty_repo_path, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = ""
        mocked_app_options['verbose'] = False

        check_integrity({'project_path': changed_repo_path})
        assert capsys.readouterr().out

        check_integrity({'project_path': empty_repo_path})
        assert capsys.readouterr().out

        check_integrity(project_dev_vars(project_path=changed_repo_path))
        assert capsys.readouterr().out

        check_integrity(project_dev_vars(project_path=empty_repo_path))
        assert capsys.readouterr().out

        check_children_integrity(
            {}, *(project_dev_vars(project_path=_) for _ in (changed_repo_path, empty_repo_path, module_repo_path)))
        assert capsys.readouterr().out

        mocked_app_options['verbose'] = True

        check_integrity({'project_path': changed_repo_path})
        assert capsys.readouterr().out

        check_integrity({'project_path': empty_repo_path})
        assert capsys.readouterr().out

        check_integrity(project_dev_vars(project_path=changed_repo_path))
        assert capsys.readouterr().out

        check_integrity(project_dev_vars(project_path=empty_repo_path))
        assert capsys.readouterr().out

        check_children_integrity(
            {}, *(project_dev_vars(project_path=_) for _ in (changed_repo_path, empty_repo_path, module_repo_path)))
        assert capsys.readouterr().out

    def test_clone_children(self, mocked_app_options):
        package_versions = ("aedev_git_repo_manager==0.2.24", "aedev_setup_project==0.3.4")
        # mocked_app_options['arguments'] = package_versions
        mocked_app_options['branch'] = ""
        mocked_app_options['domain'] = ""
        mocked_app_options['group'] = "aedev-group"
        mocked_app_options['namespace'] = "aedev"
        mocked_app_options['package'] = ""

        with _init_parent() as parent_dir:
            mocked_app_options['path'] = parent_dir
            os.makedirs(parent_dir)
            project_paths = clone_children({'project_path': parent_dir, 'project_type': PARENT_PRJ}, *package_versions)
            for idx, project_path in enumerate(project_paths):
                assert project_path.startswith(parent_dir)
                assert os.path.isdir(project_path)
                import_name = '.'.join(os.path.basename(project_path).split('_', maxsplit=1))
                version = package_versions[idx].split(PACKAGE_VERSION_SEP)[1]
                assert version == code_file_version(project_main_file(import_name, project_path=project_path))

    def test_clone_project(self, mocked_app_options):
        import_name = "aedev.git_repo_manager"
        package_name = norm_name(import_name)
        version = "0.2.24"
        mocked_app_options['branch'] = f"release{version}"
        mocked_app_options['domain'] = ""
        mocked_app_options['group'] = "aedev-group"
        mocked_app_options['namespace'] = ""
        mocked_app_options['package'] = ""
        mocked_app_options['path'] = ""

        with _init_parent() as parent_dir:
            os.makedirs(parent_dir)
            project_path = clone_project({'project_path': parent_dir, 'project_type': PARENT_PRJ}, package_name)
            assert project_path.startswith(parent_dir)
            assert project_path == os.path.join(parent_dir, package_name)
            assert os.path.isdir(project_path)
            assert version == code_file_version(project_main_file(import_name, project_path=project_path))

    def test_clone_project_via_package_option(self, mocked_app_options):
        import_name = "aedev.git_repo_manager"
        package_name = norm_name(import_name)
        version = "0.2.24"
        mocked_app_options['branch'] = "develop"
        mocked_app_options['domain'] = ""
        mocked_app_options['group'] = "aedev-group"
        mocked_app_options['namespace'] = ""
        mocked_app_options['package'] = package_name + PACKAGE_VERSION_SEP + version
        mocked_app_options['path'] = ""

        with _init_parent() as parent_dir:
            os.makedirs(parent_dir)
            project_path = clone_project({'project_path': parent_dir, 'project_type': PARENT_PRJ})
            assert project_path.startswith(parent_dir)
            assert project_path == os.path.join(parent_dir, package_name)
            assert os.path.isdir(project_path)
            assert version == code_file_version(project_main_file(import_name, project_path=project_path))

    def test_commit_children(self, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = ""        # or tst_ns_name
        mocked_app_options['path'] = os.path.dirname(module_repo_path)
        mocked_app_options['verbose'] = True
        mocked_app_options[_template_version_option(tst_ns_name + '.' + tst_ns_name)] = ""
        write_file(os.path.join(module_repo_path, COMMIT_MSG_FILE_NAME), "commit message testing commit_children")
        commit_children({}, project_dev_vars(project_path=module_repo_path))

    def test_commit_project(self, mocked_app_options, module_repo_path):
        mocked_app_options['verbose'] = True
        write_file(os.path.join(module_repo_path, COMMIT_MSG_FILE_NAME), "commit message testing commit_project action")
        commit_project({'project_path': module_repo_path})

    def test_delete_children_file(self, empty_repo_path, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = ""        # or tst_ns_name
        mocked_app_options['path'] = module_repo_path
        mocked_app_options['verbose'] = True
        mocked_app_options[_template_version_option(tst_ns_name + '.' + tst_ns_name)] = ""
        root_pdv = project_dev_vars(project_path=empty_repo_path)
        root_pdv['project_type'] = ROOT_PRJ
        mod_pdv = project_dev_vars(project_path=module_repo_path)

        del_dir = "del_dir"
        os.makedirs(os.path.join(empty_repo_path, del_dir))
        assert os.path.isdir(os.path.join(empty_repo_path, del_dir))
        os.makedirs(os.path.join(module_repo_path, del_dir))
        assert os.path.isdir(os.path.join(module_repo_path, del_dir))
        assert delete_children_file(root_pdv, del_dir, mod_pdv)
        assert not os.path.isdir(os.path.join(empty_repo_path, del_dir))
        assert not os.path.isdir(os.path.join(module_repo_path, del_dir))

        del_file = os.path.join(module_repo_path, tst_ns_name, tst_ns_por_name + PY_EXT)
        assert os.path.exists(del_file)
        assert not delete_children_file(root_pdv, del_file, mod_pdv)
        assert not os.path.exists(del_file)

    def test_install_children_editable(self, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = ""        # or tst_ns_name
        mocked_app_options['path'] = os.path.dirname(module_repo_path)
        mocked_app_options['verbose'] = True
        mocked_app_options[_template_version_option(tst_ns_name + '.' + tst_ns_name)] = ""
        install_children_editable({}, project_dev_vars(project_path=module_repo_path))

    def test_install_editable(self, mocked_app_options, module_repo_path):
        mocked_app_options['verbose'] = True
        install_editable({'project_path': module_repo_path})

    def test_new_app(self, mocked_app_options, module_repo_path):
        mocked_app_options['domain'] = ""
        mocked_app_options['namespace'] = tst_ns_name
        mocked_app_options['path'] = module_repo_path

        pdv = project_dev_vars(project_path=module_repo_path)
        assert os.path.isfile(main_file_path(module_repo_path, MODULE_PRJ, tst_ns_name))
        assert not os.path.isfile(main_file_path(module_repo_path, APP_PRJ, tst_ns_name))

        new_pdv = new_app(pdv)
        assert pdv_str(new_pdv, 'project_type') == APP_PRJ
        assert not os.path.isfile(main_file_path(module_repo_path, MODULE_PRJ, tst_ns_name))
        assert os.path.isfile(main_file_path(module_repo_path, APP_PRJ, tst_ns_name))

    def test_new_children(self, mocked_app_options, module_repo_path):
        mocked_app_options['domain'] = ""
        mocked_app_options['namespace'] = tst_ns_name
        mocked_app_options['package'] = ""
        mocked_app_options['path'] = os.path.dirname(module_repo_path)
        mocked_app_options[_template_version_option(tst_ns_name + '.' + tst_ns_name)] = ""

        parent_pdv = project_dev_vars(project_path=os.path.dirname(module_repo_path))
        module_pdv = project_dev_vars(project_path=module_repo_path)
        assert os.path.isfile(main_file_path(module_repo_path, MODULE_PRJ, tst_ns_name))
        assert not os.path.isfile(main_file_path(module_repo_path, PACKAGE_PRJ, tst_ns_name))

        new_pdv = new_children(parent_pdv, module_pdv)[0]
        assert pdv_str(new_pdv, 'project_type') == MODULE_PRJ
        assert os.path.isfile(main_file_path(module_repo_path, MODULE_PRJ, tst_ns_name))
        assert not os.path.isfile(main_file_path(module_repo_path, PACKAGE_PRJ, tst_ns_name))

    def test_new_django(self, mocked_app_options, module_repo_path):
        mocked_app_options['domain'] = ""
        mocked_app_options['namespace'] = tst_ns_name
        mocked_app_options['path'] = module_repo_path

        pdv = project_dev_vars(project_path=module_repo_path)
        assert not os.path.isfile(main_file_path(module_repo_path, DJANGO_PRJ, tst_ns_name))

        new_pdv = new_django(pdv)
        assert pdv_str(new_pdv, 'project_type') == DJANGO_PRJ
        assert os.path.isfile(main_file_path(module_repo_path, DJANGO_PRJ, tst_ns_name))

    def test_new_django_without_namespace(self, mocked_app_options, empty_repo_path):
        mocked_app_options['domain'] = ""
        mocked_app_options['namespace'] = ""
        mocked_app_options['path'] = empty_repo_path

        pdv = project_dev_vars(project_path=empty_repo_path)
        assert pdv_str(pdv, 'project_type') == NO_PRJ
        assert not os.path.isfile(main_file_path(empty_repo_path, DJANGO_PRJ, ""))

        new_pdv = new_django(pdv)
        assert pdv_str(new_pdv, 'project_type') == DJANGO_PRJ
        assert os.path.isfile(main_file_path(empty_repo_path, DJANGO_PRJ, ""))

    def test_new_module(self, mocked_app_options, module_repo_path):
        mocked_app_options['domain'] = ""
        mocked_app_options['namespace'] = tst_ns_name
        mocked_app_options['path'] = module_repo_path

        pdv = project_dev_vars(project_path=module_repo_path)
        assert os.path.isfile(main_file_path(module_repo_path, MODULE_PRJ, tst_ns_name))
        assert not os.path.isfile(main_file_path(module_repo_path, PACKAGE_PRJ, tst_ns_name))

        new_pdv = new_module(pdv)
        assert pdv_str(new_pdv, 'project_type') == MODULE_PRJ
        assert os.path.isfile(main_file_path(module_repo_path, MODULE_PRJ, tst_ns_name))
        assert not os.path.isfile(main_file_path(module_repo_path, PACKAGE_PRJ, tst_ns_name))

    def test_new_module_from_root(self, mocked_app_options, root_repo_path):
        mocked_app_options['domain'] = ""
        mocked_app_options['namespace'] = tst_ns_name
        mocked_app_options['path'] = root_repo_path

        pdv = project_dev_vars(project_path=root_repo_path)
        assert not os.path.isfile(main_file_path(root_repo_path, MODULE_PRJ, tst_ns_name))
        assert os.path.isfile(main_file_path(root_repo_path, ROOT_PRJ, tst_ns_name))

        new_pdv = new_namespace_root(pdv)
        assert pdv_str(new_pdv, 'project_type') == ROOT_PRJ
        assert not os.path.isfile(main_file_path(root_repo_path, MODULE_PRJ, tst_ns_name))
        assert os.path.isfile(main_file_path(root_repo_path, ROOT_PRJ, tst_ns_name))
        assert os.path.isfile(main_file_path(pdv_str(new_pdv, 'project_path'), ROOT_PRJ, tst_ns_name))

    def test_new_namespace_root(self, mocked_app_options, module_repo_path):
        mocked_app_options['domain'] = ""
        mocked_app_options['namespace'] = tst_ns_name
        mocked_app_options['path'] = module_repo_path

        pdv = project_dev_vars(project_path=module_repo_path)
        assert os.path.isfile(main_file_path(module_repo_path, MODULE_PRJ, tst_ns_name))
        assert not os.path.isfile(main_file_path(module_repo_path, ROOT_PRJ, tst_ns_name))

        new_pdv = new_namespace_root(pdv)
        assert pdv_str(new_pdv, 'project_type') == ROOT_PRJ
        assert os.path.isfile(main_file_path(module_repo_path, MODULE_PRJ, tst_ns_name))
        assert not os.path.isfile(main_file_path(module_repo_path, ROOT_PRJ, tst_ns_name))
        assert os.path.isfile(main_file_path(pdv_str(new_pdv, 'project_path'), ROOT_PRJ, tst_ns_name))

    def test_new_package(self, mocked_app_options, module_repo_path):
        mocked_app_options['domain'] = ""
        mocked_app_options['namespace'] = tst_ns_name
        mocked_app_options['path'] = module_repo_path

        pdv = project_dev_vars(project_path=module_repo_path)
        assert os.path.isfile(main_file_path(module_repo_path, MODULE_PRJ, tst_ns_name))
        assert not os.path.isfile(main_file_path(module_repo_path, PACKAGE_PRJ, tst_ns_name))

        new_pdv = new_package(pdv)
        assert pdv_str(new_pdv, 'project_type') == PACKAGE_PRJ
        assert not os.path.isfile(main_file_path(module_repo_path, MODULE_PRJ, tst_ns_name))
        assert os.path.isfile(main_file_path(module_repo_path, PACKAGE_PRJ, tst_ns_name))

    def test_new_project(self, mocked_app_options, module_repo_path):
        mocked_app_options['domain'] = ""
        mocked_app_options['namespace'] = tst_ns_name
        mocked_app_options['path'] = ""

        pdv = project_dev_vars(project_path=module_repo_path)
        files = set(path_items(os.path.join(module_repo_path, "**")))

        new_pdv = new_project(pdv)
        assert pdv_str(new_pdv, 'project_type') == MODULE_PRJ
        assert files == set(path_items(os.path.join(module_repo_path, "**")))

    def test_prepare_commit(self, changed_repo_path, empty_repo_path, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = ""        # or tst_ns_name
        mocked_app_options['verbose'] = True
        mocked_app_options[_template_version_option(tst_ns_name + '.' + tst_ns_name)] = ""
        title = "commit msg title"

        assert not os.path.isfile(os.path.join(changed_repo_path, COMMIT_MSG_FILE_NAME))
        changed_uncommitted = set(_git_uncommitted({'project_path': changed_repo_path}))

        prepare_commit({'project_path': changed_repo_path}, title)
        assert os.path.isfile(os.path.join(changed_repo_path, COMMIT_MSG_FILE_NAME))
        assert changed_uncommitted == set(_git_uncommitted({'project_path': changed_repo_path}))
        assert title in read_file(os.path.join(changed_repo_path, COMMIT_MSG_FILE_NAME)).split(os.linesep)[0]

        assert not os.path.isfile(os.path.join(empty_repo_path, COMMIT_MSG_FILE_NAME))
        empty_uncommitted = set(_git_uncommitted({'project_path': empty_repo_path}))

        prepare_commit({'project_path': empty_repo_path}, title=title)
        assert os.path.isfile(os.path.join(empty_repo_path, COMMIT_MSG_FILE_NAME))
        assert empty_uncommitted == set(_git_uncommitted({'project_path': empty_repo_path}))
        assert title in read_file(os.path.join(empty_repo_path, COMMIT_MSG_FILE_NAME)).split(os.linesep)[0]

        added = "added_file3.py"
        write_file(os.path.join(changed_repo_path, added), "# added empty python module")
        write_file(os.path.join(empty_repo_path, added), "# added empty python module")

        prepare_children_commit(
            {}, title,
            *(project_dev_vars(project_path=_) for _ in (changed_repo_path, empty_repo_path, module_repo_path)))
        assert added in read_file(os.path.join(changed_repo_path, COMMIT_MSG_FILE_NAME))
        assert set(_git_uncommitted({'project_path': changed_repo_path})) == changed_uncommitted | {added}
        assert added in read_file(os.path.join(empty_repo_path, COMMIT_MSG_FILE_NAME))
        assert set(_git_uncommitted({'project_path': empty_repo_path})) == empty_uncommitted | {added}
        assert title in read_file(os.path.join(changed_repo_path, COMMIT_MSG_FILE_NAME)).split(os.linesep)[0]
        assert title in read_file(os.path.join(empty_repo_path, COMMIT_MSG_FILE_NAME)).split(os.linesep)[0]

    def test_refresh_children_outsourced(self, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = tst_ns_name
        tst_dir = os.path.join(module_repo_path, TESTS_FOLDER)
        assert not os.path.isdir(tst_dir)

        refresh_children_outsourced({}, project_dev_vars(project_path=module_repo_path))
        if on_ci_host():
            assert not os.path.isdir(tst_dir)
        else:
            assert os.path.isdir(tst_dir)  # created via aedev.tpl_project, but not in CI host/server

    def test_rename_children_file(self, empty_repo_path, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = ""        # or tst_ns_name
        mocked_app_options[_template_version_option(tst_ns_name + '.' + tst_ns_name)] = ""
        mocked_app_options['path'] = module_repo_path
        mocked_app_options['verbose'] = True
        root_pdv = project_dev_vars(project_path=empty_repo_path)
        root_pdv['project_type'] = ROOT_PRJ
        mod_pdv = project_dev_vars(project_path=module_repo_path)

        src_file = "ren.efg"
        write_file(os.path.join(empty_repo_path, src_file), "file content")
        write_file(os.path.join(module_repo_path, src_file), "file content")
        dst_file = "renamed.hij"
        assert not os.path.exists(os.path.join(empty_repo_path, dst_file))
        assert not os.path.exists(os.path.join(module_repo_path, dst_file))

        assert rename_children_file(root_pdv, src_file, dst_file, mod_pdv)
        assert not os.path.exists(os.path.join(empty_repo_path, src_file))
        assert os.path.exists(os.path.join(empty_repo_path, dst_file))
        assert not os.path.exists(os.path.join(module_repo_path, src_file))
        assert os.path.exists(os.path.join(module_repo_path, dst_file))

        src_file = os.path.join(tst_ns_name, tst_ns_por_name + PY_EXT)      # rel path move within prj
        dst_file = tst_ns_por_name + PY_EXT
        assert os.path.exists(os.path.join(module_repo_path, src_file))
        assert not os.path.exists(os.path.join(module_repo_path, dst_file))
        assert not rename_children_file(root_pdv, src_file, dst_file, mod_pdv, mod_pdv)  # no src in root / 2*mod rename
        assert not os.path.exists(os.path.join(module_repo_path, src_file))
        assert os.path.exists(os.path.join(module_repo_path, dst_file))

    def test_run_children_command(self, capsys, mocked_app_options):
        mocked_app_options['delay'] = 0
        mocked_app_options['force'] = False
        output = "tst_run_chi_cmd"
        run_children_command({}, f"echo {output}", {'project_path': TESTS_FOLDER}, {'project_path': "."})
        assert capsys.readouterr().out.count(output) == 3   # 2 lines with echo output and final one on action complete

    def test_show_actions(self, capsys, changed_repo_path, empty_repo_path, mocked_app_options):
        mocked_app_options['domain'] = 'gitlab.com'
        pdv = {'remote_repo_api': GitlabCom()}

        mocked_app_options['verbose'] = False
        show_actions(pdv)
        assert capsys.readouterr().out

        mocked_app_options['verbose'] = True
        show_actions(pdv)
        assert capsys.readouterr().out

    def test_show_children_versions(self, capsys, mocked_app_options):
        mocked_app_options['branch'] = ""
        mocked_app_options['domain'] = ""
        mocked_app_options['group'] = "aedev-group"
        mocked_app_options['namespace'] = ""
        mocked_app_options['package'] = ""
        mocked_app_options['path'] = ""

        with _init_parent() as parent_dir:
            os.makedirs(parent_dir)
            root_path = clone_project({'project_path': parent_dir, 'project_type': PARENT_PRJ}, 'aedev_aedev')
            assert os.path.isdir(root_path)
            show_children_versions(project_dev_vars(project_path=root_path), project_dev_vars())
            output = capsys.readouterr().out
            assert 'aedev_git_repo_manager' in output
            assert f"local:{grm_version}" in output

    def test_show_status(self, capsys, changed_repo_path, empty_repo_path, mocked_app_options, module_repo_path):
        mocked_app_options['namespace'] = ""        # or tst_ns_name
        mocked_app_options[_template_version_option(tst_ns_name + '.' + tst_ns_name)] = ""
        mocked_app_options['verbose'] = False

        show_status({'project_path': changed_repo_path})
        assert capsys.readouterr().out

        show_status({'project_path': empty_repo_path})
        assert capsys.readouterr().out

        show_status(project_dev_vars(project_path=changed_repo_path))
        assert capsys.readouterr().out

        show_status(project_dev_vars(project_path=empty_repo_path))
        assert capsys.readouterr().out

        show_children_status(
            *(project_dev_vars(project_path=_) for _ in (changed_repo_path, empty_repo_path, module_repo_path)))
        assert capsys.readouterr().out

        mocked_app_options['verbose'] = True

        show_status({'project_path': changed_repo_path})
        assert capsys.readouterr().out

        show_status({'project_path': empty_repo_path})
        assert capsys.readouterr().out

        show_status(project_dev_vars(project_path=changed_repo_path))
        assert capsys.readouterr().out

        pdv = project_dev_vars(project_path=empty_repo_path)
        _git_checkout(pdv, branch="tst_branch")
        pdv['children_project_vars'] = {}
        pdv['namespace_name'] = tst_ns_name
        pdv['project_type'] = ROOT_PRJ
        show_status(pdv)
        assert capsys.readouterr().out

        show_children_status(
            {'project_path': empty_repo_path, 'project_type': ROOT_PRJ, 'children_project_vars': {}},
            *(project_dev_vars(project_path=_) for _ in (changed_repo_path, empty_repo_path, module_repo_path)))
        assert capsys.readouterr().out

    def test_update_children(self, empty_repo_path, module_repo_path):
        update_children({'project_path': empty_repo_path, 'project_type': ROOT_PRJ}, {'project_path': module_repo_path})

        with patch("aedev.git_repo_manager._git_fetch", return_value=['git fetch error']):
            update_children({}, {'project_path': TESTS_FOLDER})


class TestHelpers:
    """ test public helper functions. """
    def test_activate_venv(self):
        cur_venv = active_venv()
        activate_venv(LOCAL_VENV)
        assert active_venv() == '' if 'CI_PROJECT_ID' in os.environ else LOCAL_VENV
        if cur_venv:
            activate_venv(cur_venv)
            assert active_venv() == cur_venv

    def test_active_venv(self):
        assert not bool(active_venv()) == 'CI_PROJECT_ID' in os.environ      # active_venv()=='' on gitlab CI

    def test_bump_file_version_invalid_file(self):
        err = bump_file_version('::invalid_file_name::')
        assert err

    def test_bump_file_version_empty_file(self):
        tst_file = 'test_bump_version' + PY_EXT
        try:
            write_file(tst_file, "")
            err = bump_file_version(tst_file)
            assert err
        finally:
            if os.path.exists(tst_file):
                os.remove(tst_file)

    def test_bump_file_version_multi_version(self):
        tst_file = 'test_bump_version' + PY_EXT
        try:
            write_file(tst_file, f"__version__ = '1.2.3'{os.linesep}__version__ = '2.3.4'")
            err = bump_file_version(tst_file)
            assert err
        finally:
            if os.path.exists(tst_file):
                os.remove(tst_file)

    def test_bump_file_version_major(self):
        tst_file = 'test_bump_version' + PY_EXT
        try:
            write_file(tst_file, f"__version__ = '1.2.3'{os.linesep}")

            err = bump_file_version(tst_file, increment_part=1)

            assert not err

            content = read_file(tst_file)
            assert "__version__ = '1.2.3'" not in content
            assert "__version__ = '2.2.3'" in content
        finally:
            if os.path.exists(tst_file):
                os.remove(tst_file)

    def test_bump_file_version_minor(self):
        tst_file = 'test_bump_version' + PY_EXT
        try:
            write_file(tst_file, f"__version__ = '1.2.3'{os.linesep}{os.linesep}version = '2.3.4'")

            err = bump_file_version(tst_file, increment_part=2)

            assert not err

            content = read_file(tst_file)
            assert "__version__ = '1.2.3'" not in content
            assert "__version__ = '1.3.3'" in content
        finally:
            if os.path.exists(tst_file):
                os.remove(tst_file)

    def test_bump_file_version_build(self):
        tst_file = 'test_bump_version' + PY_EXT
        try:
            write_file(tst_file, f"__version__ = '1.2.3'{os.linesep}version = '2.3.4'")

            err = bump_file_version(tst_file)

            assert not err

            content = read_file(tst_file)
            assert "__version__ = '1.2.3'" not in content
            assert "__version__ = '1.2.4'" in content
        finally:
            if os.path.exists(tst_file):
                os.remove(tst_file)

    def test_bump_file_version_remove_suffix(self):
        tst_file = 'test_bump_version' + PY_EXT
        try:
            write_file(tst_file, f"__version__ = '1.2.3pre'{os.linesep}version = '2.3.4'")

            err = bump_file_version(tst_file, increment_part=2)

            assert not err

            content = read_file(tst_file)
            assert "__version__ = '1.2.3'" not in content
            assert "__version__ = '1.3.3'" in content
        finally:
            if os.path.exists(tst_file):
                os.remove(tst_file)

    def test_bump_file_version_keeping_comment(self):
        tst_file = 'test_bump_version' + PY_EXT
        comment_str = "  # comment string"
        try:
            write_file(tst_file, f"__version__ = '1.2.3'{comment_str}{os.linesep}version = '2.3.4'")

            err = bump_file_version(tst_file, increment_part=1)

            assert not err

            content = read_file(tst_file)
            assert f"__version__ = '2.2.3'{comment_str}" in content
        finally:
            if os.path.exists(tst_file):
                os.remove(tst_file)

    def test_declaration_of_template_vars(self):
        assert OUTSOURCED_FILE_NAME_PREFIX
        assert OUTSOURCED_MARKER
        assert TPL_FILE_NAME_PREFIX
        assert TEMPLATE_PLACEHOLDER_ID_PREFIX
        assert TEMPLATE_PLACEHOLDER_ID_SUFFIX
        assert TEMPLATE_PLACEHOLDER_ARGS_SUFFIX
        assert TEMPLATE_INCLUDE_FILE_PLACEHOLDER_ID

    def test_deploy_template_dst_files_not_passed(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        src_dir = join(parent_dir, 'tpl_src_prj_dir')
        tpl_dir = join(src_dir, TEMPLATES_FOLDER)
        file_name = 'template.extension'
        src_file = join(tpl_dir, file_name)
        content = "template file content"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir}

        try:
            os.makedirs(tpl_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)

            deploy_template(src_file, "", "", new_pdv)

            dst_file = join(dst_dir, file_name)
            assert os.path.isfile(dst_file)
            assert read_file(dst_file) == content

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_logged_state(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        src_dir = join(parent_dir, 'tpl_src_prj_dir')
        tpl_dir = join(src_dir, TEMPLATES_FOLDER)
        content = "logged template file content"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir}
        log_prefix = "    - "

        try:
            os.makedirs(tpl_dir)
            os.makedirs(dst_dir)
            logged = []

            file_name = "template_state_log.ext"
            src_file = join(tpl_dir, file_name)
            write_file(src_file, content)

            deploy_template(src_file, "", "", new_pdv, logger=lambda *_: logged.extend(arg for arg in _))
            assert logged[-1].startswith(log_prefix + "refresh")

            deploy_template(src_file, "", "", new_pdv, logger=lambda *_: logged.extend(arg for arg in _))
            assert logged[-1].startswith(log_prefix + "binary-exists-skip")

            lock_file = join(dst_dir, file_name + LOCK_EXT)
            write_file(lock_file, "")
            deploy_template(src_file, "", "", new_pdv, logger=lambda *_: logged.extend(arg for arg in _))
            assert logged[-1].startswith(log_prefix + "lock-extension-skip")
            os.remove(lock_file)

            src_file = join(tpl_dir, TPL_FILE_NAME_PREFIX + "template_state_log.ext")
            write_file(src_file, content)

            deploy_template(src_file, "", "", new_pdv, logger=lambda *_: logged.extend(arg for arg in _))
            assert logged[-1].startswith(log_prefix + "unchanged-skip")

            src_file = join(tpl_dir, TPL_FILE_NAME_PREFIX + "template_state_log.ext")
            write_file(src_file, content + " extended")

            deploy_template(src_file, "", "", new_pdv, logger=lambda *_: logged.extend(arg for arg in _))
            assert logged[-1].startswith(log_prefix + "missing-outsourced-marker-skip")

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_otf_in_sub_dir(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        prj_dir = join(parent_dir, 'prj_with_otf_tpl')
        tpl_dir = join(prj_dir, TEMPLATES_FOLDER)
        sub_dir_folder = 'sub_dir'
        tpl_sub_dir = join(tpl_dir, sub_dir_folder)
        file_name = OUTSOURCED_FILE_NAME_PREFIX + 'changed_template' + PY_EXT
        src_file = join(tpl_sub_dir, file_name)
        content = "# template file content"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir}
        patcher = "patching package id to be added to patched destination file"

        dst_files = set()
        try:
            os.makedirs(tpl_sub_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)

            deploy_template(src_file, sub_dir_folder, patcher, new_pdv, dst_files=dst_files)

            dst_file = join(dst_dir, sub_dir_folder, file_name[len(OUTSOURCED_FILE_NAME_PREFIX):])
            assert os.path.isfile(dst_file)
            assert OUTSOURCED_MARKER in read_file(dst_file)
            assert patcher in read_file(dst_file)
            assert read_file(dst_file).endswith(content)
            assert norm_path(dst_file) in dst_files

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_tpl_in_sub_dir(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        prj_dir = join(parent_dir, 'tpl_src_root_dir')
        tpl_dir = join(prj_dir, TEMPLATES_FOLDER)
        sub_dir_folder = 'sub_dir'
        tpl_sub_dir = join(tpl_dir, sub_dir_folder)
        file_name = TPL_FILE_NAME_PREFIX + 'changed_template' + PY_EXT
        src_file = join(tpl_sub_dir, file_name)
        content = "# template file content created in {project_path}"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir}
        patcher = "patching package id here not added to patched destination file"

        dst_files = set()
        try:
            os.makedirs(tpl_sub_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)

            deploy_template(src_file, sub_dir_folder, patcher, new_pdv, dst_files=dst_files)

            dst_file = join(dst_dir, sub_dir_folder, file_name[len(TPL_FILE_NAME_PREFIX):])
            assert os.path.isfile(dst_file)
            assert OUTSOURCED_MARKER not in read_file(dst_file)
            assert patcher not in read_file(dst_file)
            assert read_file(dst_file).endswith(content.format(project_path=dst_dir))
            assert norm_path(dst_file) in dst_files

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_otf_tpl_in_sub_dir(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        prj_dir = join(parent_dir, 'prj_root_dir')
        tpl_dir = join(prj_dir, TEMPLATES_FOLDER)
        sub_dir_folder = 'sub_dir'
        tpl_sub_dir = join(tpl_dir, sub_dir_folder)
        file_name = OUTSOURCED_FILE_NAME_PREFIX + TPL_FILE_NAME_PREFIX + 'changed_template' + PY_EXT
        src_file = join(tpl_sub_dir, file_name)
        content = "# template file content created in {project_path}"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir}
        patcher = "patching project name to be added to destination file"

        dst_files = set()
        try:
            os.makedirs(tpl_sub_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)

            deploy_template(src_file, sub_dir_folder, patcher, new_pdv, dst_files=dst_files)

            dst_file = join(dst_dir, sub_dir_folder,
                            file_name[len(OUTSOURCED_FILE_NAME_PREFIX) + len(TPL_FILE_NAME_PREFIX):])
            assert os.path.isfile(dst_file)
            assert OUTSOURCED_MARKER in read_file(dst_file)
            assert patcher in read_file(dst_file)
            assert read_file(dst_file).endswith(content.format(project_path=dst_dir))
            assert norm_path(dst_file) in dst_files

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_otf_stop_tpl(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        prj_dir = join(parent_dir, 'prj_root_dir')
        tpl_dir = join(prj_dir, TEMPLATES_FOLDER)
        sub_dir_folder = TEMPLATES_FOLDER
        tpl_sub_dir = join(tpl_dir, sub_dir_folder)
        file_name = OUTSOURCED_FILE_NAME_PREFIX + TPL_STOP_CNV_PREFIX + TPL_FILE_NAME_PREFIX + 'chg_template' + PY_EXT
        src_file = join(tpl_sub_dir, file_name)
        content = "# template file content created in {project_path}"
        dst_dir = join(parent_dir, 'dst', TEMPLATES_FOLDER)
        new_pdv = {'project_path': dst_dir}
        patcher = "patcher"

        dst_files = set()
        try:
            os.makedirs(tpl_sub_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)

            deploy_template(src_file, sub_dir_folder, patcher, new_pdv, dst_files=dst_files)

            dst_file = join(dst_dir, sub_dir_folder,
                            file_name[len(OUTSOURCED_FILE_NAME_PREFIX) + len(TPL_STOP_CNV_PREFIX):])
            assert os.path.isfile(dst_file)
            assert OUTSOURCED_MARKER in read_file(dst_file)
            assert patcher in read_file(dst_file)
            assert read_file(dst_file).endswith(content)
            assert norm_path(dst_file) in dst_files

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_otf_existing_unlocked_because_marker(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        prj_dir = join(parent_dir, 'project_dir')
        tpl_dir = join(prj_dir, TEMPLATES_FOLDER)
        file_name = OUTSOURCED_FILE_NAME_PREFIX + 'unlocked_template' + PY_EXT
        src_file = join(tpl_dir, file_name)
        content = f"# template file extra content"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir}
        patcher = "patching package id to be added to destination file"

        dst_files = set()
        try:
            os.makedirs(tpl_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)
            dst_file = join(dst_dir, file_name[len(OUTSOURCED_FILE_NAME_PREFIX):])
            write_file(dst_file, OUTSOURCED_MARKER)

            deploy_template(src_file, "", patcher, new_pdv, dst_files=dst_files)

            assert os.path.isfile(dst_file)
            assert OUTSOURCED_MARKER in read_file(dst_file)
            assert content in read_file(dst_file)
            assert patcher in read_file(dst_file)
            assert norm_path(dst_file) in dst_files

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_otf_existing_locked_without_marker(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        prj_dir = join(parent_dir, 'prj_root')
        tpl_dir = join(prj_dir, TEMPLATES_FOLDER)
        file_name = OUTSOURCED_FILE_NAME_PREFIX + 'locked_template' + PY_EXT
        src_file = join(tpl_dir, file_name)
        content = "# template file content"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir}
        patcher = "patcher id or package name"

        dst_files = set()
        try:
            os.makedirs(tpl_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)
            dst_file = join(dst_dir, file_name[len(OUTSOURCED_FILE_NAME_PREFIX):])
            dst_content = "locked because not contains marker"
            write_file(dst_file, dst_content)

            deploy_template(src_file, "", patcher, new_pdv, dst_files=dst_files)

            assert os.path.isfile(dst_file)
            assert OUTSOURCED_MARKER not in read_file(dst_file)
            assert read_file(dst_file) == dst_content
            assert patcher not in read_file(dst_file)
            assert norm_path(dst_file) in dst_files

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_otf_locked_by_file(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        prj_dir = join(parent_dir, 'prj_dir')
        tpl_dir = join(prj_dir, TEMPLATES_FOLDER)
        file_name = OUTSOURCED_FILE_NAME_PREFIX + 'locked_template' + PY_EXT
        src_file = join(tpl_dir, file_name)
        content = "# template file content"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir}

        dst_files = set()
        try:
            os.makedirs(tpl_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)
            dst_file = join(dst_dir, file_name[len(OUTSOURCED_FILE_NAME_PREFIX):])
            write_file(dst_file + '.locked', "")

            deploy_template(src_file, "", "", new_pdv, dst_files=dst_files)

            assert not os.path.isfile(dst_file)
            assert norm_path(dst_file) in dst_files

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_stp_in_sub_dir(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        src_dir = join(parent_dir, 'tpl_src_prj_dir')
        tpl_dir = join(src_dir, TEMPLATES_FOLDER)
        sub_dir_folder = 'sub_dir'
        tpl_sub_dir = join(tpl_dir, sub_dir_folder)
        file_name = SKIP_PRJ_TYPE_FILE_NAME_PREFIX + ROOT_PRJ + "_template_file_name.eee"
        src_file = join(tpl_sub_dir, file_name)
        content = "template file content"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir, 'project_type': ROOT_PRJ}

        dst_files = set()
        try:
            os.makedirs(tpl_sub_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)

            deploy_template(src_file, sub_dir_folder, "", new_pdv, dst_files=dst_files)
            assert not dst_files        # skipped deploy

            new_pdv['project_type'] = MODULE_PRJ
            deploy_template(src_file, sub_dir_folder, "", new_pdv, dst_files=dst_files)
            assert dst_files            # not skipped deploy
            dst_file = join(dst_dir, sub_dir_folder,
                            file_name[len(SKIP_PRJ_TYPE_FILE_NAME_PREFIX) + len(ROOT_PRJ) + 1:])
            assert os.path.isfile(dst_file)
            assert read_file(dst_file) == content
            assert norm_path(dst_file) in dst_files

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_tpl_locked_by_priority(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        prj_dir = join(parent_dir, 'prj')
        tpl_dir = join(prj_dir, TEMPLATES_FOLDER)
        file_name = TPL_FILE_NAME_PREFIX + 'template' + PY_EXT
        src_file = join(tpl_dir, file_name)
        content = "# template file content"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir}

        dst_files = set()
        try:
            os.makedirs(tpl_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)
            dst_file = join(dst_dir, file_name[len(TPL_FILE_NAME_PREFIX):])

            deploy_template(src_file, "", "", new_pdv, dst_files=dst_files)

            assert os.path.isfile(dst_file)
            assert read_file(dst_file) == content
            assert norm_path(dst_file) in dst_files
            dst_files_len = len(dst_files)

            write_file(src_file, 'any OTHER content')

            # second deploy try from tpl prj with lower priority
            deploy_template(src_file, "", "", new_pdv, dst_files=dst_files)

            assert os.path.isfile(dst_file)
            assert read_file(dst_file) == content
            assert dst_files_len == len(dst_files)

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_deploy_template_unchanged_in_sub_dir(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        src_dir = join(parent_dir, 'tpl_src_prj_dir')
        tpl_dir = join(src_dir, TEMPLATES_FOLDER)
        sub_dir_folder = 'sub_dir'
        tpl_sub_dir = join(tpl_dir, sub_dir_folder)
        file_name = 'unchanged_template.ext'
        src_file = join(tpl_sub_dir, file_name)
        content = "template file content"
        dst_dir = join(parent_dir, 'dst')
        new_pdv = {'project_path': dst_dir}

        dst_files = set()
        try:
            os.makedirs(tpl_sub_dir)
            write_file(src_file, content)
            os.makedirs(dst_dir)

            deploy_template(src_file, sub_dir_folder, "", new_pdv, dst_files=dst_files)

            dst_file = join(dst_dir, sub_dir_folder, file_name)
            assert os.path.isfile(dst_file)
            assert read_file(dst_file) == content
            assert norm_path(dst_file) in dst_files

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_editable_project_path(self):
        pkg_name = "tst_pkg_editable"
        egg_link_file = pkg_name + '.egg-link'

        try:
            sys.path.insert(0, TESTS_FOLDER)
            write_file(egg_link_file, TESTS_FOLDER)

            assert editable_project_path(pkg_name) == TESTS_FOLDER

        finally:
            if sys.path[0] == TESTS_FOLDER:
                sys.path.pop(0)
            if os.path.exists(egg_link_file):
                os.remove(egg_link_file)

    def test_find_extra_modules(self):
        assert 'setup' in find_extra_modules("")
        assert 'setup' in find_extra_modules(os.getcwd())
        assert 'aedev.git_repo_manager' in find_extra_modules("")
        assert 'tests.test_git_repo_manager' in find_extra_modules("")
        assert 'git_repo_manager' in find_extra_modules(os.path.join(os.getcwd(), 'aedev'))
        assert find_extra_modules(os.getcwd(), 'aedev') == ['git_repo_manager']
        assert find_extra_modules(os.getcwd(), 'aedev', 'git_repo_manager') == []

        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        tst_mod = "tst_mod"
        try:
            os.makedirs(parent_dir)
            write_file(os.path.join(parent_dir, PY_INIT), "# test __init__.py")
            write_file(os.path.join(parent_dir, tst_mod + PY_EXT), "# test module")

            assert 'setup' in find_extra_modules("")
            assert 'aedev.git_repo_manager' in find_extra_modules("")
            assert find_extra_modules(parent_dir) == [tst_mod]
            assert find_extra_modules(TESTS_FOLDER, PARENT_FOLDERS[0]) == [tst_mod]
            assert find_extra_modules(TESTS_FOLDER, portion_name=PARENT_FOLDERS[0]) == [tst_mod]
        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_increment_version(self):
        assert increment_version("") == ""
        assert increment_version("0.0.1") == "0.0.2"
        assert increment_version("9999.9999.9999") == "9999.9999.10000"

        assert increment_version(NULL_VERSION, increment_part=0) == NULL_VERSION
        assert increment_version(NULL_VERSION, increment_part=1) == "1.0.0"
        assert increment_version(NULL_VERSION, increment_part=2) == "0.1.0"
        # noinspection PyArgumentEqualDefault
        assert increment_version(NULL_VERSION, increment_part=3) == "0.0.1"

        assert increment_version(("0", "1", "2"), increment_part=0) == "0.1.2"
        assert increment_version(("0", "1", "2"), increment_part=1) == "1.1.2"
        assert increment_version(("0", "1", "2"), increment_part=2) == "0.2.2"
        assert increment_version(("0", "1", "2")) == "0.1.3"

        assert increment_version(NULL_VERSION, increment_part=-1) == NULL_VERSION
        assert increment_version(NULL_VERSION, increment_part=4) == NULL_VERSION

    def test_in_venv(self):
        cur_venv = active_venv()
        with in_venv(LOCAL_VENV):
            assert active_venv() == '' if 'CI_PROJECT_ID' in os.environ else LOCAL_VENV
        assert active_venv() == cur_venv

    def test_next_package_version(self, empty_repo_path, mocked_app_options):
        mocked_app_options['namespace'] = ""
        assert next_package_version({'project_path': TESTS_FOLDER}) == ""
        assert next_package_version({'project_path': empty_repo_path}) == "0.0.1"

    def test_next_package_version_local(self, empty_repo_path, mocked_app_options):
        mocked_app_options['namespace'] = ""
        assert next_package_version(project_dev_vars()) > "0.0.1"

    @skip_gitlab_ci
    def test_on_ci_host_local(self):
        assert not on_ci_host()

    def test_on_ci_host_on_gitlab(self):
        assert on_ci_host() == ('CI_PROJECT_ID' in os.environ)

    def test_package_version(self):
        pkg, ver = package_version('a.b', ['a_b', 'b_c'])
        assert pkg == 'a_b'
        assert ver == ""

        pkg, ver = package_version('a.b', ['bc', 'c_d', 'a_b==1.2.3'])
        assert pkg == 'a_b'
        assert ver == "1.2.3"

    def test_patch_string_empty_args(self):
        assert patch_string("", {}, invalid_place_holder_id=lambda s: "") == ""

    def test_project_dev_vars_not_exists(self, mocked_app_options):
        mocked_app_options['group'] = ""
        mocked_app_options['namespace'] = ""
        new_pkg_name = "new_pkg_nam"
        new_prj_path = os.path.join(os.path.dirname(os.getcwd()), new_pkg_name)

        pdv = project_dev_vars(project_path=new_prj_path)
        assert 'namespace_name' in pdv
        assert pdv['namespace_name'] == ''
        assert 'package_name' in pdv
        assert pdv['package_name'] == new_pkg_name
        assert 'project_path' in pdv
        assert pdv['project_path'] == new_prj_path
        assert 'project_type' in pdv
        assert pdv['project_type'] == NO_PRJ
        assert pdv['repo_group'].startswith(new_pkg_name)

        nsn = 'nsn'
        mocked_app_options['namespace'] = nsn
        new_pkg_name = nsn + '_' + new_pkg_name
        new_prj_path = os.path.join(os.path.dirname(os.getcwd()), new_pkg_name)
        pdv = project_dev_vars(project_path=new_prj_path)
        assert pdv['namespace_name'] == nsn
        assert pdv['package_name'] == new_pkg_name
        assert pdv['project_path'] == new_prj_path
        assert pdv['project_type'] == NO_PRJ
        assert pdv['repo_group'].startswith(new_pkg_name)

        mocked_app_options['group'] = "tst-group"
        pdv = project_dev_vars(project_path=new_prj_path)
        assert pdv['namespace_name'] == nsn
        assert pdv['package_name'] == new_pkg_name
        assert pdv['project_path'] == new_prj_path
        assert pdv['project_type'] == NO_PRJ
        assert pdv['repo_group'] == mocked_app_options['group']

    def test_project_dev_vars_namespace_module(self, mocked_app_options):
        mocked_app_options['group'] = "tst_grp"
        mocked_app_options['namespace'] = ""
        nsn = 'abc'
        mocked_app_options[_template_version_option(nsn + '.' + nsn)] = ""

        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        portion_name = 'tst_ns_mod'
        project_path = os.path.join(parent_dir, nsn + '_' + portion_name)
        module_path = os.path.join(project_path, nsn, portion_name + PY_EXT)

        try:
            os.makedirs(os.path.dirname(module_path))
            write_file(module_path, "mod_content = ''\n__version__ = '3.3.3'\n")

            pdv = project_dev_vars(project_path=project_path)
            assert pdv['namespace_name'] == nsn
            assert pdv['package_name'] == nsn + '_' + portion_name
            assert pdv['package_path'] == os.path.join(norm_path(project_path), nsn, portion_name)
            assert pdv['project_path'] == norm_path(project_path)
            assert pdv['project_type'] == MODULE_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert pdv['version_file'] == norm_path(module_path)

            pdv = project_dev_vars(project_path=parent_dir)
            assert pdv['namespace_name'] == ""
            assert pdv['package_name'] == os.path.basename(parent_dir)
            assert pdv['project_path'] == norm_path(parent_dir)
            assert pdv['project_type'] == PARENT_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert f"{nsn}_{portion_name}" in pdv['children_project_vars']
            assert 'portions_import_names' not in pdv

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_project_dev_vars_namespace_package(self, mocked_app_options):
        mocked_app_options['group'] = "tst_grp"
        mocked_app_options['namespace'] = ""
        nsn = 'efg'
        mocked_app_options[_template_version_option(nsn + '.' + nsn)] = ""

        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        portion_name = 'tst_ns_pkg'
        project_path = os.path.join(parent_dir, nsn + '_' + portion_name)
        package_path = os.path.join(project_path, nsn, portion_name)

        try:
            os.makedirs(package_path)
            write_file(os.path.join(package_path, PY_INIT), "pkg_ini_content = ''\n__version__ = '6.3.6'\n")

            pdv = project_dev_vars(project_path=project_path)
            assert pdv['namespace_name'] == nsn
            assert pdv['package_name'] == nsn + '_' + portion_name
            assert pdv['package_path'] == norm_path(package_path)
            assert pdv['project_path'] == norm_path(project_path)
            assert pdv['project_type'] == PACKAGE_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert pdv['version_file'] == norm_path(os.path.join(package_path, PY_INIT))

            mocked_app_options['namespace'] = nsn
            pdv = project_dev_vars(project_path=project_path)
            assert pdv['namespace_name'] == nsn
            assert pdv['package_name'] == nsn + '_' + portion_name
            assert pdv['package_path'] == norm_path(package_path)
            assert pdv['project_path'] == norm_path(project_path)
            assert pdv['project_type'] == PACKAGE_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert pdv['version_file'] == norm_path(os.path.join(package_path, PY_INIT))

            mocked_app_options['namespace'] = ""
            pdv = project_dev_vars(project_path=parent_dir)
            assert pdv['namespace_name'] == ""
            assert pdv['package_name'] == os.path.basename(parent_dir)
            assert pdv['project_path'] == norm_path(parent_dir)
            assert pdv['project_type'] == PARENT_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert f"{nsn}_{portion_name}" in pdv['children_project_vars']
            assert 'portions_import_names' not in pdv

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_project_dev_vars_namespace_root(self, mocked_app_options):
        mocked_app_options['group'] = "tst_grp"
        mocked_app_options['namespace'] = ""
        nsn = 'hij'
        mocked_app_options[_template_version_option(nsn + '.' + nsn)] = ""

        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        project_path = os.path.join(parent_dir, nsn + '_' + nsn)
        package_path = os.path.join(project_path, nsn, nsn)

        try:
            os.makedirs(package_path)
            write_file(os.path.join(package_path, PY_INIT), "root_content = ''\n__version__ = '9.9.3'\n")

            pdv = project_dev_vars(project_path=project_path)
            assert pdv['namespace_name'] == nsn
            assert pdv['package_name'] == nsn + '_' + nsn
            assert pdv['package_path'] == norm_path(package_path)
            assert pdv['project_path'] == norm_path(project_path)
            assert pdv['project_type'] == ROOT_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert pdv['version_file'] == norm_path(os.path.join(package_path, PY_INIT))
            assert not pdv['children_project_vars']
            assert not pdv['portions_import_names']

            mocked_app_options['namespace'] = nsn
            pdv = project_dev_vars(project_path=project_path)
            assert pdv['namespace_name'] == nsn
            assert pdv['package_name'] == nsn + '_' + nsn
            assert pdv['package_path'] == norm_path(package_path)
            assert pdv['project_path'] == norm_path(project_path)
            assert pdv['project_type'] == ROOT_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert pdv['version_file'] == norm_path(os.path.join(package_path, PY_INIT))
            assert not pdv['children_project_vars']
            assert not pdv['portions_import_names']

            mocked_app_options['namespace'] = ""
            pdv = project_dev_vars(project_path=parent_dir)
            assert pdv['namespace_name'] == ""
            assert pdv['package_name'] == os.path.basename(parent_dir)
            assert pdv['project_path'] == norm_path(parent_dir)
            assert pdv['project_type'] == PARENT_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert f"{nsn}_{nsn}" in pdv['children_project_vars']
            assert 'portions_import_names' not in pdv

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_project_dev_vars_namespace_root_portions(self, mocked_app_options):
        mocked_app_options['group'] = "tst_grp"
        mocked_app_options['namespace'] = ""
        nsn = 'uvw'
        mocked_app_options[_template_version_option(nsn + '.' + nsn)] = ""

        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        root_prj_path = os.path.join(parent_dir, nsn + '_' + nsn)
        root_pkg_path = os.path.join(root_prj_path, nsn, nsn)

        package_name = 'tst_ns_pkg'
        package_prj_path = os.path.join(parent_dir, nsn + '_' + package_name)
        package_pkg_path = os.path.join(package_prj_path, nsn, package_name)
        package_extra_module_name = "extra_module_name"

        module_name = 'tst_ns_module'
        module_prj_path = os.path.join(parent_dir, nsn + '_' + module_name)
        module_path = os.path.join(module_prj_path, nsn, module_name + PY_EXT)

        try:
            os.makedirs(root_pkg_path)
            write_file(os.path.join(root_pkg_path, PY_INIT), "root_content = ''\n__version__ = '111.33.63'\n")
            write_file(os.path.join(root_prj_path, REQ_DEV_FILE_NAME),
                       nsn + '_' + package_name + os.linesep + nsn + '_' + module_name)

            os.makedirs(package_pkg_path)
            write_file(os.path.join(package_pkg_path, PY_INIT), "pkg_content = ''\n__version__ = '999.333.636'\n")
            write_file(os.path.join(package_pkg_path, package_extra_module_name + PY_EXT), "extra_content = ''")

            os.makedirs(os.path.dirname(module_path))
            write_file(os.path.join(module_prj_path, nsn, module_name), "mod_content = ''\n__version__ = '6.9.699'\n")

            pdv = project_dev_vars(project_path=root_prj_path)
            assert pdv['namespace_name'] == nsn
            assert pdv['package_name'] == nsn + '_' + nsn
            assert pdv['package_path'] == norm_path(root_pkg_path)
            assert pdv['project_path'] == norm_path(root_prj_path)
            assert pdv['project_type'] == ROOT_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert pdv['version_file'] == norm_path(os.path.join(root_pkg_path, PY_INIT))

            assert f"{nsn}_{package_name}" in pdv['children_project_vars']
            assert f"{nsn}_{package_name}.{package_extra_module_name}" not in pdv['children_project_vars']
            assert f"{nsn}_{module_name}" in pdv['children_project_vars']

            assert f"{nsn}.{package_name}" in pdv['portions_import_names']
            assert f"{nsn}.{package_name}.{package_extra_module_name}" in pdv['portions_import_names']
            assert f"{nsn}.{module_name}" in pdv['portions_import_names']

            mocked_app_options['namespace'] = nsn
            pdv = project_dev_vars(project_path=root_prj_path)
            assert pdv['namespace_name'] == nsn
            assert pdv['package_name'] == nsn + '_' + nsn
            assert pdv['package_path'] == os.path.join(norm_path(root_prj_path), nsn, nsn)
            assert pdv['project_path'] == norm_path(root_prj_path)
            assert pdv['project_type'] == ROOT_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert pdv['version_file'] == norm_path(os.path.join(root_pkg_path, PY_INIT))

            assert f"{nsn}_{package_name}" in pdv['children_project_vars']
            assert f"{nsn}_{package_name}.{package_extra_module_name}" not in pdv['children_project_vars']
            assert f"{nsn}_{module_name}" in pdv['children_project_vars']

            assert f"{nsn}.{package_name}" in pdv['portions_import_names']
            assert f"{nsn}.{package_name}.{package_extra_module_name}" in pdv['portions_import_names']
            assert f"{nsn}.{module_name}" in pdv['portions_import_names']

            mocked_app_options['namespace'] = ""
            pdv = project_dev_vars(project_path=parent_dir)
            assert pdv['namespace_name'] == ""
            assert pdv['package_name'] == os.path.basename(parent_dir)
            assert pdv['project_path'] == norm_path(parent_dir)
            assert pdv['project_type'] == PARENT_PRJ
            assert pdv['repo_group'] == mocked_app_options['group']
            assert f"{nsn}_{package_name}" in pdv['children_project_vars']
            assert f"{nsn}_{package_name}.{package_extra_module_name}" not in pdv['children_project_vars']
            assert f"{nsn}_{module_name}" in pdv['children_project_vars']
            assert 'portions_import_names' not in pdv

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_pypi_versions(self):
        assert pypi_versions("") == [""]
        assert "0.1.17" in pypi_versions("ae_base")
        assert "0.2.24" in pypi_versions("aedev_git_repo_manager")

    def test_refresh_templates_empty_args(self):
        assert refresh_templates({}) == set()

    def test_refresh_templates_test_registered(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        namespace = "nsn"
        package_name = f"{namespace}_pkg_name"
        project_path = norm_path(join(parent_dir, package_name))
        tpl_projects = [
            {'import_name': namespace + '.' + namespace,
             'tpl_path': join(parent_dir, namespace + '_' + namespace, namespace, namespace, TEMPLATES_FOLDER),
             'version': '1.1.1'},
            {'import_name': TPL_IMPORT_NAME_PREFIX + 'package',
             'tpl_path': join(parent_dir, 'aedev_tpl_package', 'aedev', 'tpl_package', TEMPLATES_FOLDER),
             'version': '3.3.3'},
            {'import_name': TPL_IMPORT_NAME_PREFIX + 'project',
             'tpl_path': join(parent_dir, 'aedev_tpl_project', 'aedev', 'tpl_project', TEMPLATES_FOLDER),
             'version': '9.9.9'},
        ]

        try:
            pdv = {'namespace_name': namespace, 'package_name': package_name,
                   'project_path': project_path, 'project_type': PACKAGE_PRJ}
            _renew_prj_dir(pdv)

            assert not refresh_templates({'project_path': project_path})   # first test w/o created template folders

            assert os.path.isdir(project_path)
            assert not os.path.isdir(join(project_path, DOCS_FOLDER))
            assert not os.path.isdir(join(project_path, TEMPLATES_FOLDER))
            assert os.path.isdir(join(project_path, TESTS_FOLDER))
            assert os.path.isfile(join(project_path, namespace, package_name[len(namespace) + 1:], PY_INIT))
            assert not os.path.isfile(join(project_path, BUILD_CONFIG_FILE))

            deep_sub_dir = join('deeper', 'even_deeper')
            file_for_all = 'file_for_all.ext'
            tpl_file_for_all = OUTSOURCED_FILE_NAME_PREFIX + TPL_FILE_NAME_PREFIX + file_for_all
            for tpl_reg in tpl_projects:
                tpl_path = join(tpl_reg['tpl_path'], deep_sub_dir)
                os.makedirs(tpl_path)
                write_file(join(tpl_path, tpl_file_for_all), tpl_reg['tpl_path'])
            tpl_file = join(project_path, deep_sub_dir, file_for_all)

            # 2nd test with template in all templates (root prj has the highest priority)
            pdv = {'namespace_name': namespace, 'project_path': project_path, 'project_type': PACKAGE_PRJ,
                   'TEMPLATES_FOLDER': TEMPLATES_FOLDER, 'tpl_projects': tpl_projects}
            assert refresh_templates(pdv) == {norm_path(tpl_file)}

            assert os.path.isfile(tpl_file)
            content = read_file(tpl_file)
            assert tpl_projects[0]['tpl_path'] in content
            assert OUTSOURCED_MARKER in content

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_refresh_templates_file_include_content(self):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[-1])
        tpl_pkg_path = norm_path(join(parent_dir, 'tpl_prj'), TEMPLATES_FOLDER)
        tpl_file_name = "including_content.txt"
        tpl_file_path = join(tpl_pkg_path, OUTSOURCED_FILE_NAME_PREFIX + TPL_FILE_NAME_PREFIX + tpl_file_name)
        ver = '9.6.9999'
        tpl_projects = [{'import_name': TPL_IMPORT_NAME_PREFIX + 'project', 'tpl_path': tpl_pkg_path, 'version': ver}]
        included_file_name = norm_path(join(parent_dir, "inc.tst.file"))
        included_file_content = "replacement string"
        package_name = f"pkg_name"
        project_path = join(parent_dir, package_name)
        patched_file_name = join(project_path, tpl_file_name)
        cur_dir = os.getcwd()
        try:
            os.makedirs(project_path)
            os.makedirs(tpl_pkg_path)

            tpl = f"{TEMPLATE_PLACEHOLDER_ID_PREFIX}{TEMPLATE_INCLUDE_FILE_PLACEHOLDER_ID}"
            tpl += f"{TEMPLATE_PLACEHOLDER_ID_SUFFIX}{included_file_name}{TEMPLATE_PLACEHOLDER_ARGS_SUFFIX}"
            write_file(tpl_file_path, tpl)
            write_file(included_file_name, included_file_content)

            os.chdir(project_path)
            patched = refresh_templates({'TEMPLATES_FOLDER': "", 'tpl_projects': tpl_projects})
            os.chdir(cur_dir)

            assert patched == {norm_path(patched_file_name)}

            content = read_file(patched_file_name)
            assert included_file_content in content
            assert ver in content
            assert "TEMPLATE_PLACEHOLDER_ID_PREFIX" not in content
            assert TEMPLATE_INCLUDE_FILE_PLACEHOLDER_ID not in content
            assert "TEMPLATE_INCLUDE_FILE_PLACEHOLDER_ID" not in content
            assert TEMPLATE_PLACEHOLDER_ID_SUFFIX not in content
            assert "TEMPLATE_PLACEHOLDER_ID_SUFFIX" not in content
            assert TEMPLATE_PLACEHOLDER_ARGS_SUFFIX not in content
            assert "TEMPLATE_PLACEHOLDER_ARGS_SUFFIX" not in content
        finally:
            os.chdir(cur_dir)
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_refresh_templates_file_include_default_and_with_pev_vars(self, mocked_app_options):
        join = os.path.join
        parent_dir = join(TESTS_FOLDER, PARENT_FOLDERS[0])
        namespace_name = "tns"
        portion_name = 'destination_portion_name'
        project_path = join(parent_dir, f'{namespace_name}_{portion_name}')
        package_path = join(project_path, namespace_name)
        patched_file = "including_content.txt"
        patched_path = join(project_path, patched_file)

        tpl_imp_name = namespace_name + '.' + namespace_name
        tpl_pkg_path = norm_path(join(parent_dir, norm_name(tpl_imp_name),
                                      namespace_name, namespace_name, TEMPLATES_FOLDER))
        tpl_file_path = join(tpl_pkg_path, OUTSOURCED_FILE_NAME_PREFIX + TPL_FILE_NAME_PREFIX + patched_file)

        default = "include file default string"
        version = '6.699.987'
        tpl_projects = [{'import_name': tpl_imp_name, 'tpl_path': tpl_pkg_path, 'version': version}]
        REGISTERED_TPL_PROJECTS[tpl_imp_name + PACKAGE_VERSION_SEP + version] = tpl_projects[0]
        mocked_app_options[_template_version_option(tpl_imp_name)] = version
        mocked_app_options['namespace'] = namespace_name    # or ""
        try:
            os.makedirs(package_path)
            write_file(os.path.join(project_path, REQ_DEV_FILE_NAME), norm_name(tpl_imp_name))
            write_file(os.path.join(package_path, portion_name + PY_EXT), "__version__ = '9.6.3'")

            os.makedirs(os.path.dirname(tpl_file_path))
            tpl = "{TEMPLATE_PLACEHOLDER_ID_PREFIX}{TEMPLATE_INCLUDE_FILE_PLACEHOLDER_ID}"
            tpl += "{TEMPLATE_PLACEHOLDER_ID_SUFFIX}"
            tpl += f"not_existing_included_file_name.ext,{default}"
            tpl += "{TEMPLATE_PLACEHOLDER_ARGS_SUFFIX}"
            write_file(tpl_file_path, tpl)

            pdv = project_dev_vars(project_path=project_path)

            if on_ci_host():
                assert 'tpl_projects' not in pdv
                assert refresh_templates(pdv) == set()
                assert not os.path.isfile(patched_path)
            else:
                assert pdv['tpl_projects'] == tpl_projects
                assert refresh_templates(pdv) == {norm_path(patched_path)}
                content = read_file(patched_path)
                assert default in content
                assert tpl_imp_name in content
                assert version in content
                assert "TEMPLATE_PLACEHOLDER_ID_PREFIX" not in content
                assert TEMPLATE_INCLUDE_FILE_PLACEHOLDER_ID not in content
                assert "TEMPLATE_INCLUDE_FILE_PLACEHOLDER_ID" not in content
                assert TEMPLATE_PLACEHOLDER_ID_SUFFIX not in content
                assert "TEMPLATE_PLACEHOLDER_ID_SUFFIX" not in content
                assert TEMPLATE_PLACEHOLDER_ARGS_SUFFIX not in content
                assert "TEMPLATE_PLACEHOLDER_ARGS_SUFFIX" not in content
        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)
            del REGISTERED_TPL_PROJECTS[tpl_imp_name + PACKAGE_VERSION_SEP + version]

    def test_replace_with_file_content_or_default(self):
        assert replace_with_file_content_or_default("") == ""

        def_val = "default_value"
        assert replace_with_file_content_or_default(f"_non_file,{def_val}") == def_val
        assert replace_with_file_content_or_default(f"_non_file,{def_val},{def_val}") == def_val + "," + def_val

        assert replace_with_file_content_or_default(f"_non_file,'{def_val[:3]}' + '{def_val[3:]}'") == def_val
        assert replace_with_file_content_or_default(f"_non_file,369") == 369
        with pytest.raises(ValueError):
            assert replace_with_file_content_or_default(f"_non_file,{'a': 111}")
        with pytest.raises(ValueError):
            assert replace_with_file_content_or_default(f"_non_file,{3: '111'}")
        assert replace_with_file_content_or_default(f"_non_file,{111}") == 111
        assert replace_with_file_content_or_default(f"_non_file,[111]") == [111]
        assert replace_with_file_content_or_default(f"_non_file,(1, 2, 3)") == (1, 2, 3)

        syntax_err = "syntax error ' + 1"
        assert replace_with_file_content_or_default("_non_file," + syntax_err) == syntax_err

        file_name = os.path.join(TESTS_FOLDER, "returned_file.txt")
        content = "file_content_to_be_returned"
        try:
            assert replace_with_file_content_or_default(f"{file_name},{def_val}") == def_val
            write_file(file_name, content)
            assert replace_with_file_content_or_default(file_name) == content
            assert replace_with_file_content_or_default(f"{file_name},{def_val}") == content
        finally:
            if os.path.exists(file_name):
                os.remove(file_name)

    def test_replace_file_version(self):
        tst_file = os.path.join(TESTS_FOLDER, 'test_bump_version' + PY_EXT)
        try:
            write_file(tst_file, f"__version__ = '1.2.3'{os.linesep}")

            err = replace_file_version(tst_file, increment_part=3, new_version='1.2.6')

            assert not err

            content = read_file(tst_file)
            assert "__version__ = '1.2.3'" not in content
            assert "__version__ = '1.2.7'" in content
        finally:
            if os.path.exists(tst_file):
                os.remove(tst_file)

    def test_venv_bin_path(self, mocked_app_options):
        mocked_app_options['force'] = False
        ori_env_root = tst_env_root = os.getenv('PYENV_ROOT')
        tst_env_name = 'my_tst_venv99'
        tst_dir = os.path.join(TESTS_FOLDER, "tst_dir")
        tst_sub_dir = os.path.join(tst_dir, "tst_sub_dir")
        venv_file = os.path.join(tst_dir, '.python-version')
        try:
            os.makedirs(tst_sub_dir)
            write_file(venv_file, tst_env_name)
            if not tst_env_root:
                tst_env_root = os.environ['PYENV_ROOT'] = "/any/venv/tst/path"
            with _in_prj_dir_venv(tst_sub_dir):
                assert venv_bin_path() == os.path.join(tst_env_root, 'versions', tst_env_name, 'bin')

        finally:
            if os.path.isdir(tst_dir):
                shutil.rmtree(tst_dir)
            if ori_env_root:
                os.environ['PYENV_ROOT'] = ori_env_root
            else:
                del os.environ['PYENV_ROOT']


class TestHiddenHelpersLocal:
    """ test private helper functions that doesn't need any authentication against git remote hosts. """
    def test_action_decorator(self, mocked_app_options):
        def test_fun(*_args):
            """ test fun docstring """
            return _args

        try:
            dec = _action(APP_PRJ, MODULE_PRJ, kwarg1='1', kwarg2=2)
            assert callable(dec)
            assert callable(dec(test_fun))
            assert 'test_fun' in REGISTERED_ACTIONS
            assert APP_PRJ in REGISTERED_ACTIONS['test_fun']['project_types']
            assert MODULE_PRJ in REGISTERED_ACTIONS['test_fun']['project_types']
            assert REGISTERED_ACTIONS['test_fun']['docstring'] == " test fun docstring "

            # cae.get_option = lambda opt: ""     # prevent argument parsing via cae
            # aedev.git_repo_manager.ACTION_NAME = 'test_fun'
            # aedev.git_repo_manager.INI_PDV['project_type'] = APP_PRJ
            assert dec(test_fun)() == ()

        finally:
            REGISTERED_ACTIONS.pop('test_fun', None)

    def test_act_callable(self, mocked_app_options):
        mocked_app_options['domain'] = ""
        pdv = {'remote_repo_api': None}
        assert callable(_act_callable(pdv, 'new_app'))
        assert not callable(_act_callable(pdv, 'fork'))
        assert callable(_act_callable({'remote_repo_api': GitlabCom()}, 'fork_project'))
        assert _act_callable(pdv, 'xxx_yyy_zzz') is None

    def test_available_actions(self):
        assert _available_actions()
        assert 'show_status' in _available_actions()
        assert 'new_app' in _available_actions()
        assert 'fork_project' in _available_actions()

        assert _available_actions(project_type=NO_PRJ)
        assert 'show_status' not in _available_actions(project_type=NO_PRJ)
        assert 'new_app' in _available_actions(project_type=NO_PRJ)
        assert 'fork_project' not in _available_actions(project_type=NO_PRJ)

    def test_check_arguments(self, mocked_app_options, patched_exit_call_wrapper):
        mocked_app_options['domain'] = ""
        ini_pdv = {'project_type': PARENT_PRJ}
        act_spec = {'docstring': "act new_app docstring", 'project_types': ANY_PRJ_TYPE}
        patched_exit_call_wrapper(_init_act_args_check, ini_pdv, act_spec, 'new_app', [])

        ini_pdv['project_type'] = MODULE_PRJ
        _init_act_args_check(ini_pdv, act_spec, 'new_app', [])

        patched_exit_call_wrapper(_init_act_args_check, ini_pdv, act_spec, 'new_app', ['argument_value'])

        _init_act_args_check(ini_pdv, act_spec, 'show_repo', [])

        patched_exit_call_wrapper(_init_act_args_check, ini_pdv, act_spec, 'show_repo', [])

        ini_pdv['project_type'] = ROOT_PRJ
        patched_exit_call_wrapper(_init_act_args_check, ini_pdv, act_spec, 'install_portions_editable', [])

        act_spec['arg_names'] = ((ARG_ALL, ),
                                 ('portions-sets-expr', ),
                                 ('portion-names' + aedev.git_repo_manager.ARG_MULTIPLES,))
        _init_act_args_check(ini_pdv, act_spec, 'install_portions_editable', [ARG_ALL])

        _init_act_args_check(ini_pdv, act_spec, 'install_portions_editable', ['por1', 'por2'])

        ini_pdv['project_type'] = APP_PRJ
        patched_exit_call_wrapper(_init_act_args_check, ini_pdv, act_spec, 'install_portions_editable', ['p1', 'por2'])

        patched_exit_call_wrapper(_init_act_args_check, ini_pdv, act_spec, 'show_status', ['por1', 'por2'])

        with pytest.raises(KeyError):
            _init_act_args_check(ini_pdv, {}, 'invalid_action', [])

    def test_check_arguments_except_empty_action(self, mocked_app_options):
        mocked_app_options['domain'] = ""
        with pytest.raises(KeyError):
            _init_act_args_check({}, {}, "", [])

    def test_cl(self):
        output = []
        _cl(693, "", lines_output=output, exit_on_err=False)
        assert not output

    def test_debug_or_verbose(self, restore_app_env, mocked_app_options):
        assert _debug_or_verbose()
        mocked_app_options['verbose'] = False
        assert _debug_or_verbose()

        old_val = aedev.git_repo_manager.cae._parsed_arguments
        try:
            aedev.git_repo_manager.cae._parsed_arguments = True
            assert not _debug_or_verbose()
            mocked_app_options['verbose'] = True
            assert _debug_or_verbose()
        finally:
            aedev.git_repo_manager.cae._parsed_arguments = old_val

    def test_exit_error(self):
        po, show_help, shutdown = cae.po, cae.show_help, cae.shutdown
        try:
            po_args = None

            def _po(*args):
                nonlocal po_args
                po_args = args

            cae.po = _po

            called = 0

            def _sh():
                nonlocal called
                called += 1

            cae.show_help = _sh

            sd_args = None

            def _sd(*args):
                nonlocal sd_args
                sd_args = args

            cae.po = _po
            cae.shutdown = _sd

            _exit_error(3)
            assert po_args is None
            assert called == 1
            assert sd_args == (3, )

            _exit_error(6, 'err')
            assert po_args == ('***** err', )
            assert called == 2
            assert sd_args == (6, )

            _exit_error(9)
            assert po_args == ('***** err', )
            assert called == 3
            assert sd_args == (9, )

            # noinspection PyArgumentEqualDefault
            _exit_error(12, "")
            assert po_args == ('***** err', )
            assert called == 3
            assert sd_args == (12, )

        finally:
            cae.po, cae.show_help, cae.shutdown = po, show_help, shutdown

    def test_expected_args(self):
        assert _expected_args((('varA_arg1', 'varA_arg2'), ('varB_arg1', 'varB_arg2', 'varB_arg3'))) == \
               "varA_arg1 varA_arg2 -or- varB_arg1 varB_arg2 varB_arg3"
        assert _expected_args((('a', 'b'), ('c', ), ('d', ))) == "a b -or- c -or- d"

    def test_get_branch(self, mocked_app_options):
        branch = "tst_bra"

        mocked_app_options['branch'] = branch
        assert _get_branch({}) == branch

        mocked_app_options['branch'] = ""
        with patch("aedev.git_repo_manager._git_current_branch", return_value=branch):
            assert _get_branch({}) == branch

    def test_get_user(self, mocked_app_options):
        user = "usr"

        mocked_app_options['gitUser'] = user
        assert _get_user({}) == user

        class _RemoteApi:
            class _Conn:
                class _Usr:
                    name = user
                user = _Usr()
            connection = _Conn()

        mocked_app_options['gitUser'] = ""
        assert _get_user({'remote_repo_api': _RemoteApi()}) == user

        with patch("aedev.git_repo_manager._get_group", return_value=user):
            assert _get_user({}) == user

    def test_git_checkout(self, changed_repo_path, mocked_app_options, patched_exit_call_wrapper):
        old_files = set(path_items(os.path.join(changed_repo_path, "**")))
        mocked_app_options['force'] = False
        mocked_app_options['verbose'] = True
        pev = {'project_path': changed_repo_path}
        main_branch = _git_current_branch(pev)
        new_branch = "new_tst_branch"
        new_file = 'new_branch_file_commit_tst.py'

        assert main_branch == MAIN_BRANCH
        _git_add(pev)
        write_file(os.path.join(changed_repo_path, COMMIT_MSG_FILE_NAME), "commit title of develop branch commit")
        _git_commit(pev)

        patched_exit_call_wrapper(_git_checkout, pev, branch="")
        assert _git_current_branch(pev) == MAIN_BRANCH
        assert old_files == set(path_items(os.path.join(changed_repo_path, "**")))

        write_file(os.path.join(changed_repo_path, new_file), "# new test file\n")
        new_files = set(path_items(os.path.join(changed_repo_path, "**")))

        _git_checkout(pev, branch=new_branch)
        assert _git_current_branch(pev) == new_branch
        assert new_files == set(path_items(os.path.join(changed_repo_path, "**")))

        _git_add(pev)
        write_file(os.path.join(changed_repo_path, COMMIT_MSG_FILE_NAME), "new-branch commit message title")
        _git_commit(pev)

        _git_checkout(pev, branch=MAIN_BRANCH)
        assert _git_current_branch(pev) == MAIN_BRANCH
        assert new_files == set(path_items(os.path.join(changed_repo_path, "**")))  # checkout does not remove new files

        with _in_prj_dir_venv(changed_repo_path):
            os.remove(new_file)
        patched_exit_call_wrapper(_git_checkout, pev, branch=MAIN_BRANCH)
        assert old_files == set(path_items(os.path.join(changed_repo_path, "**")))

        _git_checkout(pev, "HEAD", new_file)
        assert _git_current_branch(pev) == MAIN_BRANCH
        assert new_files == set(path_items(os.path.join(changed_repo_path, "**")))

    def test_git_cl(self):
        output = []
        _cl(0, "git", extra_args=("--version", ), lines_output=output, exit_on_err=False)
        assert output
        # with capsys.disabled():
        #     print(f">>>>>> git --version output: {output}")

    def test_git_clone_ae_base(self, mocked_app_options):
        package_name = 'ae_base'
        mocked_app_options['verbose'] = False
        cur_dir = os.getcwd()

        project_path = _git_clone(f"https://gitlab.com/ae-group", package_name)

        assert os.getcwd() == cur_dir
        tmp_dir = aedev.git_repo_manager.TEMP_PARENT_FOLDER
        assert tmp_dir
        assert project_path.startswith(tmp_dir)
        assert os.path.isdir(os.path.join(tmp_dir, package_name))

    def test_git_clone_aedev_git_repo_manager_release(self):
        import_name = "aedev.git_repo_manager"
        version = "0.2.24"
        package_name = norm_name(import_name)

        project_path = _git_clone("https://gitlab.com/aedev-group", package_name, branch_or_tag=f"release{version}")

        tmp_dir = aedev.git_repo_manager.TEMP_PARENT_FOLDER
        assert project_path.startswith(tmp_dir)
        assert os.path.isdir(os.path.join(tmp_dir, package_name))
        assert version == code_file_version(project_main_file(import_name, project_path=project_path))

    def test_git_clone_aedev_setup_project_version_tag_to_test(self):
        import_name = "aedev.setup_project"
        version = "0.3.3"
        package_name = norm_name(import_name)
        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])

        try:
            os.makedirs(parent_dir)

            project_path = _git_clone("https://gitlab.com/aedev-group", package_name, branch_or_tag=f"v{version}",
                                      parent_path=parent_dir)

            assert project_path.startswith(norm_path(parent_dir))
            assert os.path.isdir(os.path.join(parent_dir, package_name))
            assert version == code_file_version(project_main_file(import_name, project_path=project_path))
        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_git_clone_fail(self, mocked_app_options):
        cur_dir = os.getcwd()
        mocked_app_options['force'] = False
        mocked_app_options['verbose'] = False

        assert _git_clone("_invalid_repo_", "_invalid_package_name", branch_or_tag="9.99.999") == ""
        assert os.getcwd() == cur_dir

    def test_git_commit_on_changed_repo(self, changed_repo_path, mocked_app_options, patched_exit_call_wrapper):
        files = set(path_items(os.path.join(changed_repo_path, "**")))
        mocked_app_options['force'] = False
        mocked_app_options['verbose'] = True
        pev = {'project_path': changed_repo_path}

        patched_exit_call_wrapper(_git_commit, pev)
        assert files == set(path_items(os.path.join(changed_repo_path, "**")))

        write_file(os.path.join(changed_repo_path, 'tst.py'), "# new test file")
        files = set(path_items(os.path.join(changed_repo_path, "**")))

        patched_exit_call_wrapper(_git_commit, pev)
        assert files == set(path_items(os.path.join(changed_repo_path, "**")))

        _git_add(pev)
        patched_exit_call_wrapper(_git_commit, pev)
        assert files == set(path_items(os.path.join(changed_repo_path, "**")))

        write_file(os.path.join(changed_repo_path, COMMIT_MSG_FILE_NAME), "commit title")
        _git_commit(pev)

    def test_git_commit_on_empty_repo(self, empty_repo_path, mocked_app_options, patched_exit_call_wrapper):
        files = set(path_items(os.path.join(empty_repo_path, "**")))
        mocked_app_options['force'] = False
        mocked_app_options['verbose'] = True
        pev = {'project_path': empty_repo_path}

        patched_exit_call_wrapper(_git_commit, pev)
        assert files == set(path_items(os.path.join(empty_repo_path, "**")))

        write_file(os.path.join(empty_repo_path, 'tst.py'), "# new test file")
        patched_exit_call_wrapper(_git_commit, pev)

        write_file(os.path.join(empty_repo_path, COMMIT_MSG_FILE_NAME), "commit message title\n\ncommit message body")
        patched_exit_call_wrapper(_git_commit, pev)

        _git_add(pev)       # adding untracked file tst.py
        _git_commit(pev)

    def test_git_diff_on_changed_repo(self, changed_repo_path, mocked_app_options):
        mocked_app_options['force'] = False
        mocked_app_options['verbose'] = False
        old_val = cae._parsed_arguments
        try:
            cae._parsed_arguments = True
            files = set(path_items(os.path.join(changed_repo_path, "**")))
            pev = {'project_path': changed_repo_path}

            output = _git_diff(pev)
            for file in files:
                if not file.endswith('added.cfg'):
                    assert os.path.basename(file) in "".join(output)
            assert files == set(path_items(os.path.join(changed_repo_path, "**")))

            mocked_app_options['verbose'] = True

            verbose_output = _git_diff(pev)
            assert len("".join(verbose_output)) > len("".join(output))
            for file in files:
                if not file.endswith('added.cfg'):
                    assert os.path.basename(file) in "".join(verbose_output)
        finally:
            cae._parsed_arguments = old_val

    def test_git_diff_on_changed_repo_after_add(self, changed_repo_path, mocked_app_options):
        mocked_app_options['force'] = False
        mocked_app_options['verbose'] = False
        files = set(path_items(os.path.join(changed_repo_path, "**")))
        pev = {'project_path': changed_repo_path}
        _git_add(pev)
        assert not _git_diff(pev)
        assert files == set(path_items(os.path.join(changed_repo_path, "**")))

    def test_git_diff_on_empty_repo(self, empty_repo_path, mocked_app_options):
        mocked_app_options['verbose'] = False
        files = set(path_items(os.path.join(empty_repo_path, "**")))

        assert not _git_diff({'project_path': empty_repo_path})
        assert files == set(path_items(os.path.join(empty_repo_path, "**")))

    def test_git_diff_unstaged_or_modified_files(self, changed_repo_path, mocked_app_options):
        mocked_app_options['force'] = False
        mocked_app_options['verbose'] = False
        pev = {'project_path': changed_repo_path}

        def _git_dfo(*_args) -> list:
            return _git_diff(pev, "--name-only", *_args)

        def _git_lsf(_o: str) -> list:
            _output = []
            with _in_prj_dir_venv(pev['project_path']):
                _cl(0, f"git ls-files -{_o}", lines_output=_output)
            return _output

        def _git_lsm() -> list:
            return _git_lsf('m')

        def _git_lsu() -> list:
            return _git_lsf('u')

        def _git_stf() -> list:
            return [_[3:] for _ in _git_status(pev)]

        files = [os.path.relpath(_, changed_repo_path) for _ in path_items(os.path.join(changed_repo_path, "*"))]
        assert set(files) == {'added.cfg', 'changed.py'}

        assert set(_git_dfo()) == {'changed.py', 'delete.txt'}
        assert set(_git_dfo("--diff-filter=ABCDMRTUX")) == {'changed.py', 'delete.txt'}
        assert set(_git_dfo("--cached")) == set()
        assert set(_git_dfo("--diff-filter=ABCDMRTUX", "HEAD")) == {'changed.py', 'delete.txt'}
        assert set(_git_dfo("--cached", "--diff-filter=ABCDMRTUX", "HEAD")) == set()
        assert set(_git_lsm()) == {'changed.py', 'delete.txt'}
        assert set(_git_lsu()) == set()
        assert set(_git_stf()) == {'added.cfg', 'changed.py', 'delete.txt'}

        # files.append("add2.yml")
        write_file(os.path.join(changed_repo_path, "add2.yml"), "# new file content")
        chg_file = os.path.join(changed_repo_path, 'changed.py')
        write_file(chg_file, read_file(chg_file) + os.linesep + "# changed")
        all_fil = {'add2.yml', 'added.cfg', 'changed.py', 'delete.txt'}

        assert set(_git_dfo()) == {'changed.py', 'delete.txt'}
        assert set(_git_dfo("--diff-filter=ABCDMRTUX")) == {'changed.py', 'delete.txt'}
        assert set(_git_dfo("--cached")) == set()
        assert set(_git_dfo("--diff-filter=ABCDMRTUX", "HEAD")) == {'changed.py', 'delete.txt'}
        assert set(_git_dfo("--cached", "--diff-filter=ABCDMRTUX", "HEAD")) == set()
        assert set(_git_lsm()) == {'changed.py', 'delete.txt'}
        assert set(_git_lsu()) == set()
        assert set(_git_stf()) == all_fil     # changed by adding add2.yml

        # branch = "add2_branch"
        # _git_checkout(pev, branch)
        # assert _git_current_branch(pev) == branch
        _git_add(pev)

        assert set(_git_dfo()) == set()      # changed by _git_add()
        assert set(_git_dfo("--diff-filter=ABCDMRTUX")) == set()
        assert set(_git_dfo("--cached")) == all_fil    # changed
        assert set(_git_dfo("--diff-filter=ABCDMRTUX", "HEAD")) == all_fil    # changed
        assert set(_git_dfo("--cached", "--diff-filter=ABCDMRTUX", "HEAD")) == all_fil    # changed
        assert set(_git_lsm()) == set()                         # changed by _git_add()
        assert set(_git_lsu()) == set()
        assert set(_git_stf()) == all_fil

        write_file(os.path.join(changed_repo_path, COMMIT_MSG_FILE_NAME), "commit tst msg")
        _git_commit(pev)

        assert set(_git_dfo()) == set()
        assert set(_git_dfo("--diff-filter=ABCDMRTUX")) == set()
        assert set(_git_dfo("--cached")) == set()
        assert set(_git_dfo("--diff-filter=ABCDMRTUX", "HEAD")) == set()
        assert set(_git_dfo("--cached", "--diff-filter=ABCDMRTUX", "HEAD")) == set()
        assert set(_git_lsm()) == set()
        assert set(_git_lsu()) == set()
        assert set(_git_stf()) == set()                         # changed by _git_commit()

        chg_file = os.path.join(changed_repo_path, 'changed.py')
        write_file(chg_file, read_file(chg_file) + os.linesep + "# change 2")

        assert set(_git_dfo()) == {'changed.py'}                                 # changed by change2
        assert set(_git_dfo("--diff-filter=ABCDMRTUX")) == {'changed.py'}      # changed by change2
        assert set(_git_dfo("--cached")) == set()
        assert set(_git_dfo("--diff-filter=ABCDMRTUX", "HEAD")) == {'changed.py'}  # changed
        assert set(_git_dfo("--cached", "--diff-filter=ABCDMRTUX", "HEAD")) == set()
        assert set(_git_lsm()) == {'changed.py'}                                                    # changed by change2
        assert set(_git_lsu()) == set()
        assert set(_git_stf()) == {'changed.py'}                                                    # changed by change2

        os.remove(os.path.join(changed_repo_path, 'add2.yml'))

        assert set(_git_dfo()) == {'add2.yml', 'changed.py'}     # changed by delete of add2.yml
        assert set(_git_dfo("--diff-filter=ABCDMRTUX")) == {'add2.yml', 'changed.py'}     # changed
        assert set(_git_dfo("--cached")) == set()
        assert set(_git_dfo("--diff-filter=ABCDMRTUX", "HEAD")) == {'add2.yml', 'changed.py'}  # ch
        assert set(_git_dfo("--cached", "--diff-filter=ABCDMRTUX", "HEAD")) == set()
        assert set(_git_lsm()) == {'add2.yml', 'changed.py'}                        # changed by delete of add2.yml
        assert set(_git_lsu()) == set()
        assert set(_git_stf()) == {'add2.yml', 'changed.py'}                        # changed by delete of add2.yml

        _git_add(pev)
        _git_commit(pev)

        assert set(_git_dfo()) == set()                          # changed by _git_commit()
        assert set(_git_dfo("--diff-filter=ABCDMRTUX")) == set()
        assert set(_git_dfo("--cached")) == set()
        assert set(_git_dfo("--diff-filter=ABCDMRTUX", "HEAD")) == set()
        assert set(_git_dfo("--cached", "--diff-filter=ABCDMRTUX", "HEAD")) == set()
        assert set(_git_lsm()) == set()                                             # changed by _git_commit()
        assert set(_git_lsu()) == set()
        assert set(_git_stf()) == set()                                             # changed by _git_commit()

    def test_git_init_if_needed(self, mocked_app_options):
        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        prj_dir = os.path.join(parent_dir, 'tst_git_init_dir')
        pev = {'project_path': prj_dir}
        mocked_app_options['force'] = False
        mocked_app_options['verbose'] = False
        try:
            os.makedirs(prj_dir)
            assert _git_init_if_needed(pev)
            assert os.path.isdir(os.path.join(prj_dir, GIT_FOLDER_NAME))
            assert not _git_uncommitted(pev)
            assert _git_current_branch(pev) == MAIN_BRANCH
        finally:
            if os.path.isdir(parent_dir):
                shutil.rmtree(parent_dir)

    def test_git_ls_files_vs_git_status(self, empty_repo_path, mocked_app_options):
        mocked_app_options['verbose'] = False
        pev = {'project_path': empty_repo_path}
        ls_uncommitted = []
        with _in_prj_dir_venv(empty_repo_path):
            _cl(0, "git ls-files -m", lines_output=ls_uncommitted)
        assert ls_uncommitted == []

        st_uncommitted = _git_status(pev)
        assert st_uncommitted == ['?? .gitignore']

        git_uncommitted = _git_uncommitted(pev)
        assert git_uncommitted == ['.gitignore']

    def test_git_ls_files_vs_git_status_on_changed_repo(self, changed_repo_path, mocked_app_options):
        mocked_app_options['force'] = False
        mocked_app_options['verbose'] = False
        pev = {'project_path': changed_repo_path}
        ls_uncommitted = []
        with _in_prj_dir_venv(changed_repo_path):
            _cl(0, "git ls-files -m", lines_output=ls_uncommitted)
        st_uncommitted = [_[3:] for _ in _git_status(pev)]

        assert len(ls_uncommitted) >= 2 and 'added.cfg' not in ls_uncommitted
        assert all(lsi in st_uncommitted for lsi in ls_uncommitted)

    def test_git_status_on_changed_repo(self, changed_repo_path, mocked_app_options):
        mocked_app_options['force'] = False
        mocked_app_options['verbose'] = False
        files = set(path_items(os.path.join(changed_repo_path, "**")))
        pev = {'project_path': changed_repo_path}

        output = _git_status(pev)
        for file in files:
            assert os.path.basename(file) in "".join(output)
        assert files == set(path_items(os.path.join(changed_repo_path, "**")))

        mocked_app_options['verbose'] = True

        verbose_output = _git_status(pev)
        assert len(verbose_output) > len(output)
        for file in files:
            assert os.path.basename(file) in "".join(verbose_output)

    def test_git_status_on_empty_repo(self, empty_repo_path, mocked_app_options):
        mocked_app_options['verbose'] = False
        files = set(path_items(os.path.join(empty_repo_path, "**")))
        pev = {'project_path': empty_repo_path}

        assert _git_status(pev) == ['?? .gitignore']
        assert files == set(path_items(os.path.join(empty_repo_path, "**")))

    def test_init_act_exec_args_exits(self, mocked_app_options, patched_exit_call_wrapper):
        mocked_app_options['action'] = 'what_ever_not_existing_action'
        mocked_app_options['arguments'] = ['what_ever_invalid_action_arg']
        mocked_app_options['namespace'] = ""        # or tst_ns_name
        mocked_app_options[_template_version_option(tst_ns_name + '.' + tst_ns_name)] = ""
        patched_exit_call_wrapper(_init_act_exec_args)

    def test_init_act_exec_args_new_app(self, mocked_app_options):
        mocked_app_options['action'] = 'new_app'
        mocked_app_options['arguments'] = []
        mocked_app_options['domain'] = 'github.com'
        mocked_app_options['namespace'] = ""
        mocked_app_options['path'] = ""

        ini_pdv, act_name, act_args = _init_act_exec_args()

        assert act_name == 'new_app'
        assert isinstance(ini_pdv, dict)    # PdvType
        assert 'remote_repo_api' not in ini_pdv
        assert not act_args

    def test_init_act_exec_args_show_status(self, mocked_app_options):
        mocked_app_options['action'] = 'status'
        mocked_app_options['arguments'] = []
        mocked_app_options['domain'] = ''
        mocked_app_options['namespace'] = ""
        mocked_app_options['path'] = ""

        ini_pdv, act_name, act_args = _init_act_exec_args()

        assert act_name == 'show_status'
        assert isinstance(ini_pdv, dict)    # PdvType
        assert 'remote_repo_api' not in ini_pdv
        assert not act_args

    def test_init_children_pdv_args_branch_filter(self, mocked_app_options):
        mocked_app_options['filterExpression'] = ""
        mocked_app_options['filterBranch'] = ""
        mocked_app_options['force'] = False
        filtered_branch = "filter_branch"
        a_pdv = {'package_name': "n_a", 'project_path': TESTS_FOLDER}
        b_pdv = {'package_name': "n_b", 'project_path': TESTS_FOLDER}
        ini_pdv = {'children_project_vars': {'n_a': a_pdv, 'n_b': b_pdv}, 'namespace_name': 'n',
                   'portions_packages': ['n_a', 'n_b']}

        assert _init_children_pdv_args(ini_pdv, [ARG_ALL]) == [a_pdv, b_pdv]

        with patch('aedev.git_repo_manager._git_current_branch',
                   new=lambda _: filtered_branch if _['package_name'] == "n_b" else MAIN_BRANCH):
            mocked_app_options['filterBranch'] = filtered_branch
            assert _init_children_pdv_args(ini_pdv, ['filterBranch']) == [b_pdv]

    def test_init_children_pdv_args_exit(self, mocked_app_options, patched_exit_call_wrapper):
        mocked_app_options['filterExpression'] = ""
        mocked_app_options['filterBranch'] = ""
        mocked_app_options['force'] = False
        ini_pdv = {'children_project_vars': {}, 'namespace_name': 'n'}
        patched_exit_call_wrapper(_init_children_pdv_args, ini_pdv, [])

        patched_exit_call_wrapper(_init_children_pdv_args, ini_pdv, ["undefined_package_name"])

        patched_exit_call_wrapper(_init_children_pdv_args, ini_pdv, ["filterBranch"])

        mocked_app_options['filterBranch'] = "branch_name_to_filter"
        patched_exit_call_wrapper(_init_children_pdv_args, ini_pdv, [ARG_ALL])

    def test_init_children_pdv_args_expression(self, mocked_app_options):
        mocked_app_options['filterExpression'] = ""
        mocked_app_options['filterBranch'] = ""
        mocked_app_options['force'] = False
        ch1_pdv = {'package_name': "p1", 'project_path': TESTS_FOLDER}
        ch2_pdv = {'package_name': "p2", 'project_path': TESTS_FOLDER}
        chi_vars = {'p1': ch1_pdv, 'p2': ch2_pdv}
        ini_pdv: PdvType = {'children_project_vars': chi_vars, 'project_type': PARENT_PRJ}

        assert _init_children_pdv_args(ini_pdv, ["any_pkg_name"]) == [{'package_name': 'any_pkg_name'}]

        with patch("aedev.git_repo_manager._init_children_presets",
                   return_value={'ps_a': {'p1'}, 'ps_b': {'p1', 'p2'}}):
            assert _init_children_pdv_args(ini_pdv, ["ps_a"]) == [ch1_pdv]
            assert ch1_pdv in _init_children_pdv_args(ini_pdv, ["ps_b"])
            assert ch2_pdv in _init_children_pdv_args(ini_pdv, ["ps_b"])
            assert _init_children_pdv_args(ini_pdv, ["ps_b - ps_a"]) == [ch2_pdv]

        ini_pdv['namespace_name'] = 'n'
        ch1_pdv = {'package_name': "n_1", 'project_path': TESTS_FOLDER}
        ch2_pdv = {'package_name': "n_por2", 'project_path': TESTS_FOLDER}
        ch3_pdv = {'package_name': "n_b", 'project_path': TESTS_FOLDER}
        ini_pdv['children_project_vars'] = {'n_1': ch1_pdv, 'n_por2': ch2_pdv, 'n_b': ch3_pdv}
        with patch("aedev.git_repo_manager._init_children_presets",
                   return_value={'ps_a': {'n_1', 'n_b'}, 'ps_b': {'n_1', 'n_por2'}, 'ps_c': {'n_por2'}}):
            assert _init_children_pdv_args(ini_pdv, ["ps_a ^ (ps_b - ps_c)"]) == [ch3_pdv]

            assert ch1_pdv in _init_children_pdv_args(ini_pdv, ["ps_a | (ps_b - ps_c)"])
            assert ch3_pdv in _init_children_pdv_args(ini_pdv, ["ps_a | (ps_b - ps_c)"])

            assert _init_children_pdv_args(ini_pdv, ["ps_a & ps_b"]) == [ch1_pdv]
            assert _init_children_pdv_args(ini_pdv, ["ps_b & ps_c"]) == [ch2_pdv]

            assert _init_children_pdv_args(ini_pdv, ["set(ps_a) & (ps_b - set(['n_por2']))"]) == [ch1_pdv]

            assert _init_children_pdv_args(ini_pdv, ["ps_a&ps_b"]) == [ch1_pdv]
            assert _init_children_pdv_args(ini_pdv, ["ps_a", "&", "ps_b"]) == [ch1_pdv]

    def test_init_children_pdv_args_list(self, mocked_app_options):
        mocked_app_options['filterExpression'] = ""
        mocked_app_options['filterBranch'] = ""
        mocked_app_options['force'] = False
        ch1_pdv = {'package_name': "p1", 'project_path': TESTS_FOLDER}
        ch2_pdv = {'package_name': "p2", 'project_path': TESTS_FOLDER}
        chi_vars = {'p1': ch1_pdv, 'p2': ch2_pdv}
        ini_pdv = {'children_project_vars': chi_vars, 'project_type': PARENT_PRJ}
        assert _init_children_pdv_args(ini_pdv, ["('p1', ) + ('p2', )"]) == [ch1_pdv, ch2_pdv]

        chi_vars = {key: {'package_name': key, 'project_path': TESTS_FOLDER} for key in ['a1', 'b', 'p3']}
        ini_pdv['children_project_vars'] = chi_vars
        assert _init_children_pdv_args(ini_pdv, [ARG_ALL]) == list(chi_vars.values())

        assert _init_children_pdv_args(ini_pdv, ['p3']) == [{'package_name': 'p3', 'project_path': TESTS_FOLDER}]

        mocked_app_options['force'] = True
        assert _init_children_pdv_args(ini_pdv, ['p3', 'p3']) == [{'package_name': 'p3', 'project_path': TESTS_FOLDER},
                                                                  {'package_name': 'p3', 'project_path': TESTS_FOLDER}]

        nsn = 'n'
        ch1_pdv = {'package_name': nsn + "_1", 'project_path': TESTS_FOLDER}
        ch2_pdv = {'package_name': nsn + "_por2", 'project_path': TESTS_FOLDER}
        chi_vars = {nsn + '_1': ch1_pdv, nsn + '_por2': ch2_pdv}
        ini_pdv = {'children_project_vars': chi_vars, 'namespace_name': nsn, 'project_type': ROOT_PRJ}
        assert _init_children_pdv_args(ini_pdv, ["('1', ) + ('n_por2', )"]) == [ch1_pdv, ch2_pdv]

    def test_init_children_presets(self, mocked_app_options):
        pkg_name = "pkg_nam"
        chi = {'editable_project_path': "edi_prj_dir", 'package_name': pkg_name, 'project_path': TESTS_FOLDER}

        mocked_app_options['filterBranch'] = MAIN_BRANCH
        mocked_app_options['filterExpression'] = "True"
        with patch("aedev.git_repo_manager._git_uncommitted", return_value=["non-empty"]):
            with patch("aedev.git_repo_manager._git_current_branch", return_value=MAIN_BRANCH):
                presets = _init_children_presets(OrderedDict({pkg_name: chi}))
        assert len(presets) == 6
        assert presets[ARG_ALL] == {pkg_name}
        assert presets['editable'] == {pkg_name}
        assert presets['modified'] == {pkg_name}
        assert presets['develop'] == {pkg_name}
        assert presets['filterBranch'] == {pkg_name}
        assert presets['filterExpression'] == {pkg_name}

        mocked_app_options['filterBranch'] = ""
        mocked_app_options['filterExpression'] = ":invalid:exp+ess+on"
        presets = _init_children_presets(OrderedDict({pkg_name: chi}))
        assert len(presets) == 5
        assert presets[ARG_ALL] == {pkg_name}
        assert presets['editable'] == {pkg_name}
        assert presets['modified'] == set()
        assert presets['develop'] == set()
        assert 'filterBranch' not in presets
        assert presets['filterExpression'] == set()

    def test_patch_outsourced_md(self):
        content = "content"
        patcher = "patcher"
        patched_content = _patch_outsourced("any.md", content, patcher)
        assert patched_content.endswith(content)
        assert patched_content.startswith(f"<!-- {OUTSOURCED_MARKER}")
        assert patcher in patched_content

    def test_patch_outsourced_rst(self):
        content = "content"
        patcher = "patcher"
        sep = os.linesep
        patched_content = _patch_outsourced("any.rst", content, patcher)
        assert patched_content.endswith(content)
        assert patched_content.startswith(f"{sep}..{sep}    {OUTSOURCED_MARKER}")
        assert patcher in patched_content

    def test_patch_outsourced_txt(self):
        content = "content"
        patcher = "patcher"
        patched_content = _patch_outsourced("any.txt", content, patcher)
        assert patched_content.endswith(content)
        assert patched_content.startswith(f"# {OUTSOURCED_MARKER}")
        assert patcher in patched_content

    def test_print_pdv(self, mocked_app_options):
        mocked_app_options['verbose'] = False
        _print_pdv({'project_type': PARENT_PRJ, 'long_desc_content': "long desc content (not that long ;)"})
        # assert capsys.readouterr().out worked in TestHiddenHelpersRemote, but after moving here is always empty string

    def test_register_template_aedev_root(self, mocked_app_options):
        nsn = "aedev"
        tpl_imp_name = nsn + "." + nsn
        pkg_name = norm_name(tpl_imp_name)
        tpl_path = os.path.join(pkg_name, nsn, nsn, TEMPLATES_FOLDER)
        dev_require = []
        tpl_projects = []

        assert mocked_app_options[_template_version_option(tpl_imp_name)] == ""

        _register_template(tpl_imp_name, dev_require, True, tpl_projects)
        assert dev_require
        assert dev_require[0].startswith(pkg_name + PACKAGE_VERSION_SEP)
        assert dev_require[0].split(PACKAGE_VERSION_SEP)[1]

        assert tpl_projects
        assert tpl_projects[0]['import_name'] == tpl_imp_name
        assert tpl_projects[0]['tpl_path'] != ""
        assert tpl_projects[0]['tpl_path'].endswith(tpl_path)
        assert tpl_projects[0]['version'] != ""

        pkg_name, version = package_version(tpl_imp_name, list(REGISTERED_TPL_PROJECTS.keys()))
        assert tpl_imp_name + PACKAGE_VERSION_SEP + version in REGISTERED_TPL_PROJECTS
        assert REGISTERED_TPL_PROJECTS[tpl_imp_name + PACKAGE_VERSION_SEP + version]['import_name'] == tpl_imp_name
        assert REGISTERED_TPL_PROJECTS[tpl_imp_name + PACKAGE_VERSION_SEP + version]['tpl_path'] != ""
        assert REGISTERED_TPL_PROJECTS[tpl_imp_name + PACKAGE_VERSION_SEP + version]['tpl_path'].endswith(tpl_path)
        assert REGISTERED_TPL_PROJECTS[tpl_imp_name + PACKAGE_VERSION_SEP + version]['version'] == version

    def test_register_template_not_existing(self, mocked_app_options):
        tpl_imp_name = "not.existing_tpl_package_imp_name"
        dev_require = []
        tpl_projects = []
        mocked_app_options[_template_version_option(tpl_imp_name)] = ""

        _register_template(tpl_imp_name, dev_require, True, tpl_projects)
        assert not dev_require
        assert not tpl_projects
        assert tpl_imp_name + PACKAGE_VERSION_SEP in REGISTERED_TPL_PROJECTS
        assert REGISTERED_TPL_PROJECTS[tpl_imp_name + PACKAGE_VERSION_SEP]['import_name'] == tpl_imp_name
        assert REGISTERED_TPL_PROJECTS[tpl_imp_name + PACKAGE_VERSION_SEP]['tpl_path'] == ""
        assert REGISTERED_TPL_PROJECTS[tpl_imp_name + PACKAGE_VERSION_SEP]['version'] == ""

    def test_renew_prj_dir(self, mocked_app_options):
        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        app_name = 'cpl_prj_dir_app'
        project_path = norm_path(os.path.join(parent_dir, app_name))
        mocked_app_options['group'] = "group_name"
        mocked_app_options['namespace'] = ""
        mocked_app_options['package'] = app_name
        mocked_app_options['path'] = project_path
        pdv = {'namespace_name': '', 'package_name': app_name, 'project_path': project_path, 'project_type': APP_PRJ}

        try:
            assert not os.path.isdir(project_path)

            _renew_prj_dir(pdv.copy())

            mocked_app_options['path'] = ""
            _renew_prj_dir(pdv.copy())
            assert not os.path.isdir(os.path.join(project_path, TEMPLATES_FOLDER))

            pdv['project_type'] = ROOT_PRJ
            _renew_prj_dir(pdv.copy())
            assert os.path.isdir(project_path)
            assert os.path.isdir(os.path.join(project_path, DOCS_FOLDER))
            assert os.path.isdir(os.path.join(project_path, TESTS_FOLDER))
            assert os.path.isfile(os.path.join(project_path, 'main' + PY_EXT))
            assert os.path.isfile(os.path.join(project_path, BUILD_CONFIG_FILE))

            assert not os.path.isdir(os.path.join(project_path, TEMPLATES_FOLDER))
            assert not os.path.exists(app_name)
            assert not os.path.exists(BUILD_CONFIG_FILE)

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_renew_project_change_prj_type(self, mocked_app_options):
        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        app_name = 'cpl_prj_app_name'
        project_path = os.path.join(parent_dir, app_name)
        mocked_app_options['group'] = "group_name"
        mocked_app_options['namespace'] = ""
        mocked_app_options['package'] = app_name
        mocked_app_options['path'] = project_path

        module_version_file = os.path.join(project_path, app_name + PY_EXT)
        sister_prj_path = os.path.join(os.path.dirname(os.getcwd()), app_name)
        try:
            os.makedirs(project_path)
            write_file(os.path.join(project_path, BUILD_CONFIG_FILE), "")
            _renew_project({}, MODULE_PRJ)   # project_type change creates module_version_file in project folder
            assert os.path.exists(module_version_file)
            assert not os.path.exists(sister_prj_path)

            os.remove(module_version_file)
            with _in_prj_dir_venv(project_path):    # fix for error happening only in console / not-in-PyCharm-pytest
                _cl(0, "git remote remove origin")  # remove remote to prevent ask for password in next git fetch
            _renew_project({}, APP_PRJ)
            assert not os.path.exists(module_version_file)
            assert not os.path.exists(sister_prj_path)

            assert os.path.isdir(project_path)
            assert os.path.isdir(os.path.join(project_path, DOCS_FOLDER))
            assert not os.path.isdir(os.path.join(project_path, TEMPLATES_FOLDER))
            assert os.path.isdir(os.path.join(project_path, TESTS_FOLDER))
            assert os.path.isfile(os.path.join(project_path, 'main' + PY_EXT))
            assert os.path.isfile(os.path.join(project_path, BUILD_CONFIG_FILE))

            assert not os.path.exists(app_name)
            assert not os.path.exists(BUILD_CONFIG_FILE)

        finally:
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_renew_project_exits(self, mocked_app_options, patched_exit_call_wrapper):
        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        app_name = 'tst_app_prj_name'
        project_path = norm_path(os.path.join(parent_dir, app_name))
        mocked_app_options['branch'] = MAIN_BRANCH
        mocked_app_options['domain'] = ""
        mocked_app_options['group'] = "group_name"
        mocked_app_options['namespace'] = ""
        mocked_app_options['package'] = app_name
        mocked_app_options['path'] = project_path

        sister_prj_path = os.path.join(os.path.dirname(os.getcwd()), app_name)
        cur_dir = os.getcwd()
        try:
            assert not os.path.isdir(project_path)

            ini_pdv = {}
            _renew_project(ini_pdv, APP_PRJ)
            assert os.path.isdir(project_path)

            assert not os.path.exists(sister_prj_path)
            mocked_app_options['path'] = ""              # exit reason, NO fallback to package option on prj typ chg
            os.chdir(project_path)
            patched_exit_call_wrapper(_renew_project, ini_pdv, APP_PRJ)
            os.chdir(cur_dir)
            assert not os.path.exists(sister_prj_path)

            mocked_app_options['package'] = ""           # exit reason
            patched_exit_call_wrapper(_renew_project, ini_pdv, APP_PRJ)
            assert not os.path.exists(sister_prj_path)

            mocked_app_options['package'] = app_name
            os.chdir(project_path)
            patched_exit_call_wrapper(_renew_project, ini_pdv, APP_PRJ)
            os.chdir(cur_dir)
            assert not os.path.exists(sister_prj_path)

            patched_exit_call_wrapper(_renew_project, ini_pdv, ROOT_PRJ)
            assert not os.path.exists(sister_prj_path)

            ini_pdv['namespace_name'] = ""
            ini_pdv['project_type'] = APP_PRJ
            ini_pdv['project_path'] = project_path
            mocked_app_options['group'] = ""  # exit reason
            with _in_prj_dir_venv(project_path):
                _cl(0, "git remote remove origin")  # remove remote to prevent ask for password in next git fetch
            patched_exit_call_wrapper(_renew_project, ini_pdv, APP_PRJ)
            assert not os.path.exists(sister_prj_path)

            # alternative fix to mocked_get_opt_values['repo_group'] = 'group_name'
            ini_pdv['repo_group'] = "group_name"
            os.chdir(project_path)
            with _in_prj_dir_venv(project_path):
                _cl(0, "git remote remove origin")  # remove remote to prevent ask for password in next git fetch
            patched_exit_call_wrapper(_renew_project, ini_pdv, APP_PRJ)
            os.chdir(cur_dir)
            assert not os.path.exists(sister_prj_path)

            mocked_app_options['path'] = parent_dir  # no parent-dir exit reason
            patched_exit_call_wrapper(_renew_project, ini_pdv, APP_PRJ)
            assert not os.path.exists(sister_prj_path)

            assert os.path.isdir(project_path)
            assert os.path.isdir(os.path.join(project_path, DOCS_FOLDER))
            assert not os.path.isdir(os.path.join(project_path, TEMPLATES_FOLDER))
            assert os.path.isdir(os.path.join(project_path, TESTS_FOLDER))
            assert os.path.isfile(os.path.join(project_path, 'main' + PY_EXT))
            assert os.path.isfile(os.path.join(project_path, BUILD_CONFIG_FILE))

        finally:
            os.chdir(cur_dir)
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_renew_project_new_app_from_parent_via_package(self, mocked_app_options):
        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        pkg_name = "tst_app_prj"
        project_path = norm_path(os.path.join(parent_dir, pkg_name))
        mocked_app_options['group'] = ""
        mocked_app_options['namespace'] = ""
        mocked_app_options['package'] = pkg_name
        mocked_app_options['path'] = ""
        cur_dir = os.getcwd()
        try:
            os.makedirs(parent_dir)
            os.chdir(parent_dir)
            parent_pdv = project_dev_vars()
            assert parent_pdv['project_type'] == PARENT_PRJ
            assert parent_pdv['project_path'] == norm_path("")

            _renew_project(parent_pdv, APP_PRJ)

            app_pdv = project_dev_vars(project_path=pkg_name)
            assert app_pdv['project_type'] == APP_PRJ
            assert app_pdv['project_path'] == project_path
            assert os.path.isdir(project_path)
            assert os.path.isfile(os.path.join(project_path, 'main' + PY_EXT))
            assert os.path.isfile(os.path.join(project_path, BUILD_CONFIG_FILE))
            assert os.path.isdir(os.path.join(project_path, DOCS_FOLDER))
            assert not os.path.isdir(os.path.join(project_path, TEMPLATES_FOLDER))
            assert os.path.isdir(os.path.join(project_path, TESTS_FOLDER))

        finally:
            os.chdir(cur_dir)
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_renew_project_new_app_from_parent_via_path(self, mocked_app_options):
        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        pkg_name = "tst_app_prj"
        project_path = norm_path(os.path.join(parent_dir, pkg_name))
        mocked_app_options['group'] = ""
        mocked_app_options['namespace'] = ""
        mocked_app_options['package'] = ""
        mocked_app_options['path'] = pkg_name
        cur_dir = os.getcwd()
        try:
            os.makedirs(parent_dir)
            os.chdir(parent_dir)

            _renew_project(project_dev_vars(), APP_PRJ)

            app_pdv = project_dev_vars(project_path=pkg_name)
            assert app_pdv['project_type'] == APP_PRJ
            assert app_pdv['project_path'] == project_path
            assert os.path.isdir(project_path)
            assert os.path.isfile(os.path.join(project_path, 'main' + PY_EXT))
            assert os.path.isfile(os.path.join(project_path, BUILD_CONFIG_FILE))
            assert os.path.isdir(os.path.join(project_path, DOCS_FOLDER))
            assert not os.path.isdir(os.path.join(project_path, TEMPLATES_FOLDER))
            assert os.path.isdir(os.path.join(project_path, TESTS_FOLDER))

        finally:
            os.chdir(cur_dir)
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_renew_project_new_ns_module_from_parent_via_package(self, mocked_app_options):
        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        nsn = "mnx"
        ns_root_dir = norm_path(os.path.join(parent_dir, nsn + '_' + nsn))
        por_name = "tst_ns_mod_prj"
        pkg_name = nsn + '_' + por_name
        project_path = norm_path(os.path.join(parent_dir, pkg_name))
        mocked_app_options['branch'] = ""
        mocked_app_options['domain'] = ""
        mocked_app_options['group'] = ""
        mocked_app_options['namespace'] = nsn
        mocked_app_options['package'] = por_name
        mocked_app_options['path'] = ""
        mocked_app_options[_template_version_option(nsn + '.' + nsn)] = ""
        REGISTERED_TPL_PROJECTS[nsn] = {
            'import_name': nsn + '.' + nsn,
            'tpl_path': os.path.join(ns_root_dir, nsn, nsn, TEMPLATES_FOLDER),
            'version': '3.6.9'}
        cur_dir = os.getcwd()
        try:
            os.makedirs(ns_root_dir)
            write_file(os.path.join(ns_root_dir, REQ_DEV_FILE_NAME), "")    # to be extended with new ns module
            os.chdir(parent_dir)

            _renew_project(project_dev_vars(), MODULE_PRJ)

            new_pdv = project_dev_vars(project_path=pkg_name)
            assert new_pdv['project_type'] == MODULE_PRJ
            assert norm_path(pkg_name) == project_path
            assert new_pdv['project_path'] == project_path
            assert os.path.isdir(project_path)
            assert os.path.isfile(os.path.join(project_path, nsn, por_name + PY_EXT))
            assert not os.path.isfile(os.path.join(project_path, BUILD_CONFIG_FILE))
            assert not os.path.isdir(os.path.join(project_path, DOCS_FOLDER))
            assert not os.path.isdir(os.path.join(project_path, TEMPLATES_FOLDER))
            assert os.path.isdir(os.path.join(project_path, TESTS_FOLDER))

            assert os.path.isfile(os.path.join(nsn + '_' + nsn, REQ_DEV_FILE_NAME))
            assert read_file(os.path.join(nsn + '_' + nsn, REQ_DEV_FILE_NAME)) == pkg_name + os.linesep

            # repeat to check for no duplicate entry in dev-req-file
            with _in_prj_dir_venv(project_path):
                _cl(0, "git remote remove origin")  # remove remote to prevent ask for password in next git fetch
            _renew_project(project_dev_vars(project_path=project_path), MODULE_PRJ)
            assert os.path.isfile(os.path.join(nsn + '_' + nsn, REQ_DEV_FILE_NAME))
            assert read_file(os.path.join(nsn + '_' + nsn, REQ_DEV_FILE_NAME)) == pkg_name + os.linesep

            assert _git_current_branch(new_pdv) == f"new_{MODULE_PRJ}_{pkg_name}"

        finally:
            os.chdir(cur_dir)
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)
            del REGISTERED_TPL_PROJECTS[nsn]

    def test_renew_project_new_ns_package_from_parent_via_path(self, mocked_app_options):
        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        nsn = "mns"
        ns_root_dir = norm_path(os.path.join(parent_dir, nsn + '_' + nsn))
        por_name = "tst_ns_pkg_prj"
        pkg_name = nsn + '_' + por_name
        project_path = norm_path(os.path.join(parent_dir, pkg_name))
        mocked_app_options['branch'] = ""
        mocked_app_options['domain'] = ""
        mocked_app_options['group'] = ""
        mocked_app_options['namespace'] = nsn
        mocked_app_options['package'] = ""
        mocked_app_options['path'] = project_path
        mocked_app_options[_template_version_option(nsn + '.' + nsn)] = ""
        REGISTERED_TPL_PROJECTS[nsn] = {
            'import_name': nsn + '.' + nsn,
            'tpl_path': os.path.join(ns_root_dir, nsn, nsn, TEMPLATES_FOLDER),
            'version': '3.6.12'}
        cur_dir = os.getcwd()
        try:
            os.makedirs(ns_root_dir)
            write_file(os.path.join(ns_root_dir, REQ_DEV_FILE_NAME), pkg_name + os.linesep)
            os.chdir(parent_dir)

            _renew_project(project_dev_vars(), PACKAGE_PRJ)

            new_pdv = project_dev_vars(project_path=pkg_name)
            assert new_pdv['project_type'] == PACKAGE_PRJ
            assert norm_path(pkg_name) == project_path
            assert new_pdv['project_path'] == project_path
            assert os.path.isdir(project_path)
            assert os.path.isfile(os.path.join(project_path, nsn, por_name, PY_INIT))
            assert not os.path.isfile(os.path.join(project_path, BUILD_CONFIG_FILE))
            assert not os.path.isdir(os.path.join(project_path, DOCS_FOLDER))
            assert not os.path.isdir(os.path.join(project_path, TEMPLATES_FOLDER))
            assert os.path.isdir(os.path.join(project_path, TESTS_FOLDER))

            assert os.path.isfile(os.path.join(nsn + '_' + nsn, REQ_DEV_FILE_NAME))
            assert read_file(os.path.join(nsn + '_' + nsn, REQ_DEV_FILE_NAME)) == pkg_name + os.linesep  # no extend/dup

            assert _git_current_branch(new_pdv) == f"new_{PACKAGE_PRJ}_{pkg_name}"

        finally:
            del REGISTERED_TPL_PROJECTS[nsn]
            os.chdir(cur_dir)
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_renew_project_new_package_from_parent_via_package(self, mocked_app_options):
        parent_dir = os.path.join(TESTS_FOLDER, PARENT_FOLDERS[0])
        pkg_name = "new_tst_pkg_prj"
        project_path = norm_path(os.path.join(parent_dir, pkg_name))
        mocked_app_options['branch'] = ""
        mocked_app_options['domain'] = ""
        mocked_app_options['group'] = ""
        mocked_app_options['namespace'] = ""
        mocked_app_options['package'] = pkg_name
        mocked_app_options['path'] = ""
        cur_dir = os.getcwd()
        try:
            os.makedirs(parent_dir)
            os.chdir(parent_dir)

            _renew_project(project_dev_vars(), PACKAGE_PRJ)

            new_pdv = project_dev_vars(project_path=pkg_name)
            assert new_pdv['project_type'] == PACKAGE_PRJ
            assert norm_path(pkg_name) == project_path
            assert new_pdv['project_path'] == project_path
            assert os.path.isdir(project_path)
            assert os.path.isfile(os.path.join(project_path, pkg_name, PY_INIT))
            assert not os.path.isfile(os.path.join(project_path, BUILD_CONFIG_FILE))
            assert os.path.isdir(os.path.join(project_path, DOCS_FOLDER))
            assert not os.path.isdir(os.path.join(project_path, TEMPLATES_FOLDER))
            assert os.path.isdir(os.path.join(project_path, TESTS_FOLDER))

            assert _git_current_branch(new_pdv) == f"new_{PACKAGE_PRJ}_{pkg_name}"

        finally:
            os.chdir(cur_dir)
            if os.path.exists(parent_dir):
                shutil.rmtree(parent_dir)

    def test_template_projects_new_dev_req(self, mocked_app_options, module_repo_path):
        # mocked_app_options needed to reset all _template_version_options
        root_prj_imp_name = 'aedev.aedev'
        root_prj_pgk_name, root_prj_ver = package_version(root_prj_imp_name, list(REGISTERED_TPL_PROJECTS.keys()))
        assert root_prj_pgk_name == norm_name(root_prj_imp_name)
        assert root_prj_ver

        dev_req_list = []
        pdv = {'namespace_name': 'aedev', 'dev_require': dev_req_list, 'project_path': module_repo_path,
               'project_type': MODULE_PRJ}

        tpl_projects = _template_projects(pdv)
        assert len(tpl_projects) == 2
        assert tpl_projects[0]['import_name'] == root_prj_imp_name
        assert tpl_projects[0] == REGISTERED_TPL_PROJECTS[root_prj_imp_name + PACKAGE_VERSION_SEP + root_prj_ver]
        assert tpl_projects[1]['import_name'] == 'aedev.tpl_project'
        assert pdv['dev_require'] is dev_req_list
        assert len(pdv['dev_require']) == 2

    def test_template_projects_dev_req_lock(self, mocked_app_options, module_repo_path):
        # mocked_app_options needed to reset all _template_version_options
        dev_req_list = ['any_non_tpl_prj']
        drl_copy = dev_req_list.copy()
        pdv = {'namespace_name': 'aedev', 'dev_require': dev_req_list, 'project_path': module_repo_path,
               'project_type': MODULE_PRJ}

        tpl_projects = _template_projects(pdv)
        assert len(tpl_projects) == 0
        assert pdv['dev_require'] is dev_req_list
        assert pdv['dev_require'] == drl_copy

    def test_wait(self, mocked_app_options):
        mocked_app_options['delay'] = 0
        _wait()


@skip_gitlab_ci  # skip on gitlab because of missing remote repository user account token
class TestHiddenHelpersRemote:
    """ test private helper functions that need authentication against git remote hosts. """
    def test_init_act_exec_args_show_repo(self, mocked_app_options, patched_exit_call_wrapper):
        mocked_app_options['action'] = 'show_repo'
        mocked_app_options['arguments'] = []
        mocked_app_options['domain'] = 'gitlab.com'
        mocked_app_options['gitToken'] = "anyInvalidTstToken"
        mocked_app_options['group'] = 'tst_user'
        mocked_app_options['package'] = 'repo'
        mocked_app_options['namespace'] = ""
        mocked_app_options['path'] = ""

        patched_exit_call_wrapper(_init_act_exec_args)    # fails on authenticate

        mocked_app_options['gitToken'] = cae.get_variable('gitToken')
        ini_pdv, act_name, act_args = _init_act_exec_args()

        assert act_name == 'show_repo'
        assert act_args == []
        assert isinstance(ini_pdv, dict)    # PdvType
        assert 'remote_repo_api' in ini_pdv
        assert isinstance(ini_pdv['remote_repo_api'], GitlabCom)
        assert ini_pdv['repo_group'] == mocked_app_options['group']
        # package option will not be propagated onto ini_pdv['package_name']


def test_temp_context_teardown():
    assert aedev.git_repo_manager.TEMP_CONTEXT
    aedev.git_repo_manager.TEMP_CONTEXT.cleanup()
