# 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

import ddt
from keystoneauth1 import exceptions as kaexception
import requests

from ironic.common import context
from ironic.common import keystone
from ironic.common import nova
from ironic.tests import base


@mock.patch.object(keystone, 'get_session', autospec=True)
@mock.patch.object(keystone, 'get_adapter', autospec=True)
class TestNovaAdapter(base.TestCase):

    def test_get_nova_adapter(self, mock_adapter, mock_nova_session):
        nova._NOVA_ADAPTER = None
        mock_session_obj = mock.Mock()
        expected = {'session': mock_session_obj,
                    'auth': None,
                    'version': "2.1"}
        mock_nova_session.return_value = mock_session_obj
        nova._get_nova_adapter()
        mock_nova_session.assert_called_once_with('nova')
        mock_adapter.assert_called_once_with(group='nova', **expected)

        """Check if existing adapter is used."""
        mock_nova_session.reset_mock()
        nova._get_nova_adapter()
        mock_nova_session.assert_not_called()


@ddt.ddt
@mock.patch.object(nova, 'LOG', autospec=True)
class NovaApiTestCase(base.TestCase):
    def setUp(self):
        super(NovaApiTestCase, self).setUp()

        self.api = nova
        self.ctx = context.get_admin_context()

    @ddt.unpack
    # one @ddt.data element comprises:
    # - nova_result: POST response JSON dict
    # - resp_status: POST response status_code
    # - exp_ret: Expected bool return value from power_update()
    @ddt.data([{'events': [{'status': 'completed',
                            'tag': 'POWER_OFF',
                            'name': 'power-update',
                            'server_uuid': '1234',
                            'code': 200}]},
               200, True],
              [{'events': [{'code': 422}]}, 207, False],
              [{'events': [{'code': 404}]}, 207, False],
              [{'events': [{'code': 400}]}, 207, False],
              # This (response 207, event code 200) will never happen IRL
              [{'events': [{'code': 200}]}, 207, True])
    @mock.patch.object(nova, '_get_nova_adapter', autospec=True)
    def test_power_update(self, nova_result, resp_status, exp_ret,
                          mock_adapter, mock_log):
        server_ids = ['server-id-1', 'server-id-2']
        nova_adapter = mock.Mock()
        mock_post_event = nova_adapter.post
        post_resp_mock = requests.Response()

        def json_func():
            return nova_result
        post_resp_mock.json = json_func
        post_resp_mock.status_code = resp_status
        mock_adapter.return_value = nova_adapter
        mock_post_event.return_value = post_resp_mock
        for server in server_ids:
            result = self.api.power_update(self.ctx, server, 'power on')
            self.assertEqual(exp_ret, result)

        mock_adapter.assert_has_calls([mock.call(), mock.call()])
        req_url = '/os-server-external-events'
        mock_post_event.assert_has_calls([
            mock.call(req_url,
                      json={'events': [{'name': 'power-update',
                                        'server_uuid': 'server-id-1',
                                        'tag': 'POWER_ON'}]},
                      microversion='2.76',
                      global_request_id=self.ctx.global_id,
                      raise_exc=False),
            mock.call(req_url,
                      json={'events': [{'name': 'power-update',
                                        'server_uuid': 'server-id-2',
                                        'tag': 'POWER_ON'}]},
                      microversion='2.76',
                      global_request_id=self.ctx.global_id,
                      raise_exc=False)
        ])
        if not exp_ret:
            expected = ('Nova event: %s returned with failed status.',
                        nova_result['events'][0])
            mock_log.warning.assert_called_with(*expected)
        else:
            expected = ("Nova event response: %s.", nova_result['events'][0])
            mock_log.debug.assert_called_with(*expected)

    @mock.patch.object(nova, '_get_nova_adapter', autospec=True)
    def test_invalid_power_update(self, mock_adapter, mock_log):
        nova_adapter = mock.Mock()
        mock_post_event = nova_adapter.post
        result = self.api.power_update(self.ctx, 'server', None)
        self.assertFalse(result)
        expected = ('Invalid Power State %s.', None)
        mock_log.error.assert_called_once_with(*expected)

        mock_adapter.assert_not_called()
        mock_post_event.assert_not_called()

    def test_power_update_failed(self, mock_log):
        nova_adapter = nova._get_nova_adapter()
        event = [{'name': 'power-update',
                  'server_uuid': 'server-id-1',
                  'tag': 'POWER_OFF'}]
        nova_result = requests.Response()
        with mock.patch.object(nova_adapter, 'post',
                               autospec=True) as mock_post_event:
            for stat_code in (500, 404, 400):
                mock_log.reset_mock()
                nova_result.status_code = stat_code
                type(nova_result).text = mock.PropertyMock(return_value="blah")
                mock_post_event.return_value = nova_result
                result = self.api.power_update(
                    self.ctx, 'server-id-1', 'power off')
                self.assertFalse(result)
                expected = ("Failed to notify nova on event: %s. %s.",
                            event[0], "blah")
                mock_log.warning.assert_called_once_with(*expected)

        mock_post_event.assert_has_calls([
            mock.call('/os-server-external-events',
                      json={'events': event},
                      microversion='2.76',
                      global_request_id=self.ctx.global_id,
                      raise_exc=False)
        ])

    @ddt.data({'events': [{}]},
              {'events': []},
              {'events': None},
              {})
    @mock.patch.object(nova, '_get_nova_adapter', autospec=True)
    def test_power_update_invalid_reponse_format(self, nova_result,
                                                 mock_adapter, mock_log):
        nova_adapter = mock.Mock()
        mock_post_event = nova_adapter.post
        post_resp_mock = requests.Response()

        def json_func():
            return nova_result

        post_resp_mock.json = json_func
        post_resp_mock.status_code = 207
        mock_adapter.return_value = nova_adapter
        mock_post_event.return_value = post_resp_mock
        result = self.api.power_update(self.ctx, 'server-id-1', 'power on')
        self.assertFalse(result)

        mock_adapter.assert_has_calls([mock.call()])
        req_url = '/os-server-external-events'
        mock_post_event.assert_has_calls([
            mock.call(req_url,
                      json={'events': [{'name': 'power-update',
                                        'server_uuid': 'server-id-1',
                                        'tag': 'POWER_ON'}]},
                      microversion='2.76',
                      global_request_id=self.ctx.global_id,
                      raise_exc=False),
        ])
        self.assertIn('Invalid response', mock_log.error.call_args[0][0])

    @mock.patch.object(keystone, 'get_adapter', autospec=True)
    def test_power_update_failed_no_nova(self, mock_adapter, mock_log):
        self.config(send_power_notifications=False, group="nova")
        result = self.api.power_update(self.ctx, 'server-id-1', 'power off')
        self.assertFalse(result)
        mock_adapter.assert_not_called()

    @mock.patch.object(nova, '_get_nova_adapter', autospec=True)
    def test_power_update_failed_no_nova_auth_url(self, mock_adapter,
                                                  mock_log):
        server = 'server-id-1'
        emsg = 'An auth plugin is required to determine endpoint URL'
        side_effect = kaexception.MissingAuthPlugin(emsg)
        mock_nova = mock.Mock()
        mock_adapter.return_value = mock_nova
        mock_nova.post.side_effect = side_effect
        result = self.api.power_update(self.ctx, server, 'power off')
        msg = ('Could not connect to Nova to send a power notification, '
               'please check configuration. %s', side_effect)
        self.assertFalse(result)
        mock_log.warning.assert_called_once_with(*msg)
        mock_adapter.assert_called_once_with()
