# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
#
# 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 types
from unittest import mock

import fixtures
import six
import testtools

from tempest.lib import auth
from tempest.lib import exceptions
from tempest.lib.services import clients
from tempest.tests import base
from tempest.tests.lib import fake_auth_provider
from tempest.tests.lib import fake_credentials


has_attribute = testtools.matchers.MatchesPredicateWithParams(
    lambda x, y: hasattr(x, y), '{0} does not have an attribute {1}')


class TestClientsFactory(base.TestCase):

    def setUp(self):
        super(TestClientsFactory, self).setUp()
        self.classes = []

    def _setup_fake_module(self, class_names=None, extra_dict=None):
        class_names = class_names or []
        fake_module = types.ModuleType('fake_service_client')
        _dict = {}
        # Add fake classes to the fake module
        for name in class_names:
            _dict[name] = type(name, (object,), {})
            # Store it for assertions
            self.classes.append(_dict[name])
        if extra_dict:
            _dict[extra_dict] = extra_dict
        fake_module.__dict__.update(_dict)
        fixture_importlib = self.useFixture(fixtures.MockPatch(
            'importlib.import_module', return_value=fake_module))
        return fixture_importlib.mock

    def test___init___one_class(self):
        fake_partial = 'fake_partial'
        partial_mock = self.useFixture(fixtures.MockPatch(
            'tempest.lib.services.clients.ClientsFactory._get_partial_class',
            return_value=fake_partial)).mock
        class_names = ['FakeServiceClient1']
        mock_importlib = self._setup_fake_module(class_names=class_names)
        auth_provider = fake_auth_provider.FakeAuthProvider()
        params = {'k1': 'v1', 'k2': 'v2'}
        factory = clients.ClientsFactory('fake_path', class_names,
                                         auth_provider, **params)
        # Assert module has been imported
        mock_importlib.assert_called_once_with('fake_path')
        # All attributes have been created
        for client in class_names:
            self.assertThat(factory, has_attribute(client))
        # Partial have been invoked correctly
        partial_mock.assert_called_once_with(
            self.classes[0], auth_provider, params)
        # Get the clients
        for name in class_names:
            self.assertEqual(fake_partial, getattr(factory, name))

    def test___init___two_classes(self):
        fake_partial = 'fake_partial'
        partial_mock = self.useFixture(fixtures.MockPatch(
            'tempest.lib.services.clients.ClientsFactory._get_partial_class',
            return_value=fake_partial)).mock
        class_names = ['FakeServiceClient1', 'FakeServiceClient2']
        mock_importlib = self._setup_fake_module(class_names=class_names)
        auth_provider = fake_auth_provider.FakeAuthProvider()
        params = {'k1': 'v1', 'k2': 'v2'}
        factory = clients.ClientsFactory('fake_path', class_names,
                                         auth_provider, **params)
        # Assert module has been imported
        mock_importlib.assert_called_once_with('fake_path')
        # All attributes have been created
        for client in class_names:
            self.assertThat(factory, has_attribute(client))
        # Partial have been invoked the right number of times
        partial_mock.call_count = len(class_names)
        # Get the clients
        for name in class_names:
            self.assertEqual(fake_partial, getattr(factory, name))

    def test___init___no_module(self):
        auth_provider = fake_auth_provider.FakeAuthProvider()
        class_names = ['FakeServiceClient1', 'FakeServiceClient2']
        self.assertRaises(ImportError, clients.ClientsFactory,
                          'fake_module', class_names, auth_provider)

    def test___init___not_a_class(self):
        class_names = ['FakeServiceClient1', 'FakeServiceClient2']
        extended_class_names = class_names + ['not_really_a_class']
        self._setup_fake_module(
            class_names=class_names, extra_dict='not_really_a_class')
        auth_provider = fake_auth_provider.FakeAuthProvider()
        expected_msg = '.*not_really_a_class.*str.*'
        with testtools.ExpectedException(TypeError, expected_msg):
            clients.ClientsFactory('fake_module', extended_class_names,
                                   auth_provider)

    def test___init___class_not_found(self):
        class_names = ['FakeServiceClient1', 'FakeServiceClient2']
        extended_class_names = class_names + ['not_really_a_class']
        self._setup_fake_module(class_names=class_names)
        auth_provider = fake_auth_provider.FakeAuthProvider()
        expected_msg = '.*not_really_a_class.*fake_service_client.*'
        with testtools.ExpectedException(AttributeError, expected_msg):
            clients.ClientsFactory('fake_module', extended_class_names,
                                   auth_provider)

    def test__get_partial_class_no_later_kwargs(self):
        expected_fake_client = 'not_really_a_client'
        self._setup_fake_module(class_names=[])
        auth_provider = fake_auth_provider.FakeAuthProvider()
        params = {'k1': 'v1', 'k2': 'v2'}
        factory = clients.ClientsFactory(
            'fake_path', [], auth_provider, **params)
        klass_mock = mock.Mock(return_value=expected_fake_client)
        partial = factory._get_partial_class(klass_mock, auth_provider, params)
        # Class has not be initialised yet
        klass_mock.assert_not_called()
        # Use partial and assert on parameters
        client = partial()
        self.assertEqual(expected_fake_client, client)
        klass_mock.assert_called_once_with(auth_provider=auth_provider,
                                           **params)

    def test__get_partial_class_later_kwargs(self):
        expected_fake_client = 'not_really_a_client'
        self._setup_fake_module(class_names=[])
        auth_provider = fake_auth_provider.FakeAuthProvider()
        params = {'k1': 'v1', 'k2': 'v2'}
        later_params = {'k2': 'v4', 'k3': 'v3'}
        factory = clients.ClientsFactory(
            'fake_path', [], auth_provider, **params)
        klass_mock = mock.Mock(return_value=expected_fake_client)
        partial = factory._get_partial_class(klass_mock, auth_provider, params)
        # Class has not be initialised yet
        klass_mock.assert_not_called()
        # Use partial and assert on parameters
        client = partial(**later_params)
        params.update(later_params)
        self.assertEqual(expected_fake_client, client)
        klass_mock.assert_called_once_with(auth_provider=auth_provider,
                                           **params)

    def test__get_partial_class_with_alias(self):
        expected_fake_client = 'not_really_a_client'
        client_alias = 'fake_client'
        self._setup_fake_module(class_names=[])
        auth_provider = fake_auth_provider.FakeAuthProvider()
        params = {'k1': 'v1', 'k2': 'v2'}
        later_params = {'k2': 'v4', 'k3': 'v3'}
        factory = clients.ClientsFactory(
            'fake_path', [], auth_provider, **params)
        klass_mock = mock.Mock(return_value=expected_fake_client)
        partial = factory._get_partial_class(klass_mock, auth_provider, params)
        # Class has not be initialised yet
        klass_mock.assert_not_called()
        # Use partial and assert on parameters
        client = partial(alias=client_alias, **later_params)
        params.update(later_params)
        self.assertEqual(expected_fake_client, client)
        klass_mock.assert_called_once_with(auth_provider=auth_provider,
                                           **params)
        self.assertThat(factory, has_attribute(client_alias))
        self.assertEqual(expected_fake_client, getattr(factory, client_alias))


