import textwrap

import salt.modules.parallels as parallels
from salt.exceptions import CommandExecutionError, SaltInvocationError
from tests.support.mixins import LoaderModuleMockMixin
from tests.support.mock import MagicMock, patch
from tests.support.unit import TestCase


class ParallelsTestCase(TestCase, LoaderModuleMockMixin):
    """
    Test parallels desktop execution module functions
    """

    def setup_loader_modules(self):
        return {parallels: {}}

    def test__normalize_args(self):
        """
        Test parallels._normalize_args
        """

        def _validate_ret(ret):
            """
            Assert that the returned data is a list of strings
            """
            self.assertTrue(isinstance(ret, list))
            for arg in ret:
                self.assertTrue(isinstance(arg, str))

        # Validate string arguments
        str_args = (
            "electrolytes --aqueous --anion hydroxide --cation=ammonium free radicals"
            " -- hydrogen"
        )
        _validate_ret(parallels._normalize_args(str_args))

        # Validate list arguments
        list_args = " ".join(str_args)
        _validate_ret(parallels._normalize_args(list_args))

        # Validate tuple arguments
        tuple_args = tuple(list_args)
        _validate_ret(parallels._normalize_args(tuple_args))

        # Validate dictionary arguments
        other_args = {"anion": "hydroxide", "cation": "ammonium"}
        _validate_ret(parallels._normalize_args(other_args))

    def test__find_guids(self):
        """
        Test parallels._find_guids
        """
        guid_str = textwrap.dedent(
            """
            PARENT_SNAPSHOT_ID                      SNAPSHOT_ID
                                                    {a5b8999f-5d95-4aff-82de-e515b0101b66}
            {a5b8999f-5d95-4aff-82de-e515b0101b66} *{a7345be5-ab66-478c-946e-a6c2caf14909}
        """
        )
        guids = [
            "a5b8999f-5d95-4aff-82de-e515b0101b66",
            "a7345be5-ab66-478c-946e-a6c2caf14909",
        ]

        self.assertEqual(parallels._find_guids(guid_str), guids)

    def test_prlsrvctl(self):
        """
        Test parallels.prlsrvctl
        """
        runas = "macdev"

        # Test missing prlsrvctl binary
        with patch("salt.utils.path.which", MagicMock(return_value=False)):
            with self.assertRaises(CommandExecutionError):
                parallels.prlsrvctl("info", runas=runas)

        # Simulate the existence of prlsrvctl
        with patch(
            "salt.utils.path.which", MagicMock(return_value="/usr/bin/prlsrvctl")
        ):

            # Validate 'prlsrvctl info'
            info_cmd = ["prlsrvctl", "info"]
            info_fcn = MagicMock()
            with patch.dict(parallels.__salt__, {"cmd.run": info_fcn}):
                parallels.prlsrvctl("info", runas=runas)
                info_fcn.assert_called_once_with(info_cmd, runas=runas)

            # Validate 'prlsrvctl usb list'
            usb_cmd = ["prlsrvctl", "usb", "list"]
            usb_fcn = MagicMock()
            with patch.dict(parallels.__salt__, {"cmd.run": usb_fcn}):
                parallels.prlsrvctl("usb", "list", runas=runas)
                usb_fcn.assert_called_once_with(usb_cmd, runas=runas)

            # Validate 'prlsrvctl set "--mem-limit auto"'
            set_cmd = ["prlsrvctl", "set", "--mem-limit", "auto"]
            set_fcn = MagicMock()
            with patch.dict(parallels.__salt__, {"cmd.run": set_fcn}):
                parallels.prlsrvctl("set", "--mem-limit auto", runas=runas)
                set_fcn.assert_called_once_with(set_cmd, runas=runas)

    def test_prlctl(self):
        """
        Test parallels.prlctl
        """
        runas = "macdev"

        # Test missing prlctl binary
        with patch("salt.utils.path.which", MagicMock(return_value=False)):
            with self.assertRaises(CommandExecutionError):
                parallels.prlctl("info", runas=runas)

        # Simulate the existence of prlctl
        with patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/prlctl")):

            # Validate 'prlctl user list'
            user_cmd = ["prlctl", "user", "list"]
            user_fcn = MagicMock()
            with patch.dict(parallels.__salt__, {"cmd.run": user_fcn}):
                parallels.prlctl("user", "list", runas=runas)
                user_fcn.assert_called_once_with(user_cmd, runas=runas)

            # Validate 'prlctl exec "macvm uname"'
            exec_cmd = ["prlctl", "exec", "macvm", "uname"]
            exec_fcn = MagicMock()
            with patch.dict(parallels.__salt__, {"cmd.run": exec_fcn}):
                parallels.prlctl("exec", "macvm uname", runas=runas)
                exec_fcn.assert_called_once_with(exec_cmd, runas=runas)

            # Validate 'prlctl capture "macvm --file macvm.display.png"'
            cap_cmd = ["prlctl", "capture", "macvm", "--file", "macvm.display.png"]
            cap_fcn = MagicMock()
            with patch.dict(parallels.__salt__, {"cmd.run": cap_fcn}):
                parallels.prlctl(
                    "capture", "macvm --file macvm.display.png", runas=runas
                )
                cap_fcn.assert_called_once_with(cap_cmd, runas=runas)

    def test_list_vms(self):
        """
        Test parallels.list_vms
        """
        runas = "macdev"

        # Validate a simple list
        mock_plain = MagicMock()
        with patch.object(parallels, "prlctl", mock_plain):
            parallels.list_vms(runas=runas)
            mock_plain.assert_called_once_with("list", [], runas=runas)

        # Validate listing a single VM
        mock_name = MagicMock()
        with patch.object(parallels, "prlctl", mock_name):
            parallels.list_vms(name="macvm", runas=runas)
            mock_name.assert_called_once_with("list", ["macvm"], runas=runas)

        # Validate listing templates
        mock_templ = MagicMock()
        with patch.object(parallels, "prlctl", mock_templ):
            parallels.list_vms(template=True, runas=runas)
            mock_templ.assert_called_once_with("list", ["--template"], runas=runas)

        # Validate listing extra info
        mock_info = MagicMock()
        with patch.object(parallels, "prlctl", mock_info):
            parallels.list_vms(info=True, runas=runas)
            mock_info.assert_called_once_with("list", ["--info"], runas=runas)

        # Validate listing with extra options
        mock_complex = MagicMock()
        with patch.object(parallels, "prlctl", mock_complex):
            parallels.list_vms(args=" -o uuid,status", all=True, runas=runas)
            mock_complex.assert_called_once_with(
                "list", ["-o", "uuid,status", "--all"], runas=runas
            )

    def test_clone(self):
        """
        Test parallels.clone
        """
        name = "macvm"
        runas = "macdev"

        # Validate clone
        mock_clone = MagicMock()
        with patch.object(parallels, "prlctl", mock_clone):
            parallels.clone(name, "macvm_new", runas=runas)
            mock_clone.assert_called_once_with(
                "clone", [name, "--name", "macvm_new"], runas=runas
            )

        # Validate linked clone
        mock_linked = MagicMock()
        with patch.object(parallels, "prlctl", mock_linked):
            parallels.clone(name, "macvm_link", linked=True, runas=runas)
            mock_linked.assert_called_once_with(
                "clone", [name, "--name", "macvm_link", "--linked"], runas=runas
            )

        # Validate template clone
        mock_template = MagicMock()
        with patch.object(parallels, "prlctl", mock_template):
            parallels.clone(name, "macvm_templ", template=True, runas=runas)
            mock_template.assert_called_once_with(
                "clone", [name, "--name", "macvm_templ", "--template"], runas=runas
            )

    def test_delete(self):
        """
        Test parallels.delete
        """
        name = "macvm"
        runas = "macdev"

        # Validate delete
        mock_delete = MagicMock()
        with patch.object(parallels, "prlctl", mock_delete):
            parallels.delete(name, runas=runas)
            mock_delete.assert_called_once_with("delete", name, runas=runas)

    def test_exists(self):
        """
        Test parallels.exists
        """
        name = "macvm"
        runas = "macdev"

        # Validate exists
        mock_list = MagicMock(return_value="Name: {}\nState: running".format(name))
        with patch.object(parallels, "list_vms", mock_list):
            self.assertTrue(parallels.exists(name, runas=runas))

        # Validate not exists
        mock_list = MagicMock(return_value="Name: {}\nState: running".format(name))
        with patch.object(parallels, "list_vms", mock_list):
            self.assertFalse(parallels.exists("winvm", runas=runas))

    def test_start(self):
        """
        Test parallels.start
        """
        name = "macvm"
        runas = "macdev"

        mock_start = MagicMock()
        with patch.object(parallels, "prlctl", mock_start):
            parallels.start(name, runas=runas)
            mock_start.assert_called_once_with("start", name, runas=runas)

    def test_stop(self):
        """
        Test parallels.stop
        """
        name = "macvm"
        runas = "macdev"

        # Validate stop
        mock_stop = MagicMock()
        with patch.object(parallels, "prlctl", mock_stop):
            parallels.stop(name, runas=runas)
            mock_stop.assert_called_once_with("stop", [name], runas=runas)

        # Validate immediate stop
        mock_kill = MagicMock()
        with patch.object(parallels, "prlctl", mock_kill):
            parallels.stop(name, kill=True, runas=runas)
            mock_kill.assert_called_once_with("stop", [name, "--kill"], runas=runas)

    def test_restart(self):
        """
        Test parallels.restart
        """
        name = "macvm"
        runas = "macdev"

        mock_start = MagicMock()
        with patch.object(parallels, "prlctl", mock_start):
            parallels.restart(name, runas=runas)
            mock_start.assert_called_once_with("restart", name, runas=runas)

    def test_reset(self):
        """
        Test parallels.reset
        """
        name = "macvm"
        runas = "macdev"

        mock_start = MagicMock()
        with patch.object(parallels, "prlctl", mock_start):
            parallels.reset(name, runas=runas)
            mock_start.assert_called_once_with("reset", name, runas=runas)

    def test_status(self):
        """
        Test parallels.status
        """
        name = "macvm"
        runas = "macdev"

        mock_start = MagicMock()
        with patch.object(parallels, "prlctl", mock_start):
            parallels.status(name, runas=runas)
            mock_start.assert_called_once_with("status", name, runas=runas)

    def test_exec_(self):
        """
        Test parallels.exec_
        """
        name = "macvm"
        runas = "macdev"

        mock_start = MagicMock()
        with patch.object(parallels, "prlctl", mock_start):
            parallels.exec_(name, "find /etc/paths.d", runas=runas)
            mock_start.assert_called_once_with(
                "exec", [name, "find", "/etc/paths.d"], runas=runas
            )

    def test_snapshot_id_to_name(self):
        """
        Test parallels.snapshot_id_to_name
        """
        name = "macvm"
        snap_id = "a5b8999f-5d95-4aff-82de-e515b0101b66"

        # Invalid GUID raises error
        self.assertRaises(
            SaltInvocationError, parallels.snapshot_id_to_name, name, "{8-4-4-4-12}"
        )

        # Empty return from prlctl raises error (name/snap_id mismatch?)
        mock_no_data = MagicMock(return_value="")
        with patch.object(parallels, "prlctl", mock_no_data):
            self.assertRaises(
                SaltInvocationError, parallels.snapshot_id_to_name, name, snap_id
            )

        # Data returned from prlctl is invalid YAML
        mock_invalid_data = MagicMock(return_value="[string theory is falsifiable}")
        with patch.object(parallels, "prlctl", mock_invalid_data):
            snap_name = parallels.snapshot_id_to_name(name, snap_id)
            self.assertEqual(snap_name, "")

        # Data returned from prlctl does not render as a dictionary
        mock_unknown_data = MagicMock(return_value="['sfermions', 'bosinos']")
        with patch.object(parallels, "prlctl", mock_unknown_data):
            snap_name = parallels.snapshot_id_to_name(name, snap_id)
            self.assertEqual(snap_name, "")

        # Snapshot is unnamed
        mock_no_name = MagicMock(return_value="Name:")
        with patch.object(parallels, "prlctl", mock_no_name):
            snap_name = parallels.snapshot_id_to_name(name, snap_id)
            self.assertEqual(snap_name, "")

        # If strict, then raise an error when name is not found
        mock_no_name = MagicMock(return_value="Name:")
        with patch.object(parallels, "prlctl", mock_no_name):
            self.assertRaises(
                SaltInvocationError,
                parallels.snapshot_id_to_name,
                name,
                snap_id,
                strict=True,
            )

        # Return name when found
        mock_yes_name = MagicMock(return_value="Name: top")
        with patch.object(parallels, "prlctl", mock_yes_name):
            snap_name = parallels.snapshot_id_to_name(name, snap_id)
            self.assertEqual(snap_name, "top")

    def test_snapshot_name_to_id(self):
        """
        Test parallels.snapshot_name_to_id
        """
        name = "macvm"
        snap_ids = [
            "a5b8999f-5d95-4aff-82de-e515b0101b66",
            "a7345be5-ab66-478c-946e-a6c2caf14909",
        ]
        snap_id = snap_ids[0]
        guid_str = textwrap.dedent(
            """
            PARENT_SNAPSHOT_ID                      SNAPSHOT_ID
                                                    {a5b8999f-5d95-4aff-82de-e515b0101b66}
            {a5b8999f-5d95-4aff-82de-e515b0101b66} *{a7345be5-ab66-478c-946e-a6c2caf14909}
        """
        )
        mock_guids = MagicMock(return_value=guid_str)

        # Raise error when no IDs found for snapshot name
        with patch.object(parallels, "prlctl", mock_guids):
            mock_no_names = MagicMock(return_value=[])
            with patch.object(parallels, "snapshot_id_to_name", mock_no_names):
                self.assertRaises(
                    SaltInvocationError, parallels.snapshot_name_to_id, name, "graviton"
                )

        # Validate singly-valued name
        with patch.object(parallels, "prlctl", mock_guids):
            mock_one_name = MagicMock(side_effect=["", "ν_e"])
            with patch.object(parallels, "snapshot_id_to_name", mock_one_name):
                self.assertEqual(
                    parallels.snapshot_name_to_id(name, "ν_e"), snap_ids[1]
                )

        # Validate multiply-valued name
        with patch.object(parallels, "prlctl", mock_guids):
            mock_many_names = MagicMock(side_effect=["J/Ψ", "J/Ψ"])
            with patch.object(parallels, "snapshot_id_to_name", mock_many_names):
                self.assertEqual(
                    sorted(parallels.snapshot_name_to_id(name, "J/Ψ")),
                    sorted(snap_ids),
                )

        # Raise error for multiply-valued name
        with patch.object(parallels, "prlctl", mock_guids):
            mock_many_names = MagicMock(side_effect=["J/Ψ", "J/Ψ"])
            with patch.object(parallels, "snapshot_id_to_name", mock_many_names):
                self.assertRaises(
                    SaltInvocationError,
                    parallels.snapshot_name_to_id,
                    name,
                    "J/Ψ",
                    strict=True,
                )

    def test__validate_snap_name(self):
        """
        Test parallels._validate_snap_name
        """
        name = "macvm"
        snap_id = "a5b8999f-5d95-4aff-82de-e515b0101b66"

        # Validate a GUID passthrough
        self.assertEqual(parallels._validate_snap_name(name, snap_id), snap_id)

        # Validate an unicode name
        mock_snap_symb = MagicMock(return_value=snap_id)
        with patch.object(parallels, "snapshot_name_to_id", mock_snap_symb):
            self.assertEqual(parallels._validate_snap_name(name, "π"), snap_id)
            mock_snap_symb.assert_called_once_with(name, "π", strict=True, runas=None)

        # Validate an ascii name
        mock_snap_name = MagicMock(return_value=snap_id)
        with patch.object(parallels, "snapshot_name_to_id", mock_snap_name):
            self.assertEqual(parallels._validate_snap_name(name, "pion"), snap_id)
            mock_snap_name.assert_called_once_with(
                name, "pion", strict=True, runas=None
            )

        # Validate a numerical name
        mock_snap_numb = MagicMock(return_value=snap_id)
        with patch.object(parallels, "snapshot_name_to_id", mock_snap_numb):
            self.assertEqual(parallels._validate_snap_name(name, "3.14159"), snap_id)
            mock_snap_numb.assert_called_once_with(
                name, "3.14159", strict=True, runas=None
            )

        # Validate not strict (think neutrino oscillation)
        mock_snap_non_strict = MagicMock(return_value=snap_id)
        with patch.object(parallels, "snapshot_name_to_id", mock_snap_non_strict):
            self.assertEqual(
                parallels._validate_snap_name(name, "e_ν", strict=False), snap_id
            )
            mock_snap_non_strict.assert_called_once_with(
                name, "e_ν", strict=False, runas=None
            )

    def test_list_snapshots(self):
        """
        Test parallels.list_snapshots
        """
        name = "macvm"
        guid_str = textwrap.dedent(
            """
            PARENT_SNAPSHOT_ID                      SNAPSHOT_ID
                                                    {a5b8999f-5d95-4aff-82de-e515b0101b66}
            {a5b8999f-5d95-4aff-82de-e515b0101b66} *{a7345be5-ab66-478c-946e-a6c2caf14909}
            {a5b8999f-5d95-4aff-82de-e515b0101b66}  {5da9faef-cb0e-466d-9b41-e5571b62ac2a}
        """
        )

        # Validate listing all snapshots for the VM
        mock_prlctl = MagicMock(return_value=guid_str)
        with patch.object(parallels, "prlctl", mock_prlctl):
            parallels.list_snapshots(name)
            mock_prlctl.assert_called_once_with("snapshot-list", [name], runas=None)

        # Validate listing all snapshots in tree mode
        mock_prlctl = MagicMock(return_value=guid_str)
        with patch.object(parallels, "prlctl", mock_prlctl):
            parallels.list_snapshots(name, tree=True)
            mock_prlctl.assert_called_once_with(
                "snapshot-list", [name, "--tree"], runas=None
            )

        # Validate listing a single snapshot
        snap_name = "muon"
        mock_snap_name = MagicMock(return_value=snap_name)
        with patch.object(parallels, "_validate_snap_name", mock_snap_name):
            mock_prlctl = MagicMock(return_value=guid_str)
            with patch.object(parallels, "prlctl", mock_prlctl):
                parallels.list_snapshots(name, snap_name)
                mock_prlctl.assert_called_once_with(
                    "snapshot-list", [name, "--id", snap_name], runas=None
                )

        # Validate listing snapshot ID and name pairs
        snap_names = ["electron", "muon", "tauon"]
        mock_snap_name = MagicMock(side_effect=snap_names)
        with patch.object(parallels, "snapshot_id_to_name", mock_snap_name):
            mock_prlctl = MagicMock(return_value=guid_str)
            with patch.object(parallels, "prlctl", mock_prlctl):
                ret = parallels.list_snapshots(name, names=True)
                for snap_name in snap_names:
                    self.assertIn(snap_name, ret)
                mock_prlctl.assert_called_once_with("snapshot-list", [name], runas=None)

    def test_snapshot(self):
        """
        Test parallels.snapshot
        """
        name = "macvm"

        # Validate creating a snapshot
        mock_snap = MagicMock(return_value="")
        with patch.object(parallels, "prlctl", mock_snap):
            parallels.snapshot(name)
            mock_snap.assert_called_once_with("snapshot", [name], runas=None)

        # Validate creating a snapshot with a name
        snap_name = "h_0"
        mock_snap_name = MagicMock(return_value="")
        with patch.object(parallels, "prlctl", mock_snap_name):
            parallels.snapshot(name, snap_name)
            mock_snap_name.assert_called_once_with(
                "snapshot", [name, "--name", snap_name], runas=None
            )

        # Validate creating a snapshot with a name and a description
        snap_name = "h_0"
        snap_desc = textwrap.dedent(
            "The ground state particle of the higgs multiplet family of bosons"
        )
        mock_snap_name = MagicMock(return_value="")
        with patch.object(parallels, "prlctl", mock_snap_name):
            parallels.snapshot(name, snap_name, snap_desc)
            mock_snap_name.assert_called_once_with(
                "snapshot",
                [name, "--name", snap_name, "--description", snap_desc],
                runas=None,
            )

    def test_delete_snapshot(self):
        """
        Test parallels.delete_snapshot
        """
        delete_message = (
            "Delete the snapshot...\nThe snapshot has been successfully deleted."
        )

        # Validate single ID
        name = "macvm"
        snap_name = "kaon"
        snap_id = "c2eab062-a635-4ccd-b9ae-998370f898b5"

        mock_snap_name = MagicMock(return_value=snap_id)
        with patch.object(parallels, "_validate_snap_name", mock_snap_name):
            mock_delete = MagicMock(return_value=delete_message)
            with patch.object(parallels, "prlctl", mock_delete):
                ret = parallels.delete_snapshot(name, snap_name)
                self.assertEqual(ret, delete_message)
                mock_delete.assert_called_once_with(
                    "snapshot-delete", [name, "--id", snap_id], runas=None
                )

        # Validate multiple IDs
        name = "macvm"
        snap_name = "higgs doublet"
        snap_ids = [
            "c2eab062-a635-4ccd-b9ae-998370f898b5",
            "8aca07c5-a0e1-4dcb-ba75-cb154d46d516",
        ]

        mock_snap_ids = MagicMock(return_value=snap_ids)
        with patch.object(parallels, "_validate_snap_name", mock_snap_ids):
            mock_delete = MagicMock(return_value=delete_message)
            with patch.object(parallels, "prlctl", mock_delete):
                ret = parallels.delete_snapshot(name, snap_name, all=True)
                mock_ret = {snap_ids[0]: delete_message, snap_ids[1]: delete_message}
                self.assertDictEqual(ret, mock_ret)
                mock_delete.assert_any_call(
                    "snapshot-delete", [name, "--id", snap_ids[0]], runas=None
                )
                mock_delete.assert_any_call(
                    "snapshot-delete", [name, "--id", snap_ids[1]], runas=None
                )

    def test_revert_snapshot(self):
        """
        Test parallels.revert_snapshot
        """
        name = "macvm"
        snap_name = "k-bar"
        snap_id = "c2eab062-a635-4ccd-b9ae-998370f898b5"

        mock_snap_name = MagicMock(return_value=snap_id)
        with patch.object(parallels, "_validate_snap_name", mock_snap_name):
            mock_delete = MagicMock(return_value="")
            with patch.object(parallels, "prlctl", mock_delete):
                parallels.revert_snapshot(name, snap_name)
                mock_delete.assert_called_once_with(
                    "snapshot-switch", [name, "--id", snap_id], runas=None
                )
