# -*- coding: utf-8 -*-

# Import Python libs
from __future__ import absolute_import, unicode_literals, print_function
import datetime
import logging
import os
import signal
import subprocess
import textwrap

# Import Salt Testing libs
from tests.support.case import ModuleCase
from tests.support.unit import skipIf
from tests.support.helpers import destructiveTest, skip_if_not_root, flaky

# Import Salt libs
import salt.utils.files
import salt.utils.path
import salt.utils.platform
import salt.states.file
from salt.ext.six.moves import range
from salt.ext import six

log = logging.getLogger(__name__)


@skipIf(not salt.utils.platform.is_linux(), 'These tests can only be run on linux')
class SystemModuleTest(ModuleCase):
    '''
    Validate the date/time functions in the system module
    '''
    fmt_str = "%Y-%m-%d %H:%M:%S"

    def __init__(self, arg):
        super(self.__class__, self).__init__(arg)
        self._orig_time = None
        self._machine_info = True

    def setUp(self):
        super(SystemModuleTest, self).setUp()
        os_grain = self.run_function('grains.item', ['kernel'])
        if os_grain['kernel'] not in 'Linux':
            self.skipTest(
                'Test not applicable to \'{kernel}\' kernel'.format(
                    **os_grain
                )
            )
        if self.run_function('service.available', ['systemd-timesyncd']):
            self.run_function('service.stop', ['systemd-timesyncd'])

    def tearDown(self):
        if self._orig_time is not None:
            self._restore_time()
        self._orig_time = None
        if self._machine_info is not True:
            self._restore_machine_info()
        self._machine_info = True
        if self.run_function('service.available', ['systemd-timesyncd']):
            self.run_function('service.start', ['systemd-timesyncd'])

    def _save_time(self):
        self._orig_time = datetime.datetime.utcnow()

    def _set_time(self, new_time, offset=None):
        t = new_time.timetuple()[:6]
        t += (offset,)
        return self.run_function('system.set_system_date_time', t)

    def _restore_time(self):
        result = self._set_time(self._orig_time, "+0000")
        self.assertTrue(result, msg="Unable to restore time properly")

    def _same_times(self, t1, t2, seconds_diff=30):
        '''
        Helper function to check if two datetime objects
        are close enough to the same time.
        '''
        return abs(t1 - t2) < datetime.timedelta(seconds=seconds_diff)

    def _hwclock_has_compare(self):
        '''
        Some builds of hwclock don't include the `--compare` function
        needed to test hw/sw clock synchronization. Returns false on
        systems where it's not present so that we can skip the
        comparison portion of the test.
        '''
        res = self.run_function('cmd.run_all', cmd='hwclock -h')
        return res['retcode'] == 0 and res['stdout'].find('--compare') > 0

    def _test_hwclock_sync(self):
        '''
        Check that hw and sw clocks are sync'd.
        '''
        if not self.run_function('system.has_settable_hwclock'):
            return None
        if not self._hwclock_has_compare():
            return None

        class CompareTimeout(BaseException):
            pass

        def _alrm_handler(sig, frame):
            log.warning('hwclock --compare failed to produce output after 3 seconds')
            raise CompareTimeout

        for _ in range(2):
            try:
                orig_handler = signal.signal(signal.SIGALRM, _alrm_handler)
                signal.alarm(3)
                rpipeFd, wpipeFd = os.pipe()
                log.debug('Comparing hwclock to sys clock')
                with os.fdopen(rpipeFd, "r") as rpipe:
                    with os.fdopen(wpipeFd, "w") as wpipe:
                        with salt.utils.files.fopen(os.devnull, "r") as nulFd:
                            p = subprocess.Popen(args=['hwclock', '--compare'],
                                stdin=nulFd, stdout=wpipeFd, stderr=subprocess.PIPE)
                            p.communicate()

                            # read header
                            rpipe.readline()

                            # read first time comparison
                            timeCompStr = rpipe.readline()

                            # stop
                            p.terminate()

                            timeComp = timeCompStr.split()
                            hwTime = float(timeComp[0])
                            swTime = float(timeComp[1])
                            diff = abs(hwTime - swTime)

                            self.assertTrue(diff <= 2.0,
                                msg=("hwclock difference too big: " + six.text_type(timeCompStr)))
                            break
            except CompareTimeout:
                p.terminate()
            finally:
                signal.alarm(0)
                signal.signal(signal.SIGALRM, orig_handler)
        else:
            log.error('Failed to check hwclock sync')

    def _save_machine_info(self):
        if os.path.isfile('/etc/machine-info'):
            with salt.utils.files.fopen('/etc/machine-info', 'r') as mach_info:
                self._machine_info = mach_info.read()
        else:
            self._machine_info = False

    def _restore_machine_info(self):
        if self._machine_info is not False:
            with salt.utils.files.fopen('/etc/machine-info', 'w') as mach_info:
                mach_info.write(self._machine_info)
        else:
            self.run_function('file.remove', ['/etc/machine-info'])

    def test_get_system_date_time(self):
        '''
        Test we are able to get the correct time
        '''
        t1 = datetime.datetime.now()
        res = self.run_function('system.get_system_date_time')
        t2 = datetime.datetime.strptime(res, self.fmt_str)
        msg = ("Difference in times is too large. Now: {0} Fake: {1}"
               .format(t1, t2))
        self.assertTrue(self._same_times(t1, t2, seconds_diff=2), msg=msg)

    def test_get_system_date_time_utc(self):
        '''
        Test we are able to get the correct time with utc
        '''
        t1 = datetime.datetime.utcnow()
        res = self.run_function('system.get_system_date_time',
                                utc_offset="+0000")
        t2 = datetime.datetime.strptime(res, self.fmt_str)
        msg = ("Difference in times is too large. Now: {0} Fake: {1}"
               .format(t1, t2))
        self.assertTrue(self._same_times(t1, t2, seconds_diff=2), msg=msg)

    @destructiveTest
    @skip_if_not_root
    def test_set_system_date_time(self):
        '''
        Test changing the system clock. We are only able to set it up to a
        resolution of a second so this test may appear to run in negative time.
        '''
        self._save_time()
        cmp_time = datetime.datetime.now() - datetime.timedelta(days=7)
        result = self._set_time(cmp_time)
        time_now = datetime.datetime.now()

        msg = ("Difference in times is too large. Now: {0} Fake: {1}"
               .format(time_now, cmp_time))
        self.assertTrue(result and self._same_times(time_now, cmp_time),
                        msg=msg)
        self._test_hwclock_sync()

    @destructiveTest
    @skip_if_not_root
    def test_set_system_date_time_utc(self):
        '''
        Test changing the system clock. We are only able to set it up to a
        resolution of a second so this test may appear to run in negative time.
        '''
        self._save_time()
        cmp_time = datetime.datetime.utcnow() - datetime.timedelta(days=7)
        result = self._set_time(cmp_time, offset="+0000")
        time_now = datetime.datetime.utcnow()

        msg = ("Difference in times is too large. Now: {0} Fake: {1}"
               .format(time_now, cmp_time))
        self.assertTrue(result)
        self.assertTrue(self._same_times(time_now, cmp_time), msg=msg)
        self._test_hwclock_sync()

    @destructiveTest
    @skip_if_not_root
    def test_set_system_date_time_utcoffset_east(self):
        '''
        Test changing the system clock. We are only able to set it up to a
        resolution of a second so this test may appear to run in negative time.
        '''
        self._save_time()
        cmp_time = datetime.datetime.utcnow() - datetime.timedelta(days=7)
        # 25200 seconds = 7 hours
        time_to_set = cmp_time - datetime.timedelta(seconds=25200)
        result = self._set_time(time_to_set, offset='-0700')
        time_now = datetime.datetime.utcnow()

        msg = ("Difference in times is too large. Now: {0} Fake: {1}"
               .format(time_now, cmp_time))
        self.assertTrue(result)
        self.assertTrue(self._same_times(time_now, cmp_time), msg=msg)
        self._test_hwclock_sync()

    @destructiveTest
    @skip_if_not_root
    def test_set_system_date_time_utcoffset_west(self):
        '''
        Test changing the system clock. We are only able to set it up to a
        resolution of a second so this test may appear to run in negative time.
        '''
        self._save_time()
        cmp_time = datetime.datetime.utcnow() - datetime.timedelta(days=7)
        # 7200 seconds = 2 hours
        time_to_set = cmp_time + datetime.timedelta(seconds=7200)
        result = self._set_time(time_to_set, offset='+0200')
        time_now = datetime.datetime.utcnow()

        msg = ("Difference in times is too large. Now: {0} Fake: {1}"
               .format(time_now, cmp_time))
        self.assertTrue(result)
        self.assertTrue(self._same_times(time_now, cmp_time), msg=msg)
        self._test_hwclock_sync()

    @flaky
    @destructiveTest
    @skip_if_not_root
    def test_set_system_time(self):
        '''
        Test setting the system time without adjusting the date.
        '''
        cmp_time = datetime.datetime.now().replace(hour=10, minute=5, second=0)
        self._save_time()

        result = self.run_function('system.set_system_time', ["10:05:00"])

        time_now = datetime.datetime.now()
        msg = ("Difference in times is too large. Now: {0} Fake: {1}"
               .format(time_now, cmp_time))

        self.assertTrue(result)
        self.assertTrue(self._same_times(time_now, cmp_time), msg=msg)
        self._test_hwclock_sync()

    @destructiveTest
    @skip_if_not_root
    def test_set_system_date(self):
        '''
        Test setting the system date without adjusting the time.
        '''
        cmp_time = datetime.datetime.now() - datetime.timedelta(days=7)

        self._save_time()
        result = self.run_function(
            'system.set_system_date',
            [cmp_time.strftime('%Y-%m-%d')]
        )

        time_now = datetime.datetime.now()
        msg = ("Difference in times is too large. Now: {0} Fake: {1}"
               .format(time_now, cmp_time))

        self.assertTrue(result)
        self.assertTrue(self._same_times(time_now, cmp_time), msg=msg)
        self._test_hwclock_sync()

    @skip_if_not_root
    def test_get_computer_desc(self):
        '''
        Test getting the system hostname
        '''
        res = self.run_function('system.get_computer_desc')

        hostname_cmd = salt.utils.path.which('hostnamectl')
        if hostname_cmd:
            desc = self.run_function('cmd.run', ["hostnamectl status --pretty"])
            self.assertEqual(res, desc)
        else:
            if not os.path.isfile('/etc/machine-info'):
                self.assertFalse(res)
            else:
                with salt.utils.files.fopen('/etc/machine-info', 'r') as mach_info:
                    data = mach_info.read()
                    self.assertIn(res, data.decode('string_escape'))

    @destructiveTest
    @skip_if_not_root
    def test_set_computer_desc(self):
        '''
        Test setting the computer description
        '''
        self._save_machine_info()
        desc = "test"
        ret = self.run_function('system.set_computer_desc', [desc])
        computer_desc = self.run_function('system.get_computer_desc')

        self.assertTrue(ret)
        self.assertIn(desc, computer_desc)

    @destructiveTest
    @skip_if_not_root
    def test_set_computer_desc_multiline(self):
        '''
        Test setting the computer description with a multiline string with tabs
        and double-quotes.
        '''
        self._save_machine_info()
        desc = textwrap.dedent('''\
            'First Line
            \tSecond Line: 'single-quoted string'
            \t\tThird Line: "double-quoted string with unicode: питон"''')
        ret = self.run_function('system.set_computer_desc', [desc])
        # self.run_function returns the serialized return, we need to convert
        # back to unicode to compare to desc. in the assertIn below.
        computer_desc = salt.utils.stringutils.to_unicode(
            self.run_function('system.get_computer_desc')
        )

        self.assertTrue(ret)
        self.assertIn(desc, computer_desc)

    @skip_if_not_root
    def test_has_hwclock(self):
        '''
        Verify platform has a settable hardware clock, if possible.
        '''
        if self.run_function('grains.get', ['os_family']) == 'NILinuxRT':
            self.assertTrue(self.run_function('system._has_settable_hwclock'))
            self.assertTrue(self._hwclock_has_compare())


