# 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.


from unittest import mock

from cinderclient import client as ciclient
from cinderclient.v3 import client as ciclient_v3
from gnocchiclient import client as gnclient
from gnocchiclient.v1 import client as gnclient_v1
from ironicclient import client as irclient
from ironicclient.v1 import client as irclient_v1
from keystoneauth1 import adapter as ka_adapter
from keystoneauth1 import loading as ka_loading

from watcher.common import clients
from watcher.common import context
from watcher import conf
from watcher.tests.unit import base

CONF = conf.CONF


class TestBaseClients(base.TestCase):

    def _register_watcher_clients_auth_opts(self):
        _AUTH_CONF_GROUP = 'watcher_clients_auth'
        ka_loading.register_auth_conf_options(CONF, _AUTH_CONF_GROUP)
        ka_loading.register_session_conf_options(CONF, _AUTH_CONF_GROUP)
        CONF.set_override('auth_type', 'password', group=_AUTH_CONF_GROUP)

        # ka_loading.load_auth_from_conf_options(CONF, _AUTH_CONF_GROUP)
        # ka_loading.load_session_from_conf_options(CONF, _AUTH_CONF_GROUP)
        # CONF.set_override(
        #     'auth-url', 'http://server.ip:5000', group=_AUTH_CONF_GROUP)

        # If we don't clean up the _AUTH_CONF_GROUP conf options, then other
        # tests that run after this one will fail, complaining about required
        # options that _AUTH_CONF_GROUP wants.
        def cleanup_conf_from_loading():
            # oslo_config doesn't seem to allow unregistering groups through a
            # single method, so we do this instead
            CONF.reset()
            del CONF._groups[_AUTH_CONF_GROUP]
            # register again the auth options so the group is not empty
            ka_loading.register_auth_conf_options(CONF, _AUTH_CONF_GROUP)
            ka_loading.register_session_conf_options(CONF, _AUTH_CONF_GROUP)

        self.addCleanup(cleanup_conf_from_loading)

        def reset_register_opts_mock(conf_obj, original_method):
            conf_obj.register_opts = original_method

        original_register_opts = CONF.register_opts
        self.addCleanup(reset_register_opts_mock,
                        CONF,
                        original_register_opts)

        expected = {'username': 'foousername',
                    'password': 'foopassword',
                    'auth_url': 'http://server.ip:5000',
                    'user_domain_id': 'foouserdomainid',
                    'project_domain_id': 'fooprojdomainid'}

        # Because some of the conf options for auth plugins are not registered
        # until right before they are loaded, and because the method that does
        # the actual loading of the conf option values is an anonymous method
        # (see _getter method of load_from_conf_options in
        # keystoneauth1.loading.conf.py), we need to manually monkey patch
        # the register opts method so that we can override the conf values to
        # our custom values.
        def mock_register_opts(*args, **kwargs):
            ret = original_register_opts(*args, **kwargs)
            if 'group' in kwargs and kwargs['group'] == _AUTH_CONF_GROUP:
                for key, value in expected.items():
                    CONF.set_override(key, value, group=_AUTH_CONF_GROUP)
            return ret

        CONF.register_opts = mock_register_opts


