#
# Copyright 2013-2015 eNovance
#
# 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 fixtures
import json
import time
from unittest import mock

from oslo_config import cfg
from oslo_config import fixture as fixture_config

import oslo_messaging
import requests
from urllib import parse as urlparse

from aodh import keystone_client
from aodh import notifier
from aodh import service

from aodh.tests import base as tests_base


DATA_JSON = json.loads(
    '{"current": "ALARM", "alarm_id": "foobar", "alarm_name": "testalarm",'
    ' "severity": "critical", "reason": "what ?",'
    ' "reason_data": {"test": "test"}, "previous": "OK"}'
)
NOTIFICATION = dict(alarm_id='foobar',
                    alarm_name='testalarm',
                    severity='critical',
                    condition=dict(threshold=42),
                    reason='what ?',
                    reason_data={'test': 'test'},
                    previous='OK',
                    current='ALARM')


class TestAlarmNotifierService(tests_base.BaseTestCase):

    def setUp(self):
        super(TestAlarmNotifierService, self).setUp()
        conf = service.prepare_service(argv=[], config_files=[])
        self.CONF = self.useFixture(fixture_config.Config(conf)).conf
        self.setup_messaging(self.CONF)

    def test_init_host_queue(self):
        self.service = notifier.AlarmNotifierService(0, self.CONF)
        self.service.terminate()


class TestKeystoneClient(tests_base.BaseTestCase):
    def setUp(self):
        super(TestKeystoneClient, self).setUp()
        self.config = fixture_config.Config(
            service.prepare_service(argv=[], config_files=[]))
        self.config.setUp()
        self.config.register_opts([
            cfg.StrOpt('user_domain_id', default=""),
            cfg.StrOpt('user_domain_name', default=""),
            cfg.StrOpt('username', default="username"),
            cfg.StrOpt('password', default="password"),
            cfg.StrOpt('auth_url', default="testdomain")
        ], "service_credentials")

    def test_get_trusted_client_domain_id(self):
        self.config.config(
            **{'group': "service_credentials",
               'user_domain_id': "uuid-domain"})

        client = keystone_client.get_trusted_client(
            self.config.conf, "testing")
        self.assertEqual(client.session.auth._user_domain_id, "uuid-domain")
        self.assertEqual(client.session.auth._user_domain_name, '')

    def test_get_trusted_client_domain_name(self):
        self.config.config(
            **{'group': "service_credentials",
               'user_domain_name': "testdomain"})

        client = keystone_client.get_trusted_client(
            self.config.conf, "testing")
        self.assertEqual(client.session.auth._user_domain_name, "testdomain")
        self.assertEqual(client.session.auth._user_domain_id, '')

    def test_get_trusted_client_domain(self):
        self.config.config(**{'group': "service_credentials",
                              'user_domain_name': "testdomain",
                              'user_domain_id': "uuid-gen",
                              })

        client = keystone_client.get_trusted_client(self.config.conf,
                                                    "testing")
        self.assertEqual(client.session.auth._user_domain_name, "testdomain")
        self.assertEqual(client.session.auth._user_domain_id, "uuid-gen")


