"""
Module for running arbitrary tests
"""

import builtins
import logging
import os
import random
import sys
import time
import traceback

import salt.exceptions
import salt.loader
import salt.utils.args
import salt.utils.functools
import salt.utils.hashutils
import salt.utils.platform
import salt.utils.versions
import salt.version
from salt.config import DEFAULT_HASH_TYPE
from salt.utils.decorators import depends

__proxyenabled__ = ["*"]

# Don't shadow built-in's.
__func_alias__ = {
    "true_": "true",
    "false_": "false",
    "try_": "try",
}

log = logging.getLogger(__name__)


def __virtual__():
    return True


@depends("non_existantmodulename")
def missing_func():
    return "foo"


def attr_call():
    """
    Call grains.items via the attribute

    CLI Example:

    .. code-block:: bash

        salt '*' test.attr_call
    """
    return __salt__.grains.items()


def module_report():
    """
    Return a dict containing all of the execution modules with a report on
    the overall availability via different references

    CLI Example:

    .. code-block:: bash

        salt '*' test.module_report
    """
    ret = {
        "functions": [],
        "function_attrs": [],
        "function_subs": [],
        "modules": [],
        "module_attrs": [],
        "missing_attrs": [],
        "missing_subs": [],
    }
    for ref in __salt__:
        if "." in ref:
            ret["functions"].append(ref)
        else:
            ret["modules"].append(ref)
            if hasattr(__salt__, ref):
                ret["module_attrs"].append(ref)
            for func in __salt__[ref]:
                full = f"{ref}.{func}"
                if hasattr(getattr(__salt__, ref), func):
                    ret["function_attrs"].append(full)
                if func in __salt__[ref]:
                    ret["function_subs"].append(full)
    for func in ret["functions"]:
        if func not in ret["function_attrs"]:
            ret["missing_attrs"].append(func)
        if func not in ret["function_subs"]:
            ret["missing_subs"].append(func)
    return ret


def echo(text):
    """
    Return a string - used for testing the connection

    CLI Example:

    .. code-block:: bash

        salt '*' test.echo 'foo bar baz quo qux'
    """
    return text


def ping():
    """
    Used to make sure the minion is up and responding. Not an ICMP ping.

    Returns ``True``.

    CLI Example:

    .. code-block:: bash

        salt '*' test.ping
    """

    if not salt.utils.platform.is_proxy():
        log.debug("test.ping received for minion '%s'", __opts__.get("id"))
        return True
    else:
        ping_cmd = __opts__["proxy"]["proxytype"] + ".ping"
        if __opts__.get("add_proxymodule_to_opts", False):
            return __opts__["proxymodule"][ping_cmd]()
        else:
            return __proxy__[ping_cmd]()


def sleep(length):
    """
    Instruct the minion to initiate a process that will sleep for a given
    period of time.

    CLI Example:

    .. code-block:: bash

        salt '*' test.sleep 20
    """
    time.sleep(int(length))
    return True


def rand_sleep(max=60):
    """
    Sleep for a random number of seconds, used to test long-running commands
    and minions returning at differing intervals

    CLI Example:

    .. code-block:: bash

        salt '*' test.rand_sleep 60
    """
    time.sleep(random.randint(0, max))
    return True


def version():
    """
    Return the version of salt on the minion

    CLI Example:

    .. code-block:: bash

        salt '*' test.version
    """
    return salt.version.__version__


def versions_information():
    """
    Report the versions of dependent and system software

    CLI Example:

    .. code-block:: bash

        salt '*' test.versions_information
    """
    return salt.version.versions_information()


def versions_report():
    """
    Returns versions of components used by salt

    CLI Example:

    .. code-block:: bash

        salt '*' test.versions_report
    """
    return "\n".join(salt.version.versions_report())


versions = salt.utils.functools.alias_function(versions_report, "versions")


def conf_test():
    """
    Return the value for test.foo in the minion configuration file, or return
    the default value

    CLI Example:

    .. code-block:: bash

        salt '*' test.conf_test
    """
    return __salt__["config.option"]("test.foo")


def get_opts():
    """
    Return the configuration options passed to this minion

    CLI Example:

    .. code-block:: bash

        salt '*' test.get_opts
    """
    return __opts__


def cross_test(func, args=None):
    """
    Execute a minion function via the ``__salt__`` object in the test
    module, used to verify that the Minion functions can be called
    via the ``__salt__`` module.

    CLI Example:

    .. code-block:: bash

        salt '*' test.cross_test file.gid_to_group 0
    """
    if args is None:
        args = []
    return __salt__[func](*args)


def kwarg(**kwargs):
    """
    Print out the data passed into the function ``**kwargs``, this is used to
    both test the publication data and CLI ``kwarg`` passing, but also to display
    the information available within the publication data.

    CLI Example:

    .. code-block:: bash

        salt '*' test.kwarg num=1 txt="two" env='{a: 1, b: "hello"}'
    """
    return kwargs