class TestServiceClients(base.TestCase):

    def setUp(self):
        super(TestServiceClients, self).setUp()
        self.useFixture(fixtures.MockPatch(
            'tempest.lib.services.clients.tempest_modules',
            return_value=set(['fake_service1'])))

    def test___init___creds_v2_uri(self):
        # Verify that no API request is made, since no mock
        # is required to run the test successfully
        creds = fake_credentials.FakeKeystoneV2Credentials()
        uri = 'fake_uri'
        _manager = clients.ServiceClients(creds, identity_uri=uri)
        self.assertIsInstance(_manager.auth_provider,
                              auth.KeystoneV2AuthProvider)

    def test___init___creds_v3_uri(self):
        # Verify that no API request is made, since no mock
        # is required to run the test successfully
        creds = fake_credentials.FakeKeystoneV3Credentials()
        uri = 'fake_uri'
        _manager = clients.ServiceClients(creds, identity_uri=uri)
        self.assertIsInstance(_manager.auth_provider,
                              auth.KeystoneV3AuthProvider)

    def test___init___base_creds_uri(self):
        creds = fake_credentials.FakeCredentials()
        uri = 'fake_uri'
        with testtools.ExpectedException(exceptions.InvalidCredentials):
            clients.ServiceClients(creds, identity_uri=uri)

    def test___init___invalid_creds_uri(self):
        creds = fake_credentials.FakeKeystoneV2Credentials()
        delattr(creds, 'username')
        uri = 'fake_uri'
        with testtools.ExpectedException(exceptions.InvalidCredentials):
            clients.ServiceClients(creds, identity_uri=uri)

    def test___init___creds_uri_none(self):
        creds = fake_credentials.FakeKeystoneV2Credentials()
        msg = ("Invalid Credentials\nDetails: ServiceClients requires a "
               "non-empty")
        with testtools.ExpectedException(exceptions.InvalidCredentials,
                                         value_re=msg):
            clients.ServiceClients(creds, None)

    def test___init___creds_uri_params(self):
        creds = fake_credentials.FakeKeystoneV2Credentials()
        expeted_params = {'fake_param1': 'fake_value1',
                          'fake_param2': 'fake_value2'}
        params = {'fake_service1': expeted_params}
        uri = 'fake_uri'
        _manager = clients.ServiceClients(creds, identity_uri=uri,
                                          client_parameters=params)
        self.assertIn('fake_service1', _manager.parameters)
        for _key in expeted_params:
            self.assertIn(_key, _manager.parameters['fake_service1'].keys())
            self.assertEqual(expeted_params[_key],
                             _manager.parameters['fake_service1'].get(_key))

    def test___init___creds_uri_params_unknown_services(self):
        creds = fake_credentials.FakeKeystoneV2Credentials()
        fake_params = {'fake_param1': 'fake_value1'}
        params = {'unknown_service1': fake_params,
                  'unknown_service2': fake_params}
        uri = 'fake_uri'
        msg = "(?=.*{0})(?=.*{1})".format(*list(params.keys()))
        with testtools.ExpectedException(
                exceptions.UnknownServiceClient, value_re=msg):
            clients.ServiceClients(creds, identity_uri=uri,
                                   client_parameters=params)

    def test___init___plugin_service_clients_cannot_load(self):
        creds = fake_credentials.FakeKeystoneV3Credentials()
        uri = 'fake_uri'
        fake_service_clients = {
            'service1': [{'name': 'client1',
                          'service_version': 'client1.v1',
                          'module_path': 'I cannot load this',
                          'client_names': ['SomeClient1']}],
            'service2': [{'name': 'client2',
                          'service_version': 'client2.v1',
                          'module_path': 'This neither',
                          'client_names': ['SomeClient1']}]}
        msg = "(?=.*{0})(?=.*{1})".format(
            *[x[1][0]['module_path'] for x in six.iteritems(
                fake_service_clients)])
        self.useFixture(fixtures.MockPatchObject(
            clients.ClientsRegistry(), 'get_service_clients',
            return_value=fake_service_clients))
        with testtools.ExpectedException(
                testtools.MultipleExceptions, value_re=msg):
            clients.ServiceClients(creds, identity_uri=uri)

    def test___init___plugin_service_clients_name_conflict(self):
        creds = fake_credentials.FakeKeystoneV3Credentials()
        uri = 'fake_uri'
        fake_service_clients = {
            'serviceA': [{'name': 'client1',
                          'service_version': 'client1.v1',
                          'module_path': 'fake_path_1',
                          'client_names': ['SomeClient1']}],
            'serviceB': [{'name': 'client1',
                          'service_version': 'client1.v2',
                          'module_path': 'fake_path_2',
                          'client_names': ['SomeClient2']}],
            'serviceC': [{'name': 'client1',
                          'service_version': 'client1.v1',
                          'module_path': 'fake_path_2',
                          'client_names': ['SomeClient1']}],
            'serviceD': [{'name': 'client1',
                          'service_version': 'client1.v2',
                          'module_path': 'fake_path_2',
                          'client_names': ['SomeClient2']}]}
        msg = "(?=.*{0})(?=.*{1})".format(
            *[x[1][0]['service_version'] for x in six.iteritems(
                fake_service_clients)])
        self.useFixture(fixtures.MockPatchObject(
            clients.ClientsRegistry(), 'get_service_clients',
            return_value=fake_service_clients))
        with testtools.ExpectedException(
                testtools.MultipleExceptions, value_re=msg):
            clients.ServiceClients(creds, identity_uri=uri)

    def _get_manager(self, init_region='fake_region'):
        # Get a manager to invoke _setup_parameters on
        creds = fake_credentials.FakeKeystoneV2Credentials()
        return clients.ServiceClients(creds, identity_uri='fake_uri',
                                      region=init_region)

    def test__setup_parameters_none_no_region(self):
        kwargs = {}
        _manager = self._get_manager(init_region=None)
        _params = _manager._setup_parameters(kwargs)
        self.assertNotIn('region', _params)

    def test__setup_parameters_none(self):
        kwargs = {}
        _manager = self._get_manager()
        _params = _manager._setup_parameters(kwargs)
        self.assertIn('region', _params)
        self.assertEqual('fake_region', _params['region'])

    def test__setup_parameters_all(self):
        expected_params = {'region': 'fake_region1',
                           'catalog_type': 'fake_service2_mod',
                           'fake_param1': 'fake_value1',
                           'fake_param2': 'fake_value2'}
        _manager = self._get_manager()
        _params = _manager._setup_parameters(expected_params)
        for _key in _params.keys():
            self.assertEqual(expected_params[_key],
                             _params[_key])

    def test_register_service_client_module(self):
        expected_params = {'fake_param1': 'fake_value1',
                           'fake_param2': 'fake_value2'}
        _manager = self._get_manager(init_region='fake_region_default')
        # Mock after the _manager is setup to preserve the call count
        factory_mock = self.useFixture(fixtures.MockPatch(
            'tempest.lib.services.clients.ClientsFactory')).mock
        _manager.register_service_client_module(
            name='fake_module',
            service_version='fake_service',
            module_path='fake.path.to.module',
            client_names=[],
            **expected_params)
        self.assertThat(_manager, has_attribute('fake_module'))
        # Assert called once, without check for exact parameters
        self.assertTrue(factory_mock.called)
        self.assertEqual(1, factory_mock.call_count)
        # Assert expected params are in with their values
        actual_kwargs = factory_mock.call_args[1]
        self.assertIn('region', actual_kwargs)
        self.assertEqual('fake_region_default', actual_kwargs['region'])
        for param in expected_params:
            self.assertIn(param, actual_kwargs)
            self.assertEqual(expected_params[param], actual_kwargs[param])
        # Assert the new service is registered
        self.assertIn('fake_service', _manager._registered_services)

    def test_register_service_client_module_override_default(self):
        new_region = 'new_region'
        expected_params = {'fake_param1': 'fake_value1',
                           'fake_param2': 'fake_value2',
                           'region': new_region}
        _manager = self._get_manager(init_region='fake_region_default')
        # Mock after the _manager is setup to preserve the call count
        factory_mock = self.useFixture(fixtures.MockPatch(
            'tempest.lib.services.clients.ClientsFactory')).mock
        _manager.register_service_client_module(
            name='fake_module',
            service_version='fake_service',
            module_path='fake.path.to.module',
            client_names=[],
            **expected_params)
        self.assertThat(_manager, has_attribute('fake_module'))
        # Assert called once, without check for exact parameters
        self.assertTrue(factory_mock.called)
        self.assertEqual(1, factory_mock.call_count)
        # Assert expected params are in with their values
        actual_kwargs = factory_mock.call_args[1]
        self.assertIn('region', actual_kwargs)
        self.assertEqual(new_region, actual_kwargs['region'])
        for param in expected_params:
            self.assertIn(param, actual_kwargs)
            self.assertEqual(expected_params[param], actual_kwargs[param])
        # Assert the new service is registered
        self.assertIn('fake_service', _manager._registered_services)

    def test_register_service_client_module_duplicate_name(self):
        self.useFixture(fixtures.MockPatch(
            'tempest.lib.services.clients.ClientsFactory')).mock
        _manager = self._get_manager()
        name_owner = 'this_is_a_string'
        setattr(_manager, 'fake_module', name_owner)
        expected_error = '.*' + name_owner
        with testtools.ExpectedException(
                exceptions.ServiceClientRegistrationException, expected_error):
            _manager.register_service_client_module(
                name='fake_module', module_path='fake.path.to.module',
                service_version='fake_service', client_names=[])

    def test_register_service_client_module_duplicate_service(self):
        self.useFixture(fixtures.MockPatch(
            'tempest.lib.services.clients.ClientsFactory')).mock
        _manager = self._get_manager()
        duplicate_service = 'fake_service1'
        expected_error = '.*' + duplicate_service
        _manager._registered_services = [duplicate_service]
        with testtools.ExpectedException(
                exceptions.ServiceClientRegistrationException, expected_error):
            _manager.register_service_client_module(
                name='fake_module', module_path='fake.path.to.module',
                service_version=duplicate_service, client_names=[])
