# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import unittest
from unittest import mock

from charmhelpers.contrib.storage.linux.ceph import (
    CephBrokerRq,
)

with mock.patch('charmhelpers.core.hookenv.metadata') as _meta:
    _meta.return_Value = 'ss'
    from ceph_client import requires


_hook_args = {}

TO_PATCH = []
TO_PATCH_BASE_REQUIRES = [
    'is_request_complete',
]


def mock_hook(*args, **kwargs):

    def inner(f):
        # remember what we were passed.  Note that we can't actually determine
        # the class we're attached to, as the decorator only gets the function.
        _hook_args[f.__name__] = dict(args=args, kwargs=kwargs)
        return f
    return inner


class DummyRequest(CephBrokerRq):

    def __init__(self, req_json=None, request_id=12):
        super().__init__(request_id=request_id)
        if req_json:
            self.set_ops(json.loads(req_json)['ops'])


class TestCephClientRequires(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls._patched_hook = mock.patch('charms.reactive.when', mock_hook)
        cls._patched_hook_started = cls._patched_hook.start()
        # force requires to rerun the mock_hook decorator:
        # try except is Python2/Python3 compatibility as Python3 has moved
        # reload to importlib.
        try:
            reload(requires)
        except NameError:
            import importlib
            importlib.reload(requires)

    @classmethod
    def tearDownClass(cls):
        cls._patched_hook.stop()
        cls._patched_hook_started = None
        cls._patched_hook = None
        # and fix any breakage we did to the module
        try:
            reload(requires)
        except NameError:
            import importlib
            importlib.reload(requires)

    def patch(self, method, obj=None):
        target_obj = obj or self.obj
        _m = mock.patch.object(target_obj, method)
        _mock = _m.start()
        self.addCleanup(_m.stop)
        return _mock

    def setUp(self):
        self.cr = requires.CephClientRequires('some-relation', [])
        self._patches = {}
        self._patches_start = {}
        self.obj = requires
        for method in TO_PATCH:
            setattr(self, method, self.patch(method))
        for method in TO_PATCH_BASE_REQUIRES:
            setattr(self, method, self.patch(method, requires.base_requires))
        self.patch_object(requires.base_requires.reactive, 'set_flag')
        self.patch_object(requires.base_requires.reactive, 'clear_flag')

    def tearDown(self):
        self.cr = None
        for k, v in self._patches.items():
            v.stop()
            setattr(self, k, None)
        self._patches = None
        self._patches_start = None

    def patch_kr(self, attr, return_value=None):
        mocked = mock.patch.object(self.cr, attr)
        self._patches[attr] = mocked
        started = mocked.start()
        started.return_value = return_value
        self._patches_start[attr] = started
        setattr(self, attr, started)

    def patch_object(self, obj, attr, name=None, **kwargs):
        """Patch a patchable thing.  Uses mock.patch.object() to do the work.
        Automatically unpatches at the end of the test.

        The mock gets added to the test object (self) using 'name' or the attr
        passed in the arguments.

        :param obj: an object that needs to have an attribute patched.
        :param attr: <string> that represents the attribute being patched.
        :param name: optional <string> name to call the mock.
        :param **kwargs: any other args to pass to mock.patch()
        """
        mocked = mock.patch.object(obj, attr, **kwargs)
        if name is None:
            name = attr
        started = mocked.start()
        self._patches[name] = mocked
        self._patches_start[name] = started
        setattr(self, name, started)

    def test_registered_hooks(self):
        # test that the decorators actually registered the relation
        # expressions that are meaningful for this interface: this is to
        # handle regressions.
        # The keys are the function names that the hook attaches to.
        hook_patterns = {
            'data_changed': ('endpoint.{endpoint_name}.changed', ),
            'joined': ('endpoint.{endpoint_name}.joined', ),
            'broken': ('endpoint.{endpoint_name}.broken', ),
            'changed': ('endpoint.{endpoint_name}.changed', ),
            'departed': ('endpoint.{endpoint_name}.departed', ),
        }
        for k, v in _hook_args.items():
            self.assertEqual(hook_patterns[k], v['args'])

    def test_data_changed(self):
        self.patch_kr('_key', 'key1')
        self.patch_kr('_auth', 'auth1')
        self.patch_kr('mon_hosts', 'host1')
        self.cr.changed()
        self.set_flag.assert_called_once_with('some-relation.available')

    def test_data_changed_incomplete(self):
        self.patch_kr('_key', 'key1')
        self.patch_kr('_auth', None)
        self.patch_kr('mon_hosts', 'host1')
        self.cr.changed()
        self.assertFalse(self.set_flag.called)

    def test_data_changed_existing_broker_rq(self):
        self.patch_kr('_key', 'key1')
        self.patch_kr('_auth', 'auth1')
        self.patch_kr('mon_hosts', 'host1')
        self.patch_kr('get_current_request', DummyRequest())
        self.is_request_complete.return_value = True
        self.cr.changed()
        self.set_flag.assert_has_calls([
            mock.call('some-relation.available'),
            mock.call('some-relation.pools.available')])

    def test_date_changed_existing_broker_rq_incomplete(self):
        self.patch_kr('_key', 'key1')
        self.patch_kr('_auth', 'auth1')
        self.patch_kr('mon_hosts', 'host1')
        self.is_request_complete.return_value = False
        self.cr.changed()
        # Side effect of asserting pools.available was not set.
        self.set_flag.assert_called_once_with('some-relation.available')

    def test_manage_flags(self):
        self.cr.manage_flags()
        self.clear_flag.assert_has_calls(
            [
                mock.call("some-relation.available"),
                mock.call("some-relation.connected"),
                mock.call("some-relation.pools.available"),
            ]
        )

    def test_broken(self):
        self.cr.broken()
        self.clear_flag.assert_has_calls([
            mock.call('some-relation.available'),
            mock.call('some-relation.connected'),
            mock.call('some-relation.pools.available')])

    def test_create_replicated_pool(self):
        self.patch_kr('get_current_request')
        self.patch_object(requires.base_requires, 'CephBrokerRq')
        self.patch_kr('send_request_if_needed')
        self.get_current_request.return_value = None
        rq = mock.MagicMock()
        self.CephBrokerRq.return_value = rq
        self.cr.create_replicated_pool('bob')
        rq.add_op_create_replicated_pool.assert_called_once_with(
            name='bob',
            replica_count=3,
            pg_num=None,
            weight=None,
            group=None,
            namespace=None,
            app_name=None)
        existing_request = mock.MagicMock()
        self.get_current_request.return_value = existing_request
        self.cr.create_replicated_pool('bob')
        existing_request.add_op_create_replicated_pool.assert_called_once_with(
            name='bob',
            replica_count=3,
            pg_num=None,
            weight=None,
            group=None,
            namespace=None,
            app_name=None)

    def test_request_access_to_group_new_request(self):
        self.patch_kr('send_request_if_needed')
        self.cr.request_access_to_group(
            'volumes',
            key_name='cinder',
            object_prefix_permissions={'class-read': ['rbd_children']},
            permission='rwx')
        ceph_broker_rq = self.send_request_if_needed.mock_calls[0][1][0]
        self.assertEqual(
            ceph_broker_rq.ops,
            [{
                'group': 'volumes',
                'group-permission': 'rwx',
                'name': 'cinder',
                'namespace': None,
                'object-prefix-permissions': {'class-read': ['rbd_children']},
                'op': 'add-permissions-to-key'}])

    def test_request_access_to_group_existing_request(self):
        self.patch_kr('send_request_if_needed')
        req = (
            '{"api-version": 1, '
            '"ops": [{"op": "create-pool", "name": "volumes", "replicas": 3, '
            '"pg_num": null, "weight": null, "group": null, '
            '"group-namespace": null}], '
            '"request-id": "9e34123e-fa0c-11e8-ad9c-fa163ed1cc55"}')
        existing_request = DummyRequest(req_json=req)
        self.patch_kr('get_current_request', existing_request)
        self.cr.request_access_to_group(
            'volumes',
            key_name='cinder',
            object_prefix_permissions={'class-read': ['rbd_children']},
            permission='rwx')
        self.assertEqual(
            existing_request.ops,
            [
                {
                    'op': 'create-pool',
                    'name': 'volumes',
                    'replicas': 3,
                    'group': None,
                    'group-namespace': None,
                    'pg_num': None,
                    'weight': None},
                {
                    'group': 'volumes',
                    'group-permission': 'rwx',
                    'name': 'cinder',
                    'namespace': None,
                    'object-prefix-permissions': {
                        'class-read': ['rbd_children']},
                    'op': 'add-permissions-to-key'}])

    def test_get_remote_all(self):
        unit_data = {
            'rid:1': {
                'app1/0': {
                    'key1': 'value1',
                    'key2': 'value2'},
                'app1/1': {
                    'key1': 'value1',
                    'key2': 'value3'}},
            'rid:2': {
                'app2/0': {
                    'key1': 'value1',
                    'key2': 'value3'}},
            'rid:3': {}}
        unit0_r1_mock = mock.MagicMock()
        unit0_r1_mock.received = unit_data['rid:1']['app1/0']
        unit1_r1_mock = mock.MagicMock()
        unit1_r1_mock.received = unit_data['rid:1']['app1/1']
        unit0_r2_mock = mock.MagicMock()
        unit0_r2_mock.received = unit_data['rid:2']['app2/0']
        rel1 = mock.MagicMock()
        rel1.units = [unit0_r1_mock, unit1_r1_mock]
        rel2 = mock.MagicMock()
        rel2.units = [unit0_r2_mock]
        rel3 = mock.MagicMock()
        rel3.units = []

        self.patch_kr('_relations')
        self._relations.__iter__.return_value = [rel1, rel2, rel3]
        # Check de-duplication:
        self.assertEqual(
            self.cr.get_remote_all('key1'),
            ['value1'])
        # Check multiple values:
        self.assertEqual(
            self.cr.get_remote_all('key2'),
            ['value2', 'value3'])
        # Check missing key
        self.assertEqual(
            self.cr.get_remote_all('key100'),
            [])
        # Check missing key with default
        self.assertEqual(
            self.cr.get_remote_all('key100', default='defaultvalue'),
            ['defaultvalue'])

    def test_mon_hosts(self):
        self.patch_kr('get_remote_all', ['10.0.0.10 10.0.0.12', '10.0.0.23'])
        self.assertEqual(
            self.cr.mon_hosts(),
            ['10.0.0.10', '10.0.0.12', '10.0.0.23'])