def arg(*args, **kwargs):
    """
    Print out the data passed into the function ``*args`` and ``kwargs``, this
    is used to both test the publication data and CLI argument passing, but
    also to display the information available within the publication data.

    :return: ``{"args": args, "kwargs": kwargs}``
    :rtype: dict

    CLI Example:

    .. code-block:: bash

        salt '*' test.arg 1 "two" 3.1 txt="hello" wow='{a: 1, b: "hello"}'
    """
    return {"args": args, "kwargs": kwargs}


def arg_type(*args, **kwargs):
    """
    Print out the types of the ``args`` and ``kwargs``. This is used to test the types
    of the ``args`` and ``kwargs`` passed down to the Minion

    :rtype: dict

    CLI Example:

    .. code-block:: bash

           salt '*' test.arg_type 1 'int'
    """
    ret = {"args": [], "kwargs": {}}
    # all the args
    for argument in args:
        ret["args"].append(str(type(argument)))

    # all the kwargs
    for key, val in kwargs.items():
        ret["kwargs"][key] = str(type(val))

    return ret


def arg_repr(*args, **kwargs):
    """
    Print out the data passed into the function ``*args`` and ``kwargs``, this
    is used to both test the publication data and CLI argument passing, but
    also to display the information available within the publication data.

    :return: ``{"args": repr(args), "kwargs": repr(kwargs)}``

    CLI Example:

    .. code-block:: bash

        salt '*' test.arg_repr 1 "two" 3.1 txt="hello" wow='{a: 1, b: "hello"}'
    """
    return {"args": repr(args), "kwargs": repr(kwargs)}


def arg_clean(*args, **kwargs):
    """
    Like :mod:`test.arg <salt.modules.test.arg>` but cleans ``kwargs`` of the ``__pub*`` items

    CLI Example:

    .. code-block:: bash

        salt '*' test.arg_clean 1 "two" 3.1 txt="hello" wow='{a: 1, b: "hello"}'
    """
    return dict(args=args, kwargs=salt.utils.args.clean_kwargs(**kwargs))


def fib(num):
    """
    Return the ``num``-th Fibonacci number, and the time it took to compute in
    seconds. Used for performance tests.

    This function is designed to have terrible performance.

    CLI Example:

    .. code-block:: bash

        salt '*' test.fib 3
    """
    num = int(num)
    if num < 0:
        raise ValueError("Negative number is not allowed!")
    start = time.time()
    if num < 2:
        return num, time.time() - start

    prev = 0
    curr = 1
    i = 1
    while i < num:
        prev, curr = curr, prev + curr
        i += 1

    return curr, time.time() - start


def collatz(start):
    """
    Execute the collatz conjecture from the passed starting number,
    returns the sequence and the time it took to compute. Used for
    performance tests.

    CLI Example:

    .. code-block:: bash

        salt '*' test.collatz 3
    """
    start = int(start)
    begin = time.time()
    steps = []
    while start != 1:
        steps.append(start)
        if start > 1:
            if start % 2 == 0:
                start = start / 2
            else:
                start = start * 3 + 1
    return steps, time.time() - begin


def outputter(data):
    """
    Test the outputter, pass in data to return

    CLI Example:

    .. code-block:: bash

        salt '*' test.outputter foobar
    """
    return data


def retcode(code=42):
    """
    Test that the returncode system is functioning correctly

    CLI Example:

    .. code-block:: bash

        salt '*' test.retcode 42
    """
    __context__["retcode"] = code
    return True


def provider(module):
    """
    Pass in a function name to discover what provider is being used

    CLI Example:

    .. code-block:: bash

        salt '*' test.provider service
    """
    func = ""
    for key in __salt__:
        if not key.startswith(f"{module}."):
            continue
        func = key
        break
    if not func:
        return ""
    pfn = sys.modules[__salt__[func].__module__].__file__
    pfn = os.path.basename(pfn)
    return pfn[: pfn.rindex(".")]


def providers():
    """
    Return a dict of the provider names and the files that provided them

    CLI Example:

    .. code-block:: bash

        salt '*' test.providers
    """
    ret = {}
    for funcname in __salt__:
        modname = funcname.split(".")[0]
        if modname not in ret:
            ret[provider(modname)] = modname
    return ret


def not_loaded():
    """
    List the modules that were not loaded by the salt loader system

    CLI Example:

    .. code-block:: bash

        salt '*' test.not_loaded
    """
    prov = providers()
    ret = set()
    for mod_dir in salt.loader._module_dirs(__opts__, "modules", "module"):
        if not os.path.isabs(mod_dir):
            continue
        if not os.path.isdir(mod_dir):
            continue
        for fn_ in os.listdir(mod_dir):
            if fn_.startswith("_"):
                continue
            name = fn_.split(".")[0]
            if name not in prov:
                ret.add(name)
    return sorted(ret)


