import copy
import io
import os
import shutil
import sys
import tempfile
import textwrap

import pytest

import salt.config
import salt.loader
import salt.utils.files
from salt.state import HighState
from salt.utils.pydsl import PyDslError
from tests.support.helpers import with_tempdir
from tests.support.runtests import RUNTIME_VARS
from tests.support.unit import TestCase

REQUISITES = ["require", "require_in", "use", "use_in", "watch", "watch_in"]


class CommonTestCaseBoilerplate(TestCase):
    def setUp(self):
        self.root_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
        self.addCleanup(shutil.rmtree, self.root_dir, ignore_errors=True)
        self.state_tree_dir = os.path.join(self.root_dir, "state_tree")
        self.cache_dir = os.path.join(self.root_dir, "cachedir")
        if not os.path.isdir(self.root_dir):
            os.makedirs(self.root_dir)

        if not os.path.isdir(self.state_tree_dir):
            os.makedirs(self.state_tree_dir)

        if not os.path.isdir(self.cache_dir):
            os.makedirs(self.cache_dir)
        self.config = salt.config.minion_config(None)
        self.config["root_dir"] = self.root_dir
        self.config["state_events"] = False
        self.config["id"] = "match"
        self.config["file_client"] = "local"
        self.config["file_roots"] = dict(base=[self.state_tree_dir])
        self.config["cachedir"] = self.cache_dir
        self.config["test"] = False
        self.config["grains"] = salt.loader.grains(self.config)
        self.HIGHSTATE = HighState(self.config)
        self.HIGHSTATE.push_active()

    def tearDown(self):
        try:
            self.HIGHSTATE.pop_active()
        except IndexError:
            pass
        del self.config
        del self.HIGHSTATE

    def state_highstate(self, state, dirpath):
        opts = copy.copy(self.config)
        opts["file_roots"] = dict(base=[dirpath])
        HIGHSTATE = HighState(opts)
        HIGHSTATE.push_active()
        try:
            high, errors = HIGHSTATE.render_highstate(state)
            if errors:
                import pprint

                pprint.pprint("\n".join(errors))
                pprint.pprint(high)

            out = HIGHSTATE.state.call_high(high)
            # pprint.pprint(out)
        finally:
            HIGHSTATE.pop_active()


