# -*- coding: utf-8 -*-
"""
    :codeauthor: Jayesh Kariya <jayeshk@saltstack.com>
"""
from __future__ import absolute_import, print_function, unicode_literals

import os
import tempfile
import time

import salt.config
import salt.loader
import salt.states.saltmod as saltmod
import salt.utils.event
import salt.utils.jid
from tests.support.helpers import slowTest
from tests.support.mixins import LoaderModuleMockMixin
from tests.support.mock import MagicMock, patch
from tests.support.runtests import RUNTIME_VARS
from tests.support.unit import TestCase


class SaltmodTestCase(TestCase, LoaderModuleMockMixin):
    """
    Test cases for salt.states.saltmod
    """

    def setup_loader_modules(self):
        utils = salt.loader.utils(
            salt.config.DEFAULT_MINION_OPTS.copy(), whitelist=["state"]
        )
        return {
            saltmod: {
                "__env__": "base",
                "__opts__": {
                    "__role": "master",
                    "file_client": "remote",
                    "sock_dir": tempfile.mkdtemp(dir=RUNTIME_VARS.TMP),
                    "transport": "tcp",
                },
                "__salt__": {"saltutil.cmd": MagicMock()},
                "__orchestration_jid__": salt.utils.jid.gen_jid({}),
                "__utils__": utils,
            }
        }

    # 'state' function tests: 1

    @slowTest
    def test_state(self):
        """
        Test to invoke a state run on a given target
        """
        name = "state"
        tgt = "minion1"

        comt = "Passed invalid value for 'allow_fail', must be an int"

        ret = {"name": name, "changes": {}, "result": False, "comment": comt}

        test_ret = {
            "name": name,
            "changes": {},
            "result": True,
            "comment": "States ran successfully.",
        }

        test_batch_return = {
            "minion1": {
                "ret": {
                    "test_|-notify_me_|-this is a name_|-show_notification": {
                        "comment": "Notify me",
                        "name": "this is a name",
                        "start_time": "10:43:41.487565",
                        "result": True,
                        "duration": 0.35,
                        "__run_num__": 0,
                        "__sls__": "demo",
                        "changes": {},
                        "__id__": "notify_me",
                    },
                    "retcode": 0,
                },
                "out": "highstate",
            },
            "minion2": {
                "ret": {
                    "test_|-notify_me_|-this is a name_|-show_notification": {
                        "comment": "Notify me",
                        "name": "this is a name",
                        "start_time": "10:43:41.487565",
                        "result": True,
                        "duration": 0.35,
                        "__run_num__": 0,
                        "__sls__": "demo",
                        "changes": {},
                        "__id__": "notify_me",
                    },
                    "retcode": 0,
                },
                "out": "highstate",
            },
            "minion3": {
                "ret": {
                    "test_|-notify_me_|-this is a name_|-show_notification": {
                        "comment": "Notify me",
                        "name": "this is a name",
                        "start_time": "10:43:41.487565",
                        "result": True,
                        "duration": 0.35,
                        "__run_num__": 0,
                        "__sls__": "demo",
                        "changes": {},
                        "__id__": "notify_me",
                    },
                    "retcode": 0,
                },
                "out": "highstate",
            },
        }

        self.assertDictEqual(saltmod.state(name, tgt, allow_fail="a"), ret)

        comt = "No highstate or sls specified, no execution made"
        ret.update({"comment": comt})
        self.assertDictEqual(saltmod.state(name, tgt), ret)

        comt = "Must pass in boolean for value of 'concurrent'"
        ret.update({"comment": comt})
        self.assertDictEqual(
            saltmod.state(name, tgt, highstate=True, concurrent="a"), ret
        )

        ret.update({"comment": comt, "result": None})
        with patch.dict(saltmod.__opts__, {"test": True}):
            self.assertDictEqual(saltmod.state(name, tgt, highstate=True), test_ret)

        ret.update(
            {
                "comment": "States ran successfully. No changes made to silver.",
                "result": True,
                "__jid__": "20170406104341210934",
            }
        )
        with patch.dict(saltmod.__opts__, {"test": False}):
            mock = MagicMock(
                return_value={
                    "silver": {
                        "jid": "20170406104341210934",
                        "retcode": 0,
                        "ret": {
                            "test_|-notify_me_|-this is a name_|-show_notification": {
                                "comment": "Notify me",
                                "name": "this is a name",
                                "start_time": "10:43:41.487565",
                                "result": True,
                                "duration": 0.35,
                                "__run_num__": 0,
                                "__sls__": "demo",
                                "changes": {},
                                "__id__": "notify_me",
                            }
                        },
                        "out": "highstate",
                    }
                }
            )
            with patch.dict(saltmod.__salt__, {"saltutil.cmd": mock}):
                self.assertDictEqual(saltmod.state(name, tgt, highstate=True), ret)

        ret.update(
            {
                "comment": "States ran successfully. No changes made to minion1, minion3, minion2."
            }
        )
        del ret["__jid__"]
        with patch.dict(saltmod.__opts__, {"test": False}):
            with patch.dict(
                saltmod.__salt__,
                {"saltutil.cmd": MagicMock(return_value=test_batch_return)},
            ):
                state_run = saltmod.state(name, tgt, highstate=True)

                # Test return without checking the comment contents. Comments are tested later.
                comment = state_run.pop("comment")
                ret.pop("comment")
                self.assertDictEqual(state_run, ret)

                # Check the comment contents in a non-order specific way (ordering fails sometimes on PY3)
                self.assertIn("States ran successfully. No changes made to", comment)
                for minion in ["minion1", "minion2", "minion3"]:
                    self.assertIn(minion, comment)

    # 'function' function tests: 1

    @slowTest
    def test_function(self):
        """
        Test to execute a single module function on a remote
        minion via salt or salt-ssh
        """
        name = "state"
        tgt = "larry"

        ret = {
            "name": name,
            "changes": {},
            "result": None,
            "comment": "Function state would be executed " "on target {0}".format(tgt),
        }

        with patch.dict(saltmod.__opts__, {"test": True}):
            self.assertDictEqual(saltmod.function(name, tgt), ret)

        ret.update(
            {
                "result": True,
                "changes": {"out": "highstate", "ret": {tgt: ""}},
                "comment": "Function ran successfully."
                " Function state ran on {0}.".format(tgt),
            }
        )
        with patch.dict(saltmod.__opts__, {"test": False}):
            mock_ret = {"larry": {"ret": "", "retcode": 0, "failed": False}}
            mock_cmd = MagicMock(return_value=mock_ret)
            with patch.dict(saltmod.__salt__, {"saltutil.cmd": mock_cmd}):
                self.assertDictEqual(saltmod.function(name, tgt), ret)

    # 'wait_for_event' function tests: 1

    def test_wait_for_event(self):
        """
        Test to watch Salt's event bus and block until a condition is met
        """
        name = "state"
        tgt = "minion1"

        comt = "Timeout value reached."

        ret = {"name": name, "changes": {}, "result": False, "comment": comt}

        class Mockevent(object):
            """
            Mock event class
            """

            flag = None

            def __init__(self):
                self.full = None

            def get_event(self, full):
                """
                Mock get_event method
                """
                self.full = full
                if self.flag:
                    return {"tag": name, "data": {}}
                return None

        with patch.object(
            salt.utils.event, "get_event", MagicMock(return_value=Mockevent())
        ):
            with patch.dict(saltmod.__opts__, {"sock_dir": True, "transport": True}):
                with patch.object(time, "time", MagicMock(return_value=1.0)):
                    self.assertDictEqual(
                        saltmod.wait_for_event(name, "salt", timeout=-1.0), ret
                    )

                    Mockevent.flag = True
                    ret.update(
                        {"comment": "All events seen in 0.0 seconds.", "result": True}
                    )
                    self.assertDictEqual(saltmod.wait_for_event(name, ""), ret)

                    ret.update({"comment": "Timeout value reached.", "result": False})
                    self.assertDictEqual(
                        saltmod.wait_for_event(name, tgt, timeout=-1.0), ret
                    )

    # 'runner' function tests: 1

    def test_runner(self):
        """
        Test to execute a runner module on the master
        """
        name = "state"

        ret = {
            "changes": {"return": True},
            "name": "state",
            "result": True,
            "comment": "Runner function 'state' executed.",
            "__orchestration__": True,
        }
        runner_mock = MagicMock(return_value={"return": True})

        with patch.dict(saltmod.__salt__, {"saltutil.runner": runner_mock}):
            self.assertDictEqual(saltmod.runner(name), ret)

    # 'wheel' function tests: 1

    def test_wheel(self):
        """
        Test to execute a wheel module on the master
        """
        name = "state"

        ret = {
            "changes": {"return": True},
            "name": "state",
            "result": True,
            "comment": "Wheel function 'state' executed.",
            "__orchestration__": True,
        }
        wheel_mock = MagicMock(return_value={"return": True})

        with patch.dict(saltmod.__salt__, {"saltutil.wheel": wheel_mock}):
            self.assertDictEqual(saltmod.wheel(name), ret)

    @slowTest
    def test_state_ssh(self):
        """
        Test saltmod passes roster to saltutil.cmd
        """
        origcmd = saltmod.__salt__["saltutil.cmd"]
        cmd_kwargs = {}
        cmd_args = []

        def cmd_mock(*args, **kwargs):
            cmd_args.extend(args)
            cmd_kwargs.update(kwargs)
            return origcmd(*args, **kwargs)

        with patch.dict(saltmod.__salt__, {"saltutil.cmd": cmd_mock}):
            ret = saltmod.state(
                "state.sls", tgt="*", ssh=True, highstate=True, roster="my_roster"
            )
        assert "roster" in cmd_kwargs
        assert cmd_kwargs["roster"] == "my_roster"


