# -*- coding: utf-8 -*-
"""
    :codeauthor: Pedro Algarvio (pedro@algarvio.me)


    tests.integration.shell.call
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import

import logging
import os
import re
import shutil
import sys

import pytest
import salt.utils.files
import salt.utils.json
import salt.utils.platform
import salt.utils.yaml
from salt.ext import six
from tests.integration.utils import testprogram
from tests.support.case import ShellCase
from tests.support.helpers import change_cwd, flaky, slowTest, with_tempfile
from tests.support.mixins import ShellCaseCommonTestsMixin
from tests.support.runtests import RUNTIME_VARS
from tests.support.unit import skipIf

log = logging.getLogger(__name__)


@pytest.mark.windows_whitelisted
class CallTest(ShellCase, testprogram.TestProgramCase, ShellCaseCommonTestsMixin):

    _call_binary_ = "salt-call"

    @slowTest
    def test_default_output(self):
        out = self.run_call("-l quiet test.fib 3")

        expect = ["local:", "    - 2"]
        self.assertEqual(expect, out[:-1])

    @slowTest
    def test_text_output(self):
        out = self.run_call("-l quiet --out txt test.fib 3")

        expect = ["local: (2"]

        self.assertEqual("".join(expect), "".join(out).rsplit(",", 1)[0])

    @slowTest
    def test_json_out_indent(self):
        out = self.run_call("test.ping -l quiet --out=json --out-indent=-1")
        self.assertIn('"local": true', "".join(out))

        out = self.run_call("test.ping -l quiet --out=json --out-indent=0")
        self.assertIn('"local": true', "".join(out))

        out = self.run_call("test.ping -l quiet --out=json --out-indent=1")
        self.assertIn('"local": true', "".join(out))

    @slowTest
    def test_local_sls_call(self):
        fileroot = os.path.join(RUNTIME_VARS.FILES, "file", "base")
        out = self.run_call(
            "--file-root {0} state.sls saltcalllocal".format(fileroot), local=True
        )
        self.assertIn("Name: test.echo", "".join(out))
        self.assertIn("Result: True", "".join(out))
        self.assertIn("hello", "".join(out))
        self.assertIn("Succeeded: 1", "".join(out))

    @with_tempfile()
    @slowTest
    def test_local_salt_call(self, name):
        """
        This tests to make sure that salt-call does not execute the
        function twice, see https://github.com/saltstack/salt/pull/49552
        """

        def _run_call(cmd):
            cmd = "--out=json " + cmd
            return salt.utils.json.loads("".join(self.run_call(cmd, local=True)))[
                "local"
            ]

        ret = _run_call('state.single file.append name={0} text="foo"'.format(name))
        ret = ret[next(iter(ret))]

        # Make sure we made changes
        assert ret["changes"]

        # 2nd sanity check: make sure that "foo" only exists once in the file
        with salt.utils.files.fopen(name) as fp_:
            contents = fp_.read()
        assert contents.count("foo") == 1, contents

    @skipIf(
        salt.utils.platform.is_windows() or salt.utils.platform.is_darwin(),
        "This test requires a supported master",
    )
    @slowTest
    def test_user_delete_kw_output(self):
        ret = self.run_call("-l quiet -d user.delete")
        assert "salt '*' user.delete name remove=True force=True" in "".join(ret)

    @slowTest
    def test_salt_documentation_too_many_arguments(self):
        """
        Test to see if passing additional arguments shows an error
        """
        data = self.run_call("-d virtualenv.create /tmp/ve", catch_stderr=True)
        self.assertIn(
            "You can only get documentation for one method at one time",
            "\n".join(data[1]),
        )

    @slowTest
    def test_issue_6973_state_highstate_exit_code(self):
        """
        If there is no tops/master_tops or state file matches
        for this minion, salt-call should exit non-zero if invoked with
        option --retcode-passthrough
        """
        src = os.path.join(RUNTIME_VARS.BASE_FILES, "top.sls")
        dst = os.path.join(RUNTIME_VARS.BASE_FILES, "top.sls.bak")
        shutil.move(src, dst)
        expected_comment = "No states found for this minion"
        try:
            stdout, retcode = self.run_call(
                "-l quiet --retcode-passthrough state.highstate", with_retcode=True
            )
        finally:
            shutil.move(dst, src)
        self.assertIn(expected_comment, "".join(stdout))
        self.assertNotEqual(0, retcode)

    @skipIf(sys.platform.startswith("win"), "This test does not apply on Win")
    @skipIf(True, "to be re-enabled when #23623 is merged")
    def test_return(self):
        self.run_call('cmd.run "echo returnTOmaster"')
        jobs = [a for a in self.run_run("jobs.list_jobs")]

        self.assertTrue(True in ["returnTOmaster" in j for j in jobs])
        # lookback jid
        first_match = [(i, j) for i, j in enumerate(jobs) if "returnTOmaster" in j][0]
        jid, idx = None, first_match[0]
        while idx > 0:
            jid = re.match("([0-9]+):", jobs[idx])
            if jid:
                jid = jid.group(1)
                break
            idx -= 1
        assert idx > 0
        assert jid
        master_out = [a for a in self.run_run("jobs.lookup_jid {0}".format(jid))]
        self.assertTrue(True in ["returnTOmaster" in a for a in master_out])

    @skipIf(salt.utils.platform.is_windows(), "Skip on Windows")
    @slowTest
    def test_syslog_file_not_found(self):
        """
        test when log_file is set to a syslog file that does not exist
        """
        config_dir = os.path.join(RUNTIME_VARS.TMP, "log_file_incorrect")
        if not os.path.isdir(config_dir):
            os.makedirs(config_dir)

        with change_cwd(config_dir):
            with salt.utils.files.fopen(
                self.get_config_file_path("minion"), "r"
            ) as fh_:
                minion_config = salt.utils.yaml.load(fh_.read())
                minion_config["log_file"] = "file:///dev/doesnotexist"
                with salt.utils.files.fopen(
                    os.path.join(config_dir, "minion"), "w"
                ) as fh_:
                    fh_.write(
                        salt.utils.yaml.dump(minion_config, default_flow_style=False)
                    )
            ret = self.run_script(
                "salt-call",
                '--config-dir {0} cmd.run "echo foo"'.format(config_dir),
                timeout=120,
                catch_stderr=True,
                with_retcode=True,
            )
            try:
                if sys.version_info >= (3, 5, 4):
                    self.assertIn("local:", ret[0])
                    self.assertIn(
                        "[WARNING ] The log_file does not exist. Logging not setup correctly or syslog service not started.",
                        ret[1],
                    )
                    self.assertEqual(ret[2], 0)
                else:
                    self.assertIn(
                        "Failed to setup the Syslog logging handler", "\n".join(ret[1])
                    )
                    self.assertEqual(ret[2], 2)
            finally:
                if os.path.isdir(config_dir):
                    shutil.rmtree(config_dir)

    @skipIf(True, "This test is unreliable. Need to investigate why more deeply.")
    @flaky
    def test_issue_15074_output_file_append(self):
        output_file_append = os.path.join(RUNTIME_VARS.TMP, "issue-15074")
        try:
            # Let's create an initial output file with some data
            _ = self.run_script(
                "salt-call",
                "-c {0} --output-file={1} test.versions".format(
                    RUNTIME_VARS.TMP_MINION_CONF_DIR, output_file_append
                ),
                catch_stderr=True,
                with_retcode=True,
            )

            with salt.utils.files.fopen(output_file_append) as ofa:
                output = ofa.read()

            self.run_script(
                "salt-call",
                "-c {0} --output-file={1} --output-file-append test.versions".format(
                    self.config_dir, output_file_append
                ),
                catch_stderr=True,
                with_retcode=True,
            )
            with salt.utils.files.fopen(output_file_append) as ofa:
                self.assertEqual(ofa.read(), output + output)
        finally:
            if os.path.exists(output_file_append):
                os.unlink(output_file_append)

    @skipIf(True, "This test is unreliable. Need to investigate why more deeply.")
    @flaky
    def test_issue_14979_output_file_permissions(self):
        output_file = os.path.join(RUNTIME_VARS.TMP, "issue-14979")
        with salt.utils.files.set_umask(0o077):
            try:
                # Let's create an initial output file with some data
                self.run_script(
                    "salt-call",
                    "-c {0} --output-file={1} -l trace -g".format(
                        RUNTIME_VARS.TMP_MINION_CONF_DIR, output_file
                    ),
                    catch_stderr=True,
                    with_retcode=True,
                )
                try:
                    stat1 = os.stat(output_file)
                except OSError:
                    self.fail("Failed to generate output file, see log for details")

                # Let's change umask
                os.umask(0o777)  # pylint: disable=blacklisted-function

                self.run_script(
                    "salt-call",
                    "-c {0} --output-file={1} --output-file-append -g".format(
                        RUNTIME_VARS.TMP_MINION_CONF_DIR, output_file
                    ),
                    catch_stderr=True,
                    with_retcode=True,
                )
                try:
                    stat2 = os.stat(output_file)
                except OSError:
                    self.fail("Failed to generate output file, see log for details")
                self.assertEqual(stat1.st_mode, stat2.st_mode)
                # Data was appeneded to file
                self.assertTrue(stat1.st_size < stat2.st_size)

                # Let's remove the output file
                os.unlink(output_file)

                # Not appending data
                self.run_script(
                    "salt-call",
                    "-c {0} --output-file={1} -g".format(
                        RUNTIME_VARS.TMP_MINION_CONF_DIR, output_file
                    ),
                    catch_stderr=True,
                    with_retcode=True,
                )
                try:
                    stat3 = os.stat(output_file)
                except OSError:
                    self.fail("Failed to generate output file, see log for details")
                # Mode must have changed since we're creating a new log file
                self.assertNotEqual(stat1.st_mode, stat3.st_mode)
            finally:
                if os.path.exists(output_file):
                    os.unlink(output_file)

    @skipIf(sys.platform.startswith("win"), "This test does not apply on Win")
    @slowTest
    def test_42116_cli_pillar_override(self):
        ret = self.run_call(
            "state.apply issue-42116-cli-pillar-override "
            'pillar=\'{"myhost": "localhost"}\''
        )
        for line in ret:
            line = line.lstrip()
            if line == 'Comment: Command "ping -c 2 localhost" run':
                # Successful test
                break
        else:
            log.debug("salt-call output:\n\n%s", "\n".join(ret))
            self.fail("CLI pillar override not found in pillar data")

    @slowTest
    def test_pillar_items_masterless(self):
        """
        Test to ensure we get expected output
        from pillar.items with salt-call
        """
        get_items = self.run_call("pillar.items", local=True)
        exp_out = [
            "        - Lancelot",
            "        - Galahad",
            "        - Bedevere",
            "    monty:",
            "        python",
        ]
        for out in exp_out:
            self.assertIn(out, get_items)

    def tearDown(self):
        """
        Teardown method to remove installed packages
        """
        user = ""
        user_info = self.run_call(" grains.get username", local=True)
        if (
            user_info
            and isinstance(user_info, (list, tuple))
            and isinstance(user_info[-1], six.string_types)
        ):
            user = user_info[-1].strip()
        super(CallTest, self).tearDown()

    @slowTest
    def test_exit_status_unknown_argument(self):
        """
        Ensure correct exit status when an unknown argument is passed to salt-call.
        """

        call = testprogram.TestProgramSaltCall(
            name="unknown_argument", parent_dir=self._test_dir,
        )
        # Call setup here to ensure config and script exist
        call.setup()
        stdout, stderr, status = call.run(
            args=["--unknown-argument"], catch_stderr=True, with_retcode=True,
        )
        self.assert_exit_status(
            status, "EX_USAGE", message="unknown argument", stdout=stdout, stderr=stderr
        )

    @slowTest
    def test_masterless_highstate(self):
        """
        test state.highstate in masterless mode
        """
        ret = self.run_call("state.highstate", local=True)

        destpath = os.path.join(RUNTIME_VARS.TMP, "testfile")
        exp_out = [
            "    Function: file.managed",
            "      Result: True",
            "          ID: {0}".format(destpath),
        ]

        for out in exp_out:
            self.assertIn(out, ret)

        self.assertTrue(os.path.exists(destpath))

    @slowTest
    def test_exit_status_correct_usage(self):
        """
        Ensure correct exit status when salt-call starts correctly.
        """

        call = testprogram.TestProgramSaltCall(
            name="correct_usage", parent_dir=self._test_dir,
        )
        # Call setup here to ensure config and script exist
        call.setup()
        stdout, stderr, status = call.run(
            args=["--local", "test.true"], catch_stderr=True, with_retcode=True,
        )
        self.assert_exit_status(
            status, "EX_OK", message="correct usage", stdout=stdout, stderr=stderr
        )