@skipIf(not salt.utils.platform.is_windows(), 'These tests can only be run on windows')
class WinSystemModuleTest(ModuleCase):
    '''
    Validate the date/time functions in the win_system module
    '''

    @classmethod
    def tearDownClass(cls):
        if subprocess.call('w32tm /resync', shell=True) != 0:
            log.error("Re-syncing time failed")

    def test_get_computer_name(self):
        '''
        Test getting the computer name
        '''
        ret = self.run_function('system.get_computer_name')

        self.assertTrue(isinstance(ret, six.text_type))
        import socket
        name = socket.gethostname()
        self.assertEqual(name, ret)

    @destructiveTest
    def test_set_computer_desc(self):
        '''
        Test setting the computer description
        '''
        desc = 'test description'
        set_desc = self.run_function('system.set_computer_desc', [desc])
        self.assertTrue(set_desc)

        get_desc = self.run_function('system.get_computer_desc')
        self.assertEqual(set_desc['Computer Description'], get_desc)

    def test_get_system_time(self):
        '''
        Test getting the system time
        '''
        ret = self.run_function('system.get_system_time')
        now = datetime.datetime.now()
        self.assertEqual(now.strftime("%I:%M"), ret.rsplit(':', 1)[0])

    @flaky
    @destructiveTest
    @flaky
    def test_set_system_time(self):
        '''
        Test setting the system time
        '''
        test_time = '10:55'
        set_time = self.run_function('system.set_system_time', [test_time + ' AM'])
        get_time = self.run_function('system.get_system_time').rsplit(':', 1)[0]
        self.assertEqual(get_time, test_time)

    def test_get_system_date(self):
        '''
        Test getting system date
        '''
        ret = self.run_function('system.get_system_date')
        date = datetime.datetime.now().date().strftime("%m/%d/%Y")
        self.assertEqual(date, ret)

    @destructiveTest
    def test_set_system_date(self):
        '''
        Test setting system date
        '''
        self.assertTrue(self.run_function('system.set_system_date', ['3/25/2018']))
