import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))

try:
    from unittest import mock, TestCase
except ImportError:
    import mock
import logging

from crmsh.crash_test import utils, main, config


class TestMyLoggingFormatter(TestCase):

    @classmethod
    def setUpClass(cls):
        """
        Global setUp.
        """

    def setUp(self):
        """
        Test setUp.
        """
        self.fence_info_inst = utils.FenceInfo()

    def tearDown(self):
        """
        Test tearDown.
        """

    @classmethod
    def tearDownClass(cls):
        """
        Global tearDown.
        """


class TestFenceInfo(TestCase):

    @classmethod
    def setUpClass(cls):
        """
        Global setUp.
        """

    def setUp(self):
        """
        Test setUp.
        """
        self.fence_info_inst = utils.FenceInfo()

    def tearDown(self):
        """
        Test tearDown.
        """

    @classmethod
    def tearDownClass(cls):
        """
        Global tearDown.
        """

    @mock.patch('crmsh.crash_test.utils.crmshutils.get_property')
    def test_fence_enabled_false(self, mock_get_property):
        mock_get_property.return_value = None
        res = self.fence_info_inst.fence_enabled
        self.assertEqual(res, False)
        mock_get_property.assert_called_once_with("stonith-enabled")

    @mock.patch('crmsh.crash_test.utils.crmshutils.get_property')
    def test_fence_enabled_true(self, mock_get_property):
        mock_get_property.return_value = "True"
        res = self.fence_info_inst.fence_enabled
        self.assertEqual(res, True)
        mock_get_property.assert_called_once_with("stonith-enabled")

    @mock.patch('crmsh.crash_test.utils.msg_error')
    @mock.patch('crmsh.ra.get_property_options')
    @mock.patch('crmsh.crash_test.utils.crmshutils.get_property')
    def test_fence_action_none(self, mock_get_property, mock_get_property_options, mock_error):
        mock_get_property_options.return_value = ["off", "reboot"]
        mock_get_property.return_value = None
        res = self.fence_info_inst.fence_action
        self.assertEqual(res, None)
        mock_get_property.assert_called_once_with("stonith-action")
        mock_error.assert_called_once_with('Cluster property "stonith-action" should be off|reboot')

    @mock.patch('crmsh.ra.get_property_options')
    @mock.patch('crmsh.crash_test.utils.crmshutils.get_property')
    def test_fence_action(self, mock_get_property, mock_get_property_options):
        mock_get_property_options.return_value = ["off", "reboot"]
        mock_get_property.return_value = "reboot"
        res = self.fence_info_inst.fence_action
        self.assertEqual(res, "reboot")
        mock_get_property.assert_called_once_with("stonith-action")

    @mock.patch('crmsh.crash_test.utils.crmshutils.get_property')
    def test_fence_timeout(self, mock_get_property):
        mock_get_property.return_value = "60s"
        res = self.fence_info_inst.fence_timeout
        self.assertEqual(res, "60")
        mock_get_property.assert_called_once_with("stonith-timeout")

    @mock.patch('crmsh.crash_test.utils.crmshutils.get_property')
    def test_fence_timeout_default(self, mock_get_property):
        mock_get_property.return_value = None
        res = self.fence_info_inst.fence_timeout
        self.assertEqual(res, config.FENCE_TIMEOUT)
        mock_get_property.assert_called_once_with("stonith-timeout")


