import contextlib
import copy
import datetime
import json
import tempfile
import shutil
import yaml

from mock import patch, call

import charmhelpers.contrib.openstack.deferred_events as deferred_events
import tests.utils


class TestDB(object):
    '''Test KV store for unitdata testing'''
    def __init__(self):
        self.data = {}
        self.flushed = False

    def get(self, key, default=None):
        result = self.data.get(key, default)
        if not result:
            return default
        return json.loads(result)

    def set(self, key, value):
        self.data[key] = json.dumps(value)
        return value

    def flush(self):
        self.flushed = True


class TestHookData(object):

    def __init__(self, kv):
        self.kv = kv

    @contextlib.contextmanager
    def __call__(self):
        yield self.kv, True, True


class DeferredCharmServiceEventsTestCase(tests.utils.BaseTestCase):

    def setUp(self):
        super(DeferredCharmServiceEventsTestCase, self).setUp()
        self.tmp_dir = tempfile.mkdtemp()
        self.addCleanup(lambda: shutil.rmtree(self.tmp_dir))
        self.patch_object(deferred_events.hookenv, 'service_name')
        self.service_name.return_value = 'myapp'
        self.patch_object(deferred_events.unitdata, 'HookData')
        self.db = TestDB()
        self.HookData.return_value = TestHookData(self.db)
        self.exp_event_a = deferred_events.ServiceEvent(
            timestamp=123,
            service='svcA',
            reason='ReasonA',
            action='restart',
            policy_requestor_name='myapp',
            policy_requestor_type='charm')
        self.exp_event_b = deferred_events.ServiceEvent(
            timestamp=223,
            service='svcB',
            reason='ReasonB',
            action='restart')
        self.exp_event_c = deferred_events.ServiceEvent(
            timestamp=323,
            service='svcB',
            reason='ReasonB',
            action='restart')
        self.base_expect_events = [
            self.exp_event_a,
            self.exp_event_b,
            self.exp_event_c]
        self.event_file_pair = []
        for index, event in enumerate(self.base_expect_events):
            event_file = '{}/{}.deferred'.format('/tmpdir', str(index))
            self.event_file_pair.append((
                event_file,
                event))

    def test_matching_request_event(self):
        self.assertTrue(
            self.exp_event_b.matching_request(
                self.exp_event_c))
        self.assertFalse(
            self.exp_event_a.matching_request(
                self.exp_event_b))

    @patch.object(deferred_events.glob, "glob")
    def test_deferred_events_files(self, glob):
        defer_files = [
            '/var/lib/policy-rc.d/charm-myapp/1612346300.deferred',
            '/var/lib/policy-rc.d/charm-myapp/1612346322.deferred',
            '/var/lib/policy-rc.d/charm-myapp/1612346360.deferred']

        glob.return_value = defer_files
        self.assertEqual(
            deferred_events.deferred_events_files(),
            defer_files)

    def test_read_event_file(self):
        with tempfile.NamedTemporaryFile('w') as ftmp:
            yaml.dump(vars(self.exp_event_a), ftmp)
            ftmp.flush()
            self.assertEqual(
                deferred_events.read_event_file(ftmp.name),
                self.exp_event_a)

    @patch.object(deferred_events, "deferred_events_files")
    def test_deferred_events(self, deferred_events_files):
        event_files = []
        expect = []
        for index, event in enumerate(self.base_expect_events):
            event_file = '{}/{}.deferred'.format(self.tmp_dir, str(index))
            with open(event_file, 'w') as f:
                yaml.dump(vars(event), f)
            event_files.append(event_file)
            expect.append((
                event_file,
                event))
        deferred_events_files.return_value = event_files
        self.assertEqual(
            deferred_events.deferred_events(),
            expect)

    @patch.object(deferred_events, "deferred_events")
    def test_duplicate_event_files(self, _deferred_events):
        _deferred_events.return_value = self.event_file_pair
        self.assertEqual(
            deferred_events.duplicate_event_files(self.exp_event_b),
            ['/tmpdir/1.deferred', '/tmpdir/2.deferred'])
        self.assertEqual(
            deferred_events.duplicate_event_files(deferred_events.ServiceEvent(
                timestamp=223,
                service='svcX',
                reason='ReasonX',
                action='restart')),
            [])

    @patch.object(deferred_events.uuid, "uuid1")
    def test_get_event_record_file(self, uuid1):
        uuid1.return_value = '89eb8258'
        self.assertEqual(
            deferred_events.get_event_record_file(
                'charm',
                'neutron-ovs'),
            '/var/lib/policy-rc.d/charm-neutron-ovs-89eb8258.deferred')

    @patch.object(deferred_events, "get_event_record_file")
    @patch.object(deferred_events, "duplicate_event_files")
    @patch.object(deferred_events, "init_policy_log_dir")
    def test_save_event(self, init_policy_log_dir, duplicate_event_files, get_event_record_file):
        duplicate_event_files.return_value = []
        test_file = '{}/test_save_event.yaml'.format(self.tmp_dir)
        get_event_record_file.return_value = test_file
        deferred_events.save_event(self.exp_event_a)
        with open(test_file, 'r') as f:
            contents = yaml.safe_load(f)
        self.assertEqual(contents, vars(self.exp_event_a))

    @patch.object(deferred_events.os, "remove")
    @patch.object(deferred_events, "read_event_file")
    @patch.object(deferred_events, "deferred_events_files")
    def test_clear_deferred_events(self, deferred_events_files, read_event_file,
                                   remove):
        deferred_events_files.return_value = ['/tmp/file1']
        read_event_file.return_value = self.exp_event_a
        deferred_events.clear_deferred_events('svcB', 'restart')
        self.assertFalse(remove.called)
        deferred_events.clear_deferred_events('svcA', 'restart')
        remove.assert_called_once_with('/tmp/file1')

    @patch.object(deferred_events.os, "mkdir")
    @patch.object(deferred_events.os.path, "exists")
    def test_init_policy_log_dir(self, exists, mkdir):
        exists.return_value = True
        deferred_events.init_policy_log_dir()
        self.assertFalse(mkdir.called)
        exists.return_value = False
        deferred_events.init_policy_log_dir()
        mkdir.assert_called_once_with('/var/lib/policy-rc.d')

    @patch.object(deferred_events, "deferred_events")
    def test_get_deferred_events(self, _deferred_events):
        _deferred_events.return_value = self.event_file_pair
        self.assertEqual(
            deferred_events.get_deferred_events(),
            self.base_expect_events)

    @patch.object(deferred_events, "get_deferred_events")
    def test_get_deferred_restarts(self, get_deferred_events):
        test_events = copy.deepcopy(self.base_expect_events)
        test_events.append(
            deferred_events.ServiceEvent(
                timestamp=523,
                service='svcD',
                reason='StopReasonD',
                action='stop'))
        get_deferred_events.return_value = test_events
        self.assertEqual(
            deferred_events.get_deferred_restarts(),
            self.base_expect_events)

    @patch.object(deferred_events, 'clear_deferred_events')
    def test_clear_deferred_restarts(self, clear_deferred_events):
        deferred_events.clear_deferred_restarts(['svcA', 'svcB'])
        clear_deferred_events.assert_Called_once_with(
            ['svcA', 'svcB'],
            'restart')

    @patch.object(deferred_events, 'clear_deferred_restarts')
    def test_process_svc_restart(self, clear_deferred_restarts):
        deferred_events.process_svc_restart('svcA')
        clear_deferred_restarts.assert_called_once_with(
            ['svcA'])

    @patch.object(deferred_events.hookenv, 'config')
    def test_is_restart_permitted(self, config):
        config.return_value = None
        self.assertTrue(deferred_events.is_restart_permitted())
        config.return_value = True
        self.assertTrue(deferred_events.is_restart_permitted())
        config.return_value = False
        self.assertFalse(deferred_events.is_restart_permitted())

    @patch.object(deferred_events.time, 'time')
    @patch.object(deferred_events, 'save_event')
    @patch.object(deferred_events, 'is_restart_permitted')
    def test_check_and_record_restart_request(self, is_restart_permitted,
                                              save_event, time):
        time.return_value = 123
        is_restart_permitted.return_value = False
        deferred_events.check_and_record_restart_request(
            'svcA',
            ['/tmp/test1.conf', '/tmp/test2.conf'])
        save_event.assert_called_once_with(deferred_events.ServiceEvent(
            timestamp=123,
            service='svcA',
            reason='File(s) changed: /tmp/test1.conf, /tmp/test2.conf',
            action='restart'))

    @patch.object(deferred_events.time, 'time')
    @patch.object(deferred_events, 'save_event')
    @patch.object(deferred_events.host, 'service_restart')
    @patch.object(deferred_events, 'is_restart_permitted')
    def test_deferrable_svc_restart(self, is_restart_permitted,
                                    service_restart, save_event, time):
        time.return_value = 123
        is_restart_permitted.return_value = True
        deferred_events.deferrable_svc_restart('svcA', reason='ReasonA')
        service_restart.assert_called_once_with('svcA')
        service_restart.reset_mock()
        is_restart_permitted.return_value = False
        deferred_events.deferrable_svc_restart('svcA', reason='ReasonA')
        self.assertFalse(service_restart.called)
        save_event.assert_called_once_with(deferred_events.ServiceEvent(
            timestamp=123,
            service='svcA',
            reason='ReasonA',
            action='restart'))

    @patch.object(deferred_events.policy_rcd, 'add_policy_block')
    @patch.object(deferred_events.policy_rcd, 'remove_policy_file')
    @patch.object(deferred_events, 'is_restart_permitted')
    @patch.object(deferred_events.policy_rcd, 'install_policy_rcd')
    def test_configure_deferred_restarts(self, install_policy_rcd,
                                         is_restart_permitted,
                                         remove_policy_file, add_policy_block):
        is_restart_permitted.return_value = True
        deferred_events.configure_deferred_restarts(['svcA', 'svcB'])
        remove_policy_file.assert_called_once_with()
        install_policy_rcd.assert_called_once_with()

        remove_policy_file.reset_mock()
        install_policy_rcd.reset_mock()
        is_restart_permitted.return_value = False
        deferred_events.configure_deferred_restarts(['svcA', 'svcB'])
        self.assertFalse(remove_policy_file.called)
        install_policy_rcd.assert_called_once_with()
        add_policy_block.assert_has_calls([
            call('svcA', ['stop', 'restart', 'try-restart']),
            call('svcB', ['stop', 'restart', 'try-restart'])])

    @patch.object(deferred_events.subprocess, 'check_output')
    def test_get_service_start_time(self, check_output):
        check_output.return_value = (
            b'ActiveEnterTimestamp=Tue 2021-02-02 13:19:55 UTC')
        expect = datetime.datetime.strptime(
            'Tue 2021-02-02 13:19:55 UTC',
            '%a %Y-%m-%d %H:%M:%S %Z')
        self.assertEqual(
            deferred_events.get_service_start_time('svcA'),
            expect)
        check_output.assert_called_once_with(
            ['systemctl', 'show', 'svcA', '--property=ActiveEnterTimestamp'])

    @patch.object(deferred_events, 'get_deferred_restarts')
    @patch.object(deferred_events, 'clear_deferred_restarts')
    @patch.object(deferred_events.hookenv, 'log')
    @patch.object(deferred_events, 'get_service_start_time')
    def test_check_restart_timestamps(self, get_service_start_time, log,
                                      clear_deferred_restarts,
                                      get_deferred_restarts):
        deferred_restarts = [
            # 'Tue 2021-02-02 10:19:55 UTC'
            deferred_events.ServiceEvent(
                timestamp=1612261195.0,
                service='svcA',
                reason='ReasonA',
                action='restart')]
        get_deferred_restarts.return_value = deferred_restarts
        get_service_start_time.return_value = datetime.datetime.strptime(
            'Tue 2021-02-02 13:19:55 UTC',
            '%a %Y-%m-%d %H:%M:%S %Z')
        deferred_events.check_restart_timestamps()
        clear_deferred_restarts.assert_called_once_with(['svcA'])

        clear_deferred_restarts.reset_mock()
        get_service_start_time.return_value = datetime.datetime.strptime(
            'Tue 2021-02-02 10:10:55 UTC',
            '%a %Y-%m-%d %H:%M:%S %Z')
        deferred_events.check_restart_timestamps()
        self.assertFalse(clear_deferred_restarts.called)
        log.assert_called_once_with(
            ('Restart still required, svcA was started at 2021-02-02 10:10:55,'
             ' restart was requested after that at {}'.format(
                 datetime.datetime.fromtimestamp(1612261195.0))),
            level='DEBUG')

    def test_set_deferred_hook(self):
        deferred_events.set_deferred_hook('config-changed')
        self.assertEqual(self.db.get('deferred-hooks'), ['config-changed'])
        deferred_events.set_deferred_hook('leader-settings-changed')
        self.assertEqual(
            self.db.get('deferred-hooks'),
            ['config-changed', 'leader-settings-changed'])

    def test_get_deferred_hook(self):
        deferred_events.set_deferred_hook('config-changed')
        self.assertEqual(
            deferred_events.get_deferred_hooks(),
            ['config-changed'])

    def test_clear_deferred_hooks(self):
        deferred_events.set_deferred_hook('config-changed')
        deferred_events.set_deferred_hook('leader-settings-changed')
        self.assertEqual(
            deferred_events.get_deferred_hooks(),
            ['config-changed', 'leader-settings-changed'])
        deferred_events.clear_deferred_hooks()
        self.assertEqual(
            deferred_events.get_deferred_hooks(),
            [])

    def test_clear_deferred_hook(self):
        deferred_events.set_deferred_hook('config-changed')
        deferred_events.set_deferred_hook('leader-settings-changed')
        self.assertEqual(
            deferred_events.get_deferred_hooks(),
            ['config-changed', 'leader-settings-changed'])
        deferred_events.clear_deferred_hook('leader-settings-changed')
        self.assertEqual(
            deferred_events.get_deferred_hooks(),
            ['config-changed'])