class StatemodTests(TestCase, LoaderModuleMockMixin):
    def setup_loader_modules(self):
        self.tmp_cachedir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
        return {
            saltmod: {
                "__env__": "base",
                "__opts__": {
                    "id": "webserver2",
                    "argv": [],
                    "__role": "master",
                    "cachedir": self.tmp_cachedir,
                    "extension_modules": os.path.join(self.tmp_cachedir, "extmods"),
                },
                "__salt__": {"saltutil.cmd": MagicMock()},
                "__orchestration_jid__": salt.utils.jid.gen_jid({}),
            }
        }

    def test_statemod_state(self):
        """ Smoke test for for salt.states.statemod.state().  Ensures that we
            don't take an exception if optional parameters are not specified in
            __opts__ or __env__.
        """
        args = ("webserver_setup", "webserver2")
        kwargs = {
            "tgt_type": "glob",
            "fail_minions": None,
            "pillar": None,
            "top": None,
            "batch": None,
            "orchestration_jid": None,
            "sls": "vroom",
            "queue": False,
            "concurrent": False,
            "highstate": None,
            "expr_form": None,
            "ret": "",
            "ssh": False,
            "timeout": None,
            "test": False,
            "allow_fail": 0,
            "saltenv": None,
            "expect_minions": False,
        }
        ret = saltmod.state(*args, **kwargs)
        expected = {
            "comment": "States ran successfully.",
            "changes": {},
            "name": "webserver_setup",
            "result": True,
        }
        self.assertEqual(ret, expected)