class TestUtils(TestCase):
    '''
    Unitary tests for crash_test/utils.py
    '''

    @classmethod
    def setUpClass(cls):
        """
        Global setUp.
        """

    def setUp(self):
        """
        Test setUp.
        """

    def tearDown(self):
        """
        Test tearDown.
        """

    @classmethod
    def tearDownClass(cls):
        """
        Global tearDown.
        """

    @mock.patch('crmsh.crash_test.utils.datetime')
    def test_now(self, mock_datetime):
        mock_now = mock.Mock()
        mock_datetime.now.return_value = mock_now
        mock_now.strftime.return_value = "2019/07/05 14:44:55"

        result = utils.now()

        self.assertEqual(result, "2019/07/05 14:44:55")
        mock_datetime.now.assert_called_once_with()
        mock_now.strftime.assert_called_once_with("%Y/%m/%d %H:%M:%S")

    @mock.patch('crmsh.crash_test.utils.get_handler')
    def test_manage_handler(self, mock_get_handler):
        mock_get_handler.return_value = "handler"
        utils.logger = mock.Mock()
        utils.logger.removeHandler = mock.Mock()
        utils.logger.addHandler = mock.Mock()

        with utils.manage_handler("type1", keep=False):
            pass

        mock_get_handler.assert_called_once_with(utils.logger, "type1")
        utils.logger.removeHandler.assert_called_once_with("handler")
        utils.logger.addHandler.assert_called_once_with("handler")

    @mock.patch('crmsh.crash_test.utils.manage_handler')
    def test_msg_raw(self, mock_handler):
        utils.logger = mock.Mock()
        utils.logger.log = mock.Mock()
        utils.msg_raw("level1", "msg1")
        mock_handler.assert_called_once_with("console", True)
        utils.logger.log.assert_called_once_with("level1", "msg1")

    @mock.patch('crmsh.crash_test.utils.msg_raw')
    def test_msg_info(self, mock_raw):
        utils.msg_info("msg1")
        mock_raw.assert_called_once_with(logging.INFO, "msg1", True)

    @mock.patch('crmsh.crash_test.utils.msg_raw')
    def test_msg_warn(self, mock_raw):
        utils.msg_warn("msg1")
        mock_raw.assert_called_once_with(logging.WARNING, "msg1", True)

    @mock.patch('crmsh.crash_test.utils.msg_raw')
    def test_msg_error(self, mock_raw):
        utils.msg_error("msg1")
        mock_raw.assert_called_once_with(logging.ERROR, "msg1", True)

    @mock.patch('os.fsync')
    @mock.patch('json.dumps')
    @mock.patch('builtins.open', create=True)
    def test_json_dumps(self, mock_open_file, mock_dumps, mock_fsync):
        main.ctx = mock.Mock(jsonfile="file1", task_list={"process_name": "xin", "age": 38})
        mock_open_write = mock.mock_open()
        file_handle = mock_open_write.return_value.__enter__.return_value
        mock_open_file.return_value = mock_open_write.return_value
        mock_dumps.return_value = "data"

        utils.json_dumps()

        mock_open_file.assert_called_once_with("file1", "w")
        mock_dumps.assert_called_once_with(main.ctx.task_list, indent=2)
        file_handle.write.assert_called_once_with("data")
        file_handle.flush.assert_called_once_with()
        mock_fsync.assert_called_once_with(file_handle)

    @mock.patch('crmsh.crash_test.utils.crmshutils.this_node')
    @mock.patch('crmsh.crash_test.utils.msg_error')
    @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')
    def test_this_node_false(self, mock_run, mock_error, mock_this_node):
        mock_run.return_value = (1, None, "error data")
        mock_this_node.return_value = "node1"

        res = utils.this_node()
        self.assertEqual(res, "node1")

        mock_run.assert_called_once_with("crm_node --name")
        mock_error.assert_called_once_with("error data")
        mock_this_node.assert_called_once_with()
    
    @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')
    def test_this_node(self, mock_run):
        mock_run.return_value = (0, "data", None)
        res = utils.this_node()
        self.assertEqual(res, "data")
        mock_run.assert_called_once_with("crm_node --name")

    @mock.patch('crmsh.crash_test.utils.datetime')
    def test_str_to_datetime(self, mock_datetime):
        utils.str_to_datetime("Mon Nov  2 15:37:11 2020", "%a %b %d %H:%M:%S %Y")
        mock_datetime.strptime.assert_called_once_with("Mon Nov  2 15:37:11 2020", "%a %b %d %H:%M:%S %Y")

    @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')
    def test_corosync_port_list(self, mock_run):
        output = """
totem.interface.0.bindnetaddr (str) = 10.10.10.121
totem.interface.0.mcastaddr (str) = 239.101.40.63
totem.interface.0.mcastport (u16) = 5405
totem.interface.0.ttl (u8) = 1
totem.interface.1.bindnetaddr (str) = 20.20.20.121
totem.interface.1.mcastaddr (str) = 239.6.213.31
totem.interface.1.mcastport (u16) = 5407
totem.interface.1.ttl (u8) = 1
        """
        mock_run.return_value = (0, output, None)
        result = utils.corosync_port_list()
        expected = ['5405', '5407']
        self.assertListEqual(result, expected)
        mock_run.assert_called_once_with("corosync-cmapctl totem.interface")

    def test_get_handler(self):
        mock_handler1 = mock.Mock(_name="test1_handler")
        mock_handler2 = mock.Mock(_name="test2_handler")
        mock_logger = mock.Mock(handlers=[mock_handler1, mock_handler2])
        res = utils.get_handler(mock_logger, "test1_handler")
        self.assertEqual(res, mock_handler1)

    @mock.patch('os.getuid')
    def test_is_root(self, mock_getuid):
        mock_getuid.return_value = 0
        self.assertEqual(utils.is_root(), True)
        mock_getuid.assert_called_once_with()

    @mock.patch('crmsh.crash_test.utils.crmshutils.to_ascii')
    @mock.patch('os.path.basename')
    @mock.patch('builtins.open')
    @mock.patch('os.path.join')
    @mock.patch('os.listdir')
    def test_get_process_status_false(self, mock_listdir, mock_join, mock_open_file, mock_basename, mock_to_ascii):
        mock_listdir.return_value = ['1', '2', 'none']
        mock_join.side_effect = ['/proc/1/cmdline', '/proc/2/cmdline']
        mock_open_read_1 = mock.mock_open(read_data=b'/usr/sbin/cmd1\x00--user\x00')
        mock_open_read_2 = mock.mock_open(read_data=b'/usr/sbin/cmd2\x00')
        mock_open_file.side_effect = [
            mock_open_read_1.return_value,
            mock_open_read_2.return_value
        ]
        mock_to_ascii.side_effect = [
            "/usr/sbin/cmd1\x00--user\x00",
            "/usr/sbin/cmd2\x00"
        ]
        mock_basename.side_effect = ["cmd1", "cmd2"]

        rc, pid = utils.get_process_status("sbd")
        self.assertEqual(rc, False)
        self.assertEqual(pid, -1)

        mock_listdir.assert_called_once_with('/proc')
        mock_join.assert_has_calls([
            mock.call('/proc', '1', 'cmdline'),
            mock.call('/proc', '2', 'cmdline')
            ])
        mock_open_file.assert_has_calls([
            mock.call('/proc/1/cmdline', 'rb'),
            mock.call('/proc/2/cmdline', 'rb')
            ])
        mock_to_ascii.assert_has_calls([
            mock.call(b'/usr/sbin/cmd1\x00--user\x00'),
            mock.call(b'/usr/sbin/cmd2\x00')
            ])

    @mock.patch('crmsh.crash_test.utils.crmshutils.to_ascii')
    @mock.patch('os.path.basename')
    @mock.patch('builtins.open')
    @mock.patch('os.path.join')
    @mock.patch('os.listdir')
    def test_get_process_status(self, mock_listdir, mock_join, mock_open_file, mock_basename, mock_to_ascii):
        mock_listdir.return_value = ['1', '2', 'none']
        mock_join.side_effect = ['/proc/1/cmdline', '/proc/2/cmdline']
        mock_open_read_1 = mock.mock_open(read_data=b'/usr/sbin/cmd1\x00--user\x00')
        mock_open_read_2 = mock.mock_open(read_data=b'/usr/sbin/sbd\x00')
        mock_open_file.side_effect = [
            mock_open_read_1.return_value,
            mock_open_read_2.return_value
        ]
        mock_to_ascii.side_effect = [
            "/usr/sbin/cmd1\x00--user\x00",
            "/usr/sbin/sbd\x00"
        ]
        mock_basename.side_effect = ["cmd1", "sbd"]

        rc, pid = utils.get_process_status("sbd")
        self.assertEqual(rc, True)
        self.assertEqual(pid, 2)

        mock_listdir.assert_called_once_with('/proc')
        mock_join.assert_has_calls([
            mock.call('/proc', '1', 'cmdline'),
            mock.call('/proc', '2', 'cmdline')
            ])
        mock_open_file.assert_has_calls([
            mock.call('/proc/1/cmdline', 'rb'),
            mock.call('/proc/2/cmdline', 'rb')
            ])
        mock_to_ascii.assert_has_calls([
            mock.call(b'/usr/sbin/cmd1\x00--user\x00'),
            mock.call(b'/usr/sbin/sbd\x00')
            ])

    @mock.patch('crmsh.crash_test.utils.msg_error')
    @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')
    def test_check_node_status_error_cmd(self, mock_run, mock_error):
        mock_run.return_value = (1, None, "error")
        res = utils.check_node_status("node1", "member")
        self.assertEqual(res, False)
        mock_run.assert_called_once_with("crm_node -l")
        mock_error.assert_called_once_with("error")

    @mock.patch('crmsh.crash_test.utils.msg_error')
    @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')
    def test_check_node_status(self, mock_run, mock_error):
        output = """
1084783297 15sp2-1 member
1084783193 15sp2-2 lost
        """
        mock_run.return_value = (0, output, None)

        res = utils.check_node_status("15sp2-2", "member")
        self.assertEqual(res, False)
        res = utils.check_node_status("15sp2-1", "member")
        self.assertEqual(res, True)

        mock_run.assert_has_calls([
            mock.call("crm_node -l"),
            mock.call("crm_node -l")
            ])
        mock_error.assert_not_called()

    @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')
    def test_online_nodes_empty(self, mock_run):
        mock_run.return_value = (0, "data", None)
        res = utils.online_nodes()
        self.assertEqual(res, [])
        mock_run.assert_called_once_with("crm_mon -1")

    @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')
    def test_online_nodes(self, mock_run):
        output = """
Node List:
  * Online: [ 15sp2-1 15sp2-2 ]
        """
        mock_run.return_value = (0, output, None)
        res = utils.online_nodes()
        self.assertEqual(res, ["15sp2-1", "15sp2-2"])
        mock_run.assert_called_once_with("crm_mon -1")

    @mock.patch('crmsh.crash_test.utils.online_nodes')
    def test_peer_node_list_empty(self, mock_online):
        mock_online.return_value = None
        res = utils.peer_node_list()
        self.assertEqual(res, [])
        mock_online.assert_called_once_with()

    @mock.patch('crmsh.crash_test.utils.this_node')
    @mock.patch('crmsh.crash_test.utils.online_nodes')
    def test_peer_node_list(self, mock_online, mock_this_node):
        mock_online.return_value = ["node1", "node2"]
        mock_this_node.return_value = "node1"
        res = utils.peer_node_list()
        self.assertEqual(res, ["node2"])
        mock_online.assert_called_once_with()

    # Test is_valid_sbd():
    @classmethod
    @mock.patch('os.path.exists')
    def test_is_valid_sbd_not_exist(cls, mock_os_path_exists):
        """
        Test device not exist
        """
        dev = "/dev/disk/by-id/scsi-device1"
        mock_os_path_exists.return_value = False

        res = utils.is_valid_sbd(dev)
        assert res is False

    @classmethod
    @mock.patch('crmsh.crash_test.utils.msg_error')
    @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')
    @mock.patch('os.path.exists')
    def test_is_valid_sbd_cmd_error(cls, mock_os_path_exists,
                                    mock_sbd_check_header, mock_msg_err):
        """
        Test device is not valid sbd
        """
        dev = "/dev/disk/by-id/scsi-device1"
        mock_os_path_exists.return_value = True
        mock_sbd_check_header.return_value = (-1, None, "Unknown error!")
        mock_msg_err.return_value = ""

        res = utils.is_valid_sbd(dev)
        mock_msg_err.assert_called_once_with("Unknown error!")
        assert res is False

    @classmethod
    @mock.patch('crmsh.crash_test.utils.msg_error')
    @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')
    @mock.patch('os.path.exists')
    def test_is_valid_sbd_not_sbd(cls, mock_os_path_exists,
                                  mock_sbd_check_header, mock_msg_err):
        """
        Test device is not SBD device
        """
        dev = "/dev/disk/by-id/scsi-device1"
        err_output = """
==Dumping header on disk {}
==Header on disk {} NOT dumped
sbd failed; please check the logs.
""".format(dev, dev)
        mock_os_path_exists.return_value = True
        mock_sbd_check_header.return_value = (1, "==Dumping header on disk {}".format(dev),
                                              err_output)

        res = utils.is_valid_sbd(dev)
        assert res is False
        mock_msg_err.assert_called_once_with(err_output)

    @classmethod
    @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')
    @mock.patch('os.path.exists')
    def test_is_valid_sbd_is_sbd(cls, mock_os_path_exists,
                                 mock_sbd_check_header):
        """
        Test device is not SBD device
        """
        dev = "/dev/disk/by-id/scsi-device1"
        std_output = """
==Dumping header on disk {}
Header version     : 2.1
UUID               : f4c99362-6522-46fc-8ce4-7db60aff19bb
Number of slots    : 255
Sector size        : 512
Timeout (watchdog) : 5
Timeout (allocate) : 2
Timeout (loop)     : 1
Timeout (msgwait)  : 10
==Header on disk {} is dumped
""".format(dev, dev)
        mock_os_path_exists.return_value = True
        mock_sbd_check_header.return_value = (0, std_output, None)

        res = utils.is_valid_sbd(dev)
        assert res is True

    # Test find_candidate_sbd() and _find_match_count()
    @classmethod
    @mock.patch('glob.glob')
    @mock.patch('os.path.basename')
    @mock.patch('os.path.dirname')
    def test_find_candidate_no_dev(cls, mock_os_path_dname, mock_os_path_bname,
                                   mock_glob):
        """
        Test no suitable device
        """
        mock_os_path_dname.return_value = "/dev/disk/by-id"
        mock_os_path_bname.return_value = "scsi-label_CN_devA"
        mock_glob.return_value = []

        res = utils.find_candidate_sbd("/not-exist-folder/not-exist-dev")
        assert res == ""

    @classmethod
    @mock.patch('crmsh.crash_test.utils.is_valid_sbd')
    @mock.patch('glob.glob')
    @mock.patch('os.path.basename')
    @mock.patch('os.path.dirname')
    def test_find_candidate_no_can(cls, mock_os_path_dname, mock_os_path_bname,
                                   mock_glob, mock_is_valid_sbd):
        """
        Test no valid candidate device
        """
        mock_os_path_dname.return_value = "/dev/disk/by-id"
        mock_os_path_bname.return_value = "scsi-label_CN_devA"
        mock_glob.return_value = ["/dev/disk/by-id/scsi-label_DE_devA",
                                  "/dev/disk/by-id/scsi-label_DE_devB",
                                  "/dev/disk/by-id/scsi-label_DE_devC",
                                  "/dev/disk/by-id/scsi-label_DE_devD"]
        mock_is_valid_sbd.side_effect = [False, False, False, False]

        res = utils.find_candidate_sbd("/dev/disk/by-id/scsi-label_CN_devA")
        assert res == ""

    @classmethod
    @mock.patch('crmsh.crash_test.utils.is_valid_sbd')
    @mock.patch('glob.glob')
    @mock.patch('os.path.basename')
    @mock.patch('os.path.dirname')
    def test_find_candidate_has_multi(cls, mock_os_path_dname, mock_os_path_bname,
                                      mock_glob, mock_is_valid_sbd):
        """
        Test has multiple valid candidate devices
        """
        mock_os_path_dname.return_value = "/dev/disk/by-id"
        mock_os_path_bname.return_value = "scsi-label_CN_devA"
        mock_glob.return_value = ["/dev/disk/by-id/scsi-label_DE_devA",
                                  "/dev/disk/by-id/scsi-label_DE_devB",
                                  "/dev/disk/by-id/scsi-label_CN_devC",
                                  "/dev/disk/by-id/scsi-label_CN_devD",
                                  "/dev/disk/by-id/scsi-mp_China_devE",
                                  "/dev/disk/by-id/scsi-mp_China_devF"]
        mock_is_valid_sbd.side_effect = [True, False, False, True, True, False]

        res = utils.find_candidate_sbd("/dev/disk/by-id/scsi-label_CN_devA")
        assert res == "/dev/disk/by-id/scsi-label_CN_devD"