class TestAlarmNotifier(tests_base.BaseTestCase):
    def setUp(self):
        super(TestAlarmNotifier, self).setUp()
        conf = service.prepare_service(argv=[], config_files=[])
        self.CONF = self.useFixture(fixture_config.Config(conf)).conf
        self.setup_messaging(self.CONF)
        self._msg_notifier = oslo_messaging.Notifier(
            self.transport, topics=['alarming'], driver='messaging',
            publisher_id='testpublisher')
        self.zaqar = FakeZaqarClient(self)
        self.useFixture(fixtures.MockPatch(
            'aodh.notifier.zaqar.ZaqarAlarmNotifier.get_zaqar_client',
            return_value=self.zaqar))
        self.service = notifier.AlarmNotifierService(0, self.CONF)
        self.addCleanup(self.service.terminate)

    def test_notify_alarm(self):
        data = {
            'actions': ['test://'],
            'alarm_id': 'foobar',
            'alarm_name': 'testalarm',
            'severity': 'critical',
            'previous': 'OK',
            'current': 'ALARM',
            'reason': 'Everything is on fire',
            'reason_data': {'fire': 'everywhere'}
        }
        self._msg_notifier.sample({}, 'alarm.update', data)
        time.sleep(1)
        notifications = self.service.notifiers['test'].obj.notifications
        self.assertEqual(1, len(notifications))
        self.assertEqual((urlparse.urlsplit(data['actions'][0]),
                          data['alarm_id'],
                          data['alarm_name'],
                          data['severity'],
                          data['previous'],
                          data['current'],
                          data['reason'],
                          data['reason_data']),
                         notifications[0])

    @mock.patch('aodh.notifier.LOG.debug')
    def test_notify_alarm_with_batch_listener(self, logger):
        data1 = {
            'actions': ['test://'],
            'alarm_id': 'foobar',
            'alarm_name': 'testalarm',
            'severity': 'critical',
            'previous': 'OK',
            'current': 'ALARM',
            'reason': 'Everything is on fire',
            'reason_data': {'fire': 'everywhere'}
        }
        data2 = {
            'actions': ['test://'],
            'alarm_id': 'foobar2',
            'alarm_name': 'testalarm2',
            'severity': 'low',
            'previous': 'ALARM',
            'current': 'OK',
            'reason': 'Everything is fine',
            'reason_data': {'fine': 'fine'}
        }
        self.service.terminate()
        self.CONF.set_override("batch_size", 2, 'notifier')
        # Init a new service with new configuration
        self.svc = notifier.AlarmNotifierService(0, self.CONF)
        self.addCleanup(self.svc.terminate)
        self._msg_notifier.sample({}, 'alarm.update', data1)
        self._msg_notifier.sample({}, 'alarm.update', data2)
        time.sleep(1)
        notifications = self.svc.notifiers['test'].obj.notifications
        self.assertEqual(2, len(notifications))
        self.assertEqual((urlparse.urlsplit(data1['actions'][0]),
                          data1['alarm_id'],
                          data1['alarm_name'],
                          data1['severity'],
                          data1['previous'],
                          data1['current'],
                          data1['reason'],
                          data1['reason_data']),
                         notifications[0])
        self.assertEqual((urlparse.urlsplit(data2['actions'][0]),
                          data2['alarm_id'],
                          data2['alarm_name'],
                          data2['severity'],
                          data2['previous'],
                          data2['current'],
                          data2['reason'],
                          data2['reason_data']),
                         notifications[1])
        self.assertEqual(mock.call('Received %s messages in batch.', 2),
                         logger.call_args_list[0])

    @staticmethod
    def _notification(action):
        notification = {}
        notification.update(NOTIFICATION)
        notification['actions'] = [action]
        return notification

    @mock.patch('aodh.notifier.rest.LOG')
    def test_notify_alarm_rest_action_ok(self, m_log):
        action = 'http://host/action'

        with mock.patch.object(requests.Session, 'post') as poster:
            self._msg_notifier.sample({},
                                      'alarm.update',
                                      self._notification(action))
            time.sleep(1)
            poster.assert_called_with(action, data=mock.ANY,
                                      headers=mock.ANY)
            args, kwargs = poster.call_args
            self.assertEqual(
                {
                    'x-openstack-request-id':
                    kwargs['headers']['x-openstack-request-id'],
                    'content-type': 'application/json'
                },
                kwargs['headers'])
            self.assertEqual(DATA_JSON, json.loads(kwargs['data']))
            self.assertEqual(2, len(m_log.info.call_args_list))
            expected = mock.call('Notifying alarm <%(id)s> gets response: '
                                 '%(status_code)s %(reason)s.',
                                 mock.ANY)
            self.assertEqual(expected, m_log.info.call_args_list[1])

    def test_notify_alarm_rest_action_with_ssl_client_cert(self):
        action = 'https://host/action'
        certificate = "/etc/ssl/cert/whatever.pem"

        self.CONF.set_override("rest_notifier_certificate_file", certificate)

        with mock.patch.object(requests.Session, 'post') as poster:
            self._msg_notifier.sample({},
                                      'alarm.update',
                                      self._notification(action))
            time.sleep(1)
            poster.assert_called_with(action, data=mock.ANY,
                                      headers=mock.ANY,
                                      cert=certificate, verify=True)
            args, kwargs = poster.call_args
            self.assertEqual(
                {
                    'x-openstack-request-id':
                    kwargs['headers']['x-openstack-request-id'],
                    'content-type': 'application/json'
                },
                kwargs['headers'])
            self.assertEqual(DATA_JSON, json.loads(kwargs['data']))

    def test_notify_alarm_rest_action_with_ssl_client_cert_and_key(self):
        action = 'https://host/action'
        certificate = "/etc/ssl/cert/whatever.pem"
        key = "/etc/ssl/cert/whatever.key"

        self.CONF.set_override("rest_notifier_certificate_file", certificate)
        self.CONF.set_override("rest_notifier_certificate_key", key)

        with mock.patch.object(requests.Session, 'post') as poster:
            self._msg_notifier.sample({},
                                      'alarm.update',
                                      self._notification(action))
            time.sleep(1)
            poster.assert_called_with(action, data=mock.ANY,
                                      headers=mock.ANY,
                                      cert=(certificate, key), verify=True)
            args, kwargs = poster.call_args
            self.assertEqual(
                {
                    'x-openstack-request-id':
                    kwargs['headers']['x-openstack-request-id'],
                    'content-type': 'application/json'},
                kwargs['headers'])
            self.assertEqual(DATA_JSON, json.loads(kwargs['data']))

    def test_notify_alarm_rest_action_with_ssl_verify_disable_by_cfg(self):
        action = 'https://host/action'

        self.CONF.set_override("rest_notifier_ssl_verify", False)

        with mock.patch.object(requests.Session, 'post') as poster:
            self._msg_notifier.sample({},
                                      'alarm.update',
                                      self._notification(action))
            time.sleep(1)
            poster.assert_called_with(action, data=mock.ANY,
                                      headers=mock.ANY,
                                      verify=False)
            args, kwargs = poster.call_args
            self.assertEqual(
                {
                    'x-openstack-request-id':
                    kwargs['headers']['x-openstack-request-id'],
                    'content-type': 'application/json'
                },
                kwargs['headers'])
            self.assertEqual(DATA_JSON, json.loads(kwargs['data']))

    def test_notify_alarm_rest_action_with_ssl_server_verify_enable(self):
        action = 'https://host/action'
        ca_bundle = "/path/to/custom_cert.pem"

        self.CONF.set_override("rest_notifier_ca_bundle_certificate_path",
                               ca_bundle)

        with mock.patch.object(requests.Session, 'post') as poster:
            self._msg_notifier.sample({},
                                      'alarm.update',
                                      self._notification(action))
            time.sleep(1)
            poster.assert_called_with(action, data=mock.ANY,
                                      headers=mock.ANY,
                                      verify=ca_bundle)
            args, kwargs = poster.call_args
            self.assertEqual(DATA_JSON, json.loads(kwargs['data']))

    def test_notify_alarm_rest_action_with_ssl_verify_disable(self):
        action = 'https://host/action?aodh-alarm-ssl-verify=0'

        with mock.patch.object(requests.Session, 'post') as poster:
            self._msg_notifier.sample({},
                                      'alarm.update',
                                      self._notification(action))
            time.sleep(1)
            poster.assert_called_with(action, data=mock.ANY,
                                      headers=mock.ANY,
                                      verify=False)
            args, kwargs = poster.call_args
            self.assertEqual(
                {
                    'x-openstack-request-id':
                    kwargs['headers']['x-openstack-request-id'],
                    'content-type': 'application/json'
                },
                kwargs['headers'])
            self.assertEqual(DATA_JSON, json.loads(kwargs['data']))

    def test_notify_alarm_rest_action_with_ssl_verify_enable_by_user(self):
        action = 'https://host/action?aodh-alarm-ssl-verify=1'

        self.CONF.set_override("rest_notifier_ssl_verify", False)

        with mock.patch.object(requests.Session, 'post') as poster:
            self._msg_notifier.sample({},
                                      'alarm.update',
                                      self._notification(action))
            time.sleep(1)
            poster.assert_called_with(action, data=mock.ANY,
                                      headers=mock.ANY,
                                      verify=True)
            args, kwargs = poster.call_args
            self.assertEqual(
                {
                    'x-openstack-request-id':
                    kwargs['headers']['x-openstack-request-id'],
                    'content-type': 'application/json'
                },
                kwargs['headers'])
            self.assertEqual(DATA_JSON, json.loads(kwargs['data']))

    @staticmethod
    def _fake_urlsplit(*args, **kwargs):
        raise Exception("Evil urlsplit!")

    def test_notify_alarm_invalid_url(self):
        with mock.patch('oslo_utils.netutils.urlsplit',
                        self._fake_urlsplit):
            LOG = mock.MagicMock()
            with mock.patch('aodh.notifier.LOG', LOG):
                self._msg_notifier.sample(
                    {}, 'alarm.update',
                    {
                        'actions': ['no-such-action-i-am-sure'],
                        'alarm_id': 'foobar',
                        'condition': {'threshold': 42},
                    })
                time.sleep(1)
                self.assertTrue(LOG.error.called)

    def test_notify_alarm_invalid_action(self):
        LOG = mock.MagicMock()
        with mock.patch('aodh.notifier.LOG', LOG):
            self._msg_notifier.sample(
                {}, 'alarm.update',
                {
                    'actions': ['no-such-action-i-am-sure://'],
                    'alarm_id': 'foobar',
                    'condition': {'threshold': 42},
                })
            time.sleep(1)
            self.assertTrue(LOG.error.called)

    def test_notify_alarm_trust_action(self):
        action = 'trust+http://trust-1234@host/action'
        url = 'http://host/action'

        client = mock.MagicMock()
        client.session.auth.get_access.return_value.auth_token = 'token_1234'

        self.useFixture(
            fixtures.MockPatch('aodh.keystone_client.get_trusted_client',
                               lambda *args: client))

        with mock.patch.object(requests.Session, 'post') as poster:
            self._msg_notifier.sample({}, 'alarm.update',
                                      self._notification(action))
            time.sleep(1)
            poster.assert_called_with(
                url, data=mock.ANY, headers=mock.ANY)
            args, kwargs = poster.call_args
            self.assertEqual(
                {
                    'X-Auth-Token': 'token_1234',
                    'x-openstack-request-id':
                    kwargs['headers']['x-openstack-request-id'],
                    'content-type': 'application/json'
                },
                kwargs['headers'])

            self.assertEqual(DATA_JSON, json.loads(kwargs['data']))

    def test_zaqar_notifier_action(self):
        with mock.patch.object(notifier.zaqar.ZaqarAlarmNotifier,
                               '_get_client_conf') as get_conf:
            action = ('zaqar://?topic=critical'
                      '&subscriber=http://example.com/data'
                      '&subscriber=mailto:foo@example.com&ttl=7200')
            self._msg_notifier.sample({}, 'alarm.update',
                                      self._notification(action))
            time.sleep(1)
            get_conf.assert_called()
            self.assertEqual(self.zaqar,
                             self.service.notifiers['zaqar'].obj._zclient)
            self.assertEqual(2, self.zaqar.subscriptions)
            self.assertEqual(1, self.zaqar.posts)

    def test_presigned_zaqar_notifier_action(self):
        action = ('zaqar://?'
                  'subscriber=http://example.com/data&ttl=7200'
                  '&signature=mysignature&expires=2016-06-29T01:49:56'
                  '&paths=/v2/queues/beijing/messages'
                  '&methods=GET,PATCH,POST,PUT&queue_name=foobar-critical'
                  '&project_id=my_project_id')
        self._msg_notifier.sample({}, 'alarm.update',
                                  self._notification(action))
        time.sleep(1)
        self.assertEqual(1, self.zaqar.subscriptions)
        self.assertEqual(1, self.zaqar.posts)

    def test_trust_zaqar_notifier_action(self):
        client = mock.MagicMock()
        client.session.auth.get_access.return_value.auth_token = 'token_1234'

        self.useFixture(
            fixtures.MockPatch('aodh.keystone_client.get_trusted_client',
                               lambda *args: client))

        action = 'trust+zaqar://trust-1234:delete@?queue_name=foobar-critical'
        self._msg_notifier.sample({}, 'alarm.update',
                                  self._notification(action))
        time.sleep(1)
        self.assertEqual(0, self.zaqar.subscriptions)
        self.assertEqual(1, self.zaqar.posts)


class FakeZaqarClient(object):

    def __init__(self, testcase):
        self.testcase = testcase
        self.subscriptions = 0
        self.posts = 0

    def queue(self, queue_name, **kwargs):
        self.testcase.assertEqual('foobar-critical', queue_name)
        self.testcase.assertEqual({}, kwargs)
        return FakeZaqarQueue(self)

    def subscription(self, queue_name, **kwargs):
        self.testcase.assertEqual('foobar-critical', queue_name)
        subscribers = ['http://example.com/data', 'mailto:foo@example.com']
        self.testcase.assertIn(kwargs['subscriber'], subscribers)
        self.testcase.assertEqual(7200, kwargs['ttl'])
        self.subscriptions += 1


class FakeZaqarQueue(object):

    def __init__(self, client):
        self.client = client
        self.testcase = client.testcase

    def post(self, message):
        expected_message = {'body': {'alarm_name': 'testalarm',
                                     'reason_data': {'test': 'test'},
                                     'current': 'ALARM',
                                     'alarm_id': 'foobar',
                                     'reason': 'what ?',
                                     'severity': 'critical',
                                     'previous': 'OK'}}
        self.testcase.assertEqual(expected_message, message)
        self.client.posts += 1