class TestClients(TestBaseClients):

    def test_get_keystone_session(self):
        self._register_watcher_clients_auth_opts()

        osc = clients.OpenStackClients()

        expected = {'username': 'foousername',
                    'password': 'foopassword',
                    'auth_url': 'http://server.ip:5000',
                    'user_domain_id': 'foouserdomainid',
                    'project_domain_id': 'fooprojdomainid'}

        sess = osc.session
        self.assertEqual(expected['auth_url'], sess.auth.auth_url)
        self.assertEqual(expected['username'], sess.auth._username)
        self.assertEqual(expected['password'], sess.auth._password)
        self.assertEqual(expected['user_domain_id'], sess.auth._user_domain_id)
        self.assertEqual(expected['project_domain_id'],
                         sess.auth._project_domain_id)

    @mock.patch.object(gnclient, 'Client')
    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_gnocchi(self, mock_session, mock_call):
        osc = clients.OpenStackClients()
        osc._gnocchi = None
        osc.gnocchi()
        mock_call.assert_called_once_with(
            CONF.gnocchi_client.api_version,
            adapter_options={
                "interface": CONF.gnocchi_client.endpoint_type,
                "region_name": CONF.gnocchi_client.region_name},
            session=mock_session)

    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_gnocchi_diff_vers(self, mock_session):
        # gnocchiclient currently only has one version (v1)
        CONF.set_override('api_version', '1', group='gnocchi_client')
        osc = clients.OpenStackClients()
        osc._gnocchi = None
        osc.gnocchi()
        self.assertEqual(gnclient_v1.Client, type(osc.gnocchi()))

    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_gnocchi_diff_endpoint(self, mock_session):
        # gnocchiclient currently only has one version (v1)
        CONF.set_override('endpoint_type', 'publicURL', group='gnocchi_client')
        osc = clients.OpenStackClients()
        osc._gnocchi = None
        osc.gnocchi()
        self.assertEqual('publicURL', osc.gnocchi().api.interface)

    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_gnocchi_cached(self, mock_session):
        osc = clients.OpenStackClients()
        osc._gnocchi = None
        gnocchi = osc.gnocchi()
        gnocchi_cached = osc.gnocchi()
        self.assertEqual(gnocchi, gnocchi_cached)

    @mock.patch.object(ciclient, 'Client')
    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_cinder(self, mock_session, mock_call):
        osc = clients.OpenStackClients()
        osc._cinder = None
        osc.cinder()
        mock_call.assert_called_once_with(
            CONF.cinder_client.api_version,
            endpoint_type=CONF.cinder_client.endpoint_type,
            region_name=CONF.cinder_client.region_name,
            session=mock_session)

    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_cinder_diff_vers(self, mock_session):
        CONF.set_override('api_version', '3', group='cinder_client')
        osc = clients.OpenStackClients()
        osc._cinder = None
        osc.cinder()
        self.assertEqual(ciclient_v3.Client, type(osc.cinder()))

    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_cinder_diff_endpoint(self, mock_session):
        CONF.set_override('endpoint_type',
                          'internalURL', group='cinder_client')
        osc = clients.OpenStackClients()
        osc._cinder = None
        osc.cinder()
        self.assertEqual('internalURL', osc.cinder().client.interface)

    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_cinder_cached(self, mock_session):
        osc = clients.OpenStackClients()
        osc._cinder = None
        cinder = osc.cinder()
        cinder_cached = osc.cinder()
        self.assertEqual(cinder, cinder_cached)

    @mock.patch.object(irclient, 'Client')
    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_ironic(self, mock_session, mock_call):
        ironic_url = 'http://localhost:6385/'
        mock_session.get_endpoint.return_value = ironic_url
        osc = clients.OpenStackClients()
        osc._ironic = None
        osc.ironic()
        mock_call.assert_called()

    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_ironic_diff_vers(self, mock_session):
        ironic_url = 'http://localhost:6385/'
        mock_session.get_endpoint.return_value = ironic_url
        CONF.set_override('api_version', '1', group='ironic_client')
        osc = clients.OpenStackClients()
        osc._ironic = None
        osc.ironic()
        self.assertEqual(irclient_v1.Client, type(osc.ironic()))

    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_ironic_diff_endpoint(self, mock_session):
        ironic_url = 'http://localhost:6385/'
        mock_session.get_endpoint.return_value = ironic_url
        osc = clients.OpenStackClients()
        osc._ironic = None
        osc.ironic()
        mock_session.get_endpoint.assert_called_with(
            interface='publicURL',
            region_name=None,
            service_type='baremetal')

        CONF.set_override('endpoint_type', 'internalURL',
                          group='ironic_client')
        osc._ironic = None
        osc.ironic()
        mock_session.get_endpoint.assert_called_with(
            interface='internalURL',
            region_name=None,
            service_type='baremetal')

    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_ironic_cached(self, mock_session):
        ironic_url = 'http://localhost:6385/'
        mock_session.get_endpoint.return_value = ironic_url
        osc = clients.OpenStackClients()
        osc._ironic = None
        ironic = osc.ironic()
        ironic_cached = osc.ironic()
        self.assertEqual(ironic, ironic_cached)

    @mock.patch.object(ka_adapter, 'Adapter')
    @mock.patch.object(clients.OpenStackClients, 'session')
    def test_clients_placement(self, mock_session, mock_call):
        osc = clients.OpenStackClients()
        osc.placement()
        headers = {'accept': 'application/json'}
        mock_call.assert_called_once_with(
            session=mock_session,
            service_type='placement',
            default_microversion=CONF.placement_client.api_version,
            interface=CONF.placement_client.interface,
            region_name=CONF.placement_client.region_name,
            additional_headers=headers)