def opts_pkg():
    """
    Return an ``opts`` package with the ``grains`` and ``opts`` for this Minion.
    This is primarily used to create the options used for Master side
    state compiling routines

    CLI Example:

    .. code-block:: bash

        salt '*' test.opts_pkg
    """
    ret = {}
    ret.update(__opts__)
    ret["grains"] = __grains__.value()
    return ret


def rand_str(size=9999999999, hash_type=None):
    """
    This function has been renamed to
    :mod:`test.random_hash <salt.modules.test.random_hash>`. This function will stay to
    ensure backwards compatibility, but please switch to using the preferred name
    :mod:`test.random_hash <salt.modules.test.random_hash>`.
    """
    return random_hash(size=size, hash_type=hash_type)


def random_hash(size=9999999999, hash_type=None):
    """
    .. versionadded:: 2015.5.2
    .. versionchanged:: 2018.3.0
        Function has been renamed from ``test.rand_str`` to
        ``test.random_hash``

    Generates a random number between 1 and ``size``, then returns a hash of
    that number. If no ``hash_type`` is passed, the ``hash_type`` specified by the
    Minion's :conf_minion:`hash_type` config option is used.

    CLI Example:

    .. code-block:: bash

        salt '*' test.random_hash
        salt '*' test.random_hash hash_type=sha512
    """
    if not hash_type:
        hash_type = __opts__.get("hash_type", DEFAULT_HASH_TYPE)
    return salt.utils.hashutils.random_hash(size=size, hash_type=hash_type)


def exception(message="Test Exception"):
    """
    Raise an exception

    Optionally provide an error message or output the full stack.

    CLI Example:

    .. code-block:: bash

        salt '*' test.exception 'Oh noes!'
    """
    raise Exception(message)


def stack():
    """
    Return the current stack trace

    CLI Example:

    .. code-block:: bash

        salt '*' test.stack
    """
    return "".join(traceback.format_stack())


def tty(*args, **kwargs):  # pylint: disable=W0613
    """
    Deprecated! Moved to :mod:`cmd.tty <salt.modules.cmdmod.tty>`

    CLI Example:

    .. code-block:: bash

        salt '*' test.tty tty0 'This is a test'
        salt '*' test.tty pts3 'This is a test'
    """
    return "ERROR: This function has been moved to cmd.tty"


def try_(module, return_try_exception=False, **kwargs):
    """
    Try to run a module command. On an exception return ``None``.
    If ``return_try_exception`` is set to ``True``, return the exception.
    This can be helpful in templates where running a module might fail as expected.

    Jinja Example:

    .. code-block:: jinja

        {% for i in range(0,230) %}
            {{ salt['test.try'](module='ipmi.get_users', bmc_host='172.2.2.'+i)|yaml(False) }}
        {% endfor %}
    """
    try:
        return __salt__[module](**kwargs)
    except Exception as e:  # pylint: disable=broad-except
        if return_try_exception:
            return e
    return None


def assertion(assertion):
    """
    Assert the given argument

    CLI Example:

    .. code-block:: bash

        salt '*' test.assertion False
    """
    assert assertion


def true_():
    """
    Always return ``True``

    CLI Example:

    .. code-block:: bash

        salt '*' test.true
    """
    return True


def false_():
    """
    Always return ``False``

    CLI Example:

    .. code-block:: bash

        salt '*' test.false
    """
    return False


def raise_exception(name, *args, **kwargs):
    """
    Raise an exception. Built-in exceptions and those in
    :mod:`salt.exceptions <salt.internals.salt.exceptions>`
    can be raised by this test function. If no matching exception is found,
    then no exception will be raised and this function will return ``False``.

    This function is designed to test Salt's exception and return code
    handling.

    CLI Example:

    .. code-block:: bash

        salt '*' test.raise_exception TypeError "An integer is required"
        salt '*' test.raise_exception salt.exceptions.CommandExecutionError "Something went wrong"
    """

    def _is_exc(cls):
        for base in cls.__bases__:
            if base is BaseException:
                break
            else:
                return _is_exc(base)
        else:
            return False
        return True

    try:
        if name.startswith("salt.exceptions."):
            exc = getattr(salt.exceptions, name[16:])
        else:
            exc = getattr(builtins, name)
        if _is_exc(exc):
            raise exc(*args, **salt.utils.args.clean_kwargs(**kwargs))
        else:
            log.error("%s is not an exception", name)
            return False
    except AttributeError:
        log.error("No such exception: %s", name)
        return False


def deprecation_warning():
    r"""
    Return True, but also produce two DeprecationWarnings. One by date, the
    other by the codename - release Oganesson, which should correspond to Salt
    3108.

    CLI Example:

    .. code-block:: bash

        salt \* test.deprecation_warning
    """
    # This warn should always stay in Salt.
    salt.utils.versions.warn_until(
        3108,
        "This is a test deprecation warning by version.",
    )
    salt.utils.versions.warn_until_date(
        "30000101",
        "This is a test deprecation warning by date very far into the future ({date}).",
    )
    return True