class PyDSLRendererTestCase(CommonTestCaseBoilerplate):
    """
    WARNING: If tests in here are flaky, they may need
    to be moved to their own class. Sharing HighState, especially
    through setUp/tearDown can create dangerous race conditions!
    """

    def render_sls(self, content, sls="", saltenv="base", **kws):
        if "env" in kws:
            # "env" is not supported; Use "saltenv".
            kws.pop("env")

        return self.HIGHSTATE.state.rend["pydsl"](
            io.StringIO(content), saltenv=saltenv, sls=sls, **kws
        )

    @pytest.mark.slow_test
    def test_state_declarations(self):
        result = self.render_sls(
            textwrap.dedent(
                """
            state('A').cmd.run('ls -la', cwd='/var/tmp')
            state().file.managed('myfile.txt', source='salt://path/to/file')
            state('X').cmd('run', 'echo hello world', cwd='/')

            a_cmd = state('A').cmd
            a_cmd.run(shell='/bin/bash')
            state('A').service.running(name='apache')
        """
            )
        )
        self.assertTrue("A" in result and "X" in result)
        A_cmd = result["A"]["cmd"]
        self.assertEqual(A_cmd[0], "run")
        self.assertEqual(A_cmd[1]["name"], "ls -la")
        self.assertEqual(A_cmd[2]["cwd"], "/var/tmp")
        self.assertEqual(A_cmd[3]["shell"], "/bin/bash")

        A_service = result["A"]["service"]
        self.assertEqual(A_service[0], "running")
        self.assertEqual(A_service[1]["name"], "apache")

        X_cmd = result["X"]["cmd"]
        self.assertEqual(X_cmd[0], "run")
        self.assertEqual(X_cmd[1]["name"], "echo hello world")
        self.assertEqual(X_cmd[2]["cwd"], "/")

        del result["A"]
        del result["X"]
        self.assertEqual(len(result), 2)
        # 2 rather than 1 because pydsl adds an extra no-op state
        # declaration.

        s_iter = iter(result.values())
        try:
            s = next(s_iter)["file"]
        except KeyError:
            s = next(s_iter)["file"]
        self.assertEqual(s[0], "managed")
        self.assertEqual(s[1]["name"], "myfile.txt")
        self.assertEqual(s[2]["source"], "salt://path/to/file")

    @pytest.mark.slow_test
    def test_requisite_declarations(self):
        result = self.render_sls(
            textwrap.dedent(
                """
            state('X').cmd.run('echo hello')
            state('A').cmd.run('mkdir tmp', cwd='/var')
            state('B').cmd.run('ls -la', cwd='/var/tmp') \
                        .require(state('X').cmd) \
                        .require(cmd='A') \
                        .watch(service='G')
            state('G').service.running(name='collectd')
            state('G').service.watch_in(state('A').cmd)

            state('H').cmd.require_in(cmd='echo hello')
            state('H').cmd.run('echo world')
        """
            )
        )
        self.assertEqual(len(result), 6)
        self.assertTrue(set("X A B G H".split()).issubset(set(result.keys())))
        b = result["B"]["cmd"]
        self.assertEqual(b[0], "run")
        self.assertEqual(b[1]["name"], "ls -la")
        self.assertEqual(b[2]["cwd"], "/var/tmp")
        self.assertEqual(b[3]["require"][0]["cmd"], "X")
        self.assertEqual(b[4]["require"][0]["cmd"], "A")
        self.assertEqual(b[5]["watch"][0]["service"], "G")
        self.assertEqual(result["G"]["service"][2]["watch_in"][0]["cmd"], "A")
        self.assertEqual(result["H"]["cmd"][1]["require_in"][0]["cmd"], "echo hello")

    @pytest.mark.slow_test
    def test_include_extend(self):
        result = self.render_sls(
            textwrap.dedent(
                """
            include(
                'some.sls.file',
                'another.sls.file',
                'more.sls.file',
                delayed=True
            )
            A = state('A').cmd.run('echo hoho', cwd='/')
            state('B').cmd.run('echo hehe', cwd='/')
            extend(
                A,
                state('X').cmd.run(cwd='/a/b/c'),
                state('Y').file('managed', name='a_file.txt'),
                state('Z').service.watch(file='A')
            )
        """
            )
        )
        self.assertEqual(len(result), 4)
        self.assertEqual(
            result["include"],
            [
                {"base": sls}
                for sls in ("some.sls.file", "another.sls.file", "more.sls.file")
            ],
        )
        extend = result["extend"]
        self.assertEqual(extend["X"]["cmd"][0], "run")
        self.assertEqual(extend["X"]["cmd"][1]["cwd"], "/a/b/c")
        self.assertEqual(extend["Y"]["file"][0], "managed")
        self.assertEqual(extend["Y"]["file"][1]["name"], "a_file.txt")
        self.assertEqual(len(extend["Z"]["service"]), 1)
        self.assertEqual(extend["Z"]["service"][0]["watch"][0]["file"], "A")

        self.assertEqual(result["B"]["cmd"][0], "run")
        self.assertTrue("A" not in result)
        self.assertEqual(extend["A"]["cmd"][0], "run")

    @pytest.mark.slow_test
    def test_cmd_call(self):
        result = self.HIGHSTATE.state.call_template_str(
            textwrap.dedent(
                """\
            #!pydsl
            state('A').cmd.run('echo this is state A', cwd='/')

            some_var = 12345
            def do_something(a, b, *args, **kws):
                return dict(result=True, changes={'a': a, 'b': b, 'args': args, 'kws': kws, 'some_var': some_var})

            state('C').cmd.call(do_something, 1, 2, 3, x=1, y=2) \
                          .require(state('A').cmd)

            state('G').cmd.wait('echo this is state G', cwd='/') \
                          .watch(state('C').cmd)
        """
            )
        )
        ret = next(result[k] for k in result.keys() if "do_something" in k)
        changes = ret["changes"]
        self.assertEqual(
            changes, dict(a=1, b=2, args=(3,), kws=dict(x=1, y=2), some_var=12345)
        )

        ret = next(result[k] for k in result.keys() if "-G_" in k)
        self.assertEqual(ret["changes"]["stdout"], "this is state G")

    @pytest.mark.slow_test
    def test_multiple_state_func_in_state_mod(self):
        with self.assertRaisesRegex(PyDslError, "Multiple state functions"):
            self.render_sls(
                textwrap.dedent(
                    """
                state('A').cmd.run('echo hoho')
                state('A').cmd.wait('echo hehe')
            """
                )
            )

    @pytest.mark.slow_test
    def test_no_state_func_in_state_mod(self):
        with self.assertRaisesRegex(PyDslError, "No state function specified"):
            self.render_sls(
                textwrap.dedent(
                    """
                state('B').cmd.require(cmd='hoho')
            """
                )
            )

    @pytest.mark.slow_test
    def test_load_highstate(self):
        result = self.render_sls(
            textwrap.dedent(
                '''
            import salt.utils.yaml
            __pydsl__.load_highstate(salt.utils.yaml.safe_load("""
            A:
              cmd.run:
                - name: echo hello
                - cwd: /
            B:
              pkg:
                - installed
              service:
                - running
                - require:
                  - pkg: B
                - watch:
                  - cmd: A
            """))

            state('A').cmd.run(name='echo hello world')
            '''
            )
        )
        self.assertEqual(len(result), 3)
        self.assertEqual(result["A"]["cmd"][0], "run")
        self.assertIn({"name": "echo hello"}, result["A"]["cmd"])
        self.assertIn({"cwd": "/"}, result["A"]["cmd"])
        self.assertIn({"name": "echo hello world"}, result["A"]["cmd"])
        self.assertEqual(len(result["A"]["cmd"]), 4)

        self.assertEqual(len(result["B"]["pkg"]), 1)
        self.assertEqual(result["B"]["pkg"][0], "installed")

        self.assertEqual(result["B"]["service"][0], "running")
        self.assertIn({"require": [{"pkg": "B"}]}, result["B"]["service"])
        self.assertIn({"watch": [{"cmd": "A"}]}, result["B"]["service"])
        self.assertEqual(len(result["B"]["service"]), 3)

    @pytest.mark.slow_test
    def test_ordered_states(self):
        result = self.render_sls(
            textwrap.dedent(
                """
            __pydsl__.set(ordered=True)
            A = state('A')
            state('B').cmd.run('echo bbbb')
            A.cmd.run('echo aaa')
            state('B').cmd.run(cwd='/')
            state('C').cmd.run('echo ccc')
            state('B').file.managed(source='/a/b/c')
            """
            )
        )
        self.assertEqual(len(result["B"]["cmd"]), 3)
        self.assertEqual(result["A"]["cmd"][1]["require"][0]["cmd"], "B")
        self.assertEqual(result["C"]["cmd"][1]["require"][0]["cmd"], "A")
        self.assertEqual(result["B"]["file"][1]["require"][0]["cmd"], "C")

    @with_tempdir()
    @pytest.mark.slow_test
    def test_pipe_through_stateconf(self, dirpath):
        output = os.path.join(dirpath, "output")
        write_to(
            os.path.join(dirpath, "xxx.sls"),
            textwrap.dedent(
                """#!stateconf -os yaml . jinja
            .X:
              cmd.run:
                - name: echo X >> {0}
                - cwd: /
            .Y:
              cmd.run:
                - name: echo Y >> {0}
                - cwd: /
            .Z:
              cmd.run:
                - name: echo Z >> {0}
                - cwd: /
            """.format(
                    output.replace("\\", "/")
                )
            ),
        )
        write_to(
            os.path.join(dirpath, "yyy.sls"),
            textwrap.dedent(
                """\
            #!pydsl|stateconf -ps

            __pydsl__.set(ordered=True)
            state('.D').cmd.run('echo D >> {0}', cwd='/')
            state('.E').cmd.run('echo E >> {0}', cwd='/')
            state('.F').cmd.run('echo F >> {0}', cwd='/')
            """.format(
                    output.replace("\\", "/")
                )
            ),
        )

        write_to(
            os.path.join(dirpath, "aaa.sls"),
            textwrap.dedent(
                """\
            #!pydsl|stateconf -ps

            include('xxx', 'yyy')

            # make all states in xxx run BEFORE states in this sls.
            extend(state('.start').stateconf.require(stateconf='xxx::goal'))

            # make all states in yyy run AFTER this sls.
            extend(state('.goal').stateconf.require_in(stateconf='yyy::start'))

            __pydsl__.set(ordered=True)

            state('.A').cmd.run('echo A >> {0}', cwd='/')
            state('.B').cmd.run('echo B >> {0}', cwd='/')
            state('.C').cmd.run('echo C >> {0}', cwd='/')
            """.format(
                    output.replace("\\", "/")
                )
            ),
        )

        self.state_highstate({"base": ["aaa"]}, dirpath)
        with salt.utils.files.fopen(output, "r") as f:
            self.assertEqual("".join(f.read().split()), "XYZABCDEF")

    @with_tempdir()
    @pytest.mark.slow_test
    def test_compile_time_state_execution(self, dirpath):
        if not sys.stdin.isatty():
            self.skipTest("Not attached to a TTY")
        # The Windows shell will include any spaces before the redirect
        # in the text that is redirected.
        # For example: echo hello > test.txt will contain "hello "
        write_to(
            os.path.join(dirpath, "aaa.sls"),
            textwrap.dedent(
                """\
            #!pydsl

            __pydsl__.set(ordered=True)
            A = state('A')
            A.cmd.run('echo hehe>{0}/zzz.txt', cwd='/')
            A.file.managed('{0}/yyy.txt', source='salt://zzz.txt')
            A()
            A()

            state().cmd.run('echo hoho>>{0}/yyy.txt', cwd='/')

            A.file.managed('{0}/xxx.txt', source='salt://zzz.txt')
            A()
            """.format(
                    dirpath.replace("\\", "/")
                )
            ),
        )
        self.state_highstate({"base": ["aaa"]}, dirpath)
        with salt.utils.files.fopen(os.path.join(dirpath, "yyy.txt"), "rt") as f:
            self.assertEqual(f.read(), "hehe" + os.linesep + "hoho" + os.linesep)
        with salt.utils.files.fopen(os.path.join(dirpath, "xxx.txt"), "rt") as f:
            self.assertEqual(f.read(), "hehe" + os.linesep)

    @with_tempdir()
    @pytest.mark.slow_test
    def test_nested_high_state_execution(self, dirpath):
        output = os.path.join(dirpath, "output")
        write_to(
            os.path.join(dirpath, "aaa.sls"),
            textwrap.dedent(
                """\
            #!pydsl
            __salt__['state.sls']('bbb')
            state().cmd.run('echo bbbbbb', cwd='/')
            """
            ),
        )
        write_to(
            os.path.join(dirpath, "bbb.sls"),
            textwrap.dedent(
                """
            # {{ salt['state.sls']('ccc') }}
            test:
              cmd.run:
                - name: echo bbbbbbb
                - cwd: /
            """
            ),
        )
        write_to(
            os.path.join(dirpath, "ccc.sls"),
            textwrap.dedent(
                """
            #!pydsl
            state().cmd.run('echo ccccc', cwd='/')
            """
            ),
        )
        self.state_highstate({"base": ["aaa"]}, dirpath)

    @with_tempdir()
    @pytest.mark.slow_test
    def test_repeat_includes(self, dirpath):
        output = os.path.join(dirpath, "output")
        write_to(
            os.path.join(dirpath, "b.sls"),
            textwrap.dedent(
                """\
            #!pydsl
            include('c')
            include('d')
            """
            ),
        )
        write_to(
            os.path.join(dirpath, "c.sls"),
            textwrap.dedent(
                """\
            #!pydsl
            modtest = include('e')
            modtest.success
            """
            ),
        )
        write_to(
            os.path.join(dirpath, "d.sls"),
            textwrap.dedent(
                """\
            #!pydsl
            modtest = include('e')
            modtest.success
            """
            ),
        )
        write_to(
            os.path.join(dirpath, "e.sls"),
            textwrap.dedent(
                """\
            #!pydsl
            success = True
            """
            ),
        )
        self.state_highstate({"base": ["b"]}, dirpath)
        self.state_highstate({"base": ["c", "d"]}, dirpath)


def write_to(fpath, content):
    with salt.utils.files.fopen(fpath, "w") as f:
        f.write(content)