class TestGetSDKConnection(TestBaseClients):
    """Test cases for get_sdk_connection function."""

    def setUp(self):
        self._register_watcher_clients_auth_opts()
        return super().setUp()

    @mock.patch('openstack.connection.Connection', autospec=True)
    def test_get_sdk_connection_with_context(
            self, mock_connect):
        """Test SDK connection creation with context."""

        context_obj = context.RequestContext(
            auth_token='test_token', project_id='test_project_id',
            project_domain='test_project_domain_id'
        )
        mock_connection = mock.Mock()
        mock_connect.return_value = mock_connection

        result = clients.get_sdk_connection(
            'watcher_clients_auth', context=context_obj
        )

        mock_connect.assert_called_once_with(
            token='test_token',
            auth_type='v3token',
            project_id='test_project_id',
            project_domain_id='test_project_domain_id',
            auth_url='http://server.ip:5000',
            interface=None,
            region_name=None
        )
        self.assertEqual(mock_connection, result)

    @mock.patch.object(ka_loading, 'load_auth_from_conf_options',
                       autospec=True)
    @mock.patch('openstack.connection.Connection', autospec=True)
    def test_get_sdk_connection_with_session(
            self, mock_connect, mock_load_auth):
        """Test SDK connection creation with provided session."""
        mock_session = mock.Mock()
        mock_connection = mock.Mock()
        mock_connect.return_value = mock_connection

        result = clients.get_sdk_connection(
            'watcher_clients_auth', session=mock_session
        )

        mock_connect.assert_called_once_with(
            session=mock_session,
            oslo_conf=CONF
        )
        mock_load_auth.assert_called_once_with(
            CONF, 'watcher_clients_auth'
        )
        self.assertEqual(mock_connection, result)

    @mock.patch.object(ka_loading, 'load_session_from_conf_options',
                       autospec=True)
    @mock.patch.object(ka_loading, 'load_auth_from_conf_options',
                       autospec=True)
    @mock.patch('openstack.connection.Connection', autospec=True)
    def test_get_sdk_connection_no_session_no_context(
            self, mock_connect, mock_load_auth, mock_load_session):
        """Test SDK connection creation without session or context."""
        mock_auth = mock.Mock()
        mock_session = mock.Mock()
        mock_connection = mock.Mock()
        mock_load_auth.return_value = mock_auth
        mock_load_session.return_value = mock_session
        mock_connect.return_value = mock_connection

        result = clients.get_sdk_connection('watcher_clients_auth')

        mock_load_auth.assert_called_once_with(
            CONF, 'watcher_clients_auth'
        )
        mock_load_session.assert_called_once_with(
            CONF,
            'watcher_clients_auth',
            auth=mock_auth)
        mock_connect.assert_called_once_with(
            session=mock_session,
            oslo_conf=CONF
        )
        self.assertEqual(mock_connection, result)
