#
#    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 datetime
from unittest import mock
import uuid

from oslo_serialization import jsonutils as json
from oslo_utils import timeutils

from heat.common import identifier
from heat.common import template_format
from heat.engine.clients.os import heat_plugin
from heat.engine.clients.os import swift as swift_plugin
from heat.engine import environment
from heat.engine import resource
from heat.engine.resources.openstack.heat import wait_condition_handle as h_wch
from heat.engine import scheduler
from heat.engine import stack as parser
from heat.engine import template as tmpl
from heat.objects import resource as resource_objects
from heat.tests import common
from heat.tests import utils

test_template_heat_waitcondition = '''
heat_template_version: 2013-05-23
resources:
    wait_condition:
        type: OS::Heat::WaitCondition
        properties:
            handle: {get_resource: wait_handle}
            timeout: 5
    wait_handle:
        type: OS::Heat::WaitConditionHandle
'''

test_template_heat_waitcondition_count = '''
heat_template_version: 2013-05-23
resources:
    wait_condition:
        type: OS::Heat::WaitCondition
        properties:
            handle: {get_resource: wait_handle}
            count: 3
            timeout: 5
    wait_handle:
        type: OS::Heat::WaitConditionHandle
'''

test_template_heat_waithandle_token = '''
heat_template_version: 2013-05-23
resources:
    wait_handle:
        type: OS::Heat::WaitConditionHandle
'''

test_template_heat_waithandle_heat = '''
heat_template_version: 2013-05-23
resources:
    wait_handle:
        type: OS::Heat::WaitConditionHandle
        properties:
            signal_transport: HEAT_SIGNAL
'''

test_template_heat_waithandle_swift = '''
heat_template_version: 2013-05-23
resources:
    wait_handle:
        type: OS::Heat::WaitConditionHandle
        properties:
            signal_transport: TEMP_URL_SIGNAL
'''

test_template_heat_waithandle_zaqar = '''
heat_template_version: 2013-05-23
resources:
    wait_handle:
        type: OS::Heat::WaitConditionHandle
        properties:
            signal_transport: ZAQAR_SIGNAL
'''

test_template_heat_waithandle_none = '''
heat_template_version: 2013-05-23
resources:
    wait_handle:
        type: OS::Heat::WaitConditionHandle
        properties:
            signal_transport: NO_SIGNAL
'''

test_template_update_waithandle = '''
heat_template_version: 2013-05-23
resources:
    update_wait_handle:
        type: OS::Heat::UpdateWaitConditionHandle
'''

test_template_waithandle_bad_type = '''
heat_template_version: 2013-05-23
resources:
    wait_condition:
        type: OS::Heat::WaitCondition
        properties:
            handle: {get_resource: wait_handle}
            timeout: 5
    wait_handle:
        type: OS::Heat::RandomString
'''

test_template_waithandle_bad_reference = '''
heat_template_version: pike
resources:
    wait_condition:
        type: OS::Heat::WaitCondition
        properties:
            handle: wait_handel
            timeout: 5
    wait_handle:
        type: OS::Heat::WaitConditionHandle
        properties:
            signal_transport: NO_SIGNAL
'''


class HeatWaitConditionTest(common.HeatTestCase):

    def setUp(self):
        super(HeatWaitConditionTest, self).setUp()
        self.tenant_id = 'test_tenant'

    def create_stack(self, stack_id=None,
                     template=test_template_heat_waitcondition_count,
                     params=None,
                     stub=True, stub_status=True):
        params = params or {}
        temp = template_format.parse(template)
        template = tmpl.Template(temp,
                                 env=environment.Environment(params))
        ctx = utils.dummy_context(tenant_id=self.tenant_id)
        stack = parser.Stack(ctx, 'test_stack', template,
                             disable_rollback=True)

        # Stub out the stack ID so we have a known value
        if stack_id is None:
            stack_id = str(uuid.uuid4())

        self.stack_id = stack_id
        with utils.UUIDStub(self.stack_id):
            stack.store()

        if stub:
            id = identifier.ResourceIdentifier('test_tenant', stack.name,
                                               stack.id, '', 'wait_handle')
            self.patchobject(h_wch.HeatWaitConditionHandle, 'identifier',
                             return_value=id)

        if stub_status:
            self.patchobject(h_wch.HeatWaitConditionHandle, 'get_status')

        return stack

    def test_post_complete_to_handle(self):
        self.stack = self.create_stack()
        mock_get_status = h_wch.HeatWaitConditionHandle.get_status
        mock_get_status.side_effect = ([['SUCCESS'],
                                        ['SUCCESS', 'SUCCESS'],
                                        ['SUCCESS', 'SUCCESS', 'SUCCESS']])

        self.stack.create()

        rsrc = self.stack['wait_condition']
        self.assertEqual((rsrc.CREATE, rsrc.COMPLETE),
                         rsrc.state)

        r = resource_objects.Resource.get_by_name_and_stack(
            self.stack.context, 'wait_handle', self.stack.id)
        self.assertEqual('wait_handle', r.name)
        self.assertEqual(3, mock_get_status.call_count)

    def test_post_failed_to_handle(self):
        self.stack = self.create_stack()
        mock_get_status = h_wch.HeatWaitConditionHandle.get_status
        mock_get_status.side_effect = ([['SUCCESS'],
                                        ['SUCCESS', 'SUCCESS'],
                                        ['SUCCESS', 'SUCCESS', 'FAILURE']])

        self.stack.create()

        rsrc = self.stack['wait_condition']
        self.assertEqual((rsrc.CREATE, rsrc.FAILED),
                         rsrc.state)
        reason = rsrc.status_reason
        self.assertTrue(reason.startswith('WaitConditionFailure:'))

        r = resource_objects.Resource.get_by_name_and_stack(
            self.stack.context, 'wait_handle', self.stack.id)
        self.assertEqual('wait_handle', r.name)
        self.assertEqual(3, mock_get_status.call_count)

    def _test_wait_handle_invalid(self, tmpl, handle_name):
        self.stack = self.create_stack(template=tmpl)
        self.stack.create()
        rsrc = self.stack['wait_condition']
        self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state)
        reason = rsrc.status_reason
        error_msg = ('ValueError: resources.wait_condition: '
                     '%s is not a valid wait condition handle.') % handle_name
        self.assertEqual(reason, error_msg)

    def test_wait_handle_bad_type(self):
        self._test_wait_handle_invalid(test_template_waithandle_bad_type,
                                       'wait_handle')

    def test_wait_handle_bad_reference(self):
        self._test_wait_handle_invalid(
            test_template_waithandle_bad_reference, 'wait_handel')

    def test_timeout(self):
        self.stack = self.create_stack()

        # Avoid the stack create exercising the timeout code at the same time
        self.stack.timeout_secs = mock.Mock(return_value=None)

        now = timeutils.utcnow()
        periods = [0, 0.001, 0.1, 4.1, 5.1]
        periods.extend(range(10, 100, 5))
        fake_clock = [now + datetime.timedelta(0, t) for t in periods]
        timeutils.set_time_override(fake_clock)
        self.addCleanup(timeutils.clear_time_override)

        h_wch.HeatWaitConditionHandle.get_status(
        ).MultipleTimes().AndReturn([])

        self.stack.create()

        rsrc = self.stack['wait_condition']

        self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state)
        reason = rsrc.status_reason
        self.assertTrue(reason.startswith('WaitConditionTimeout:'))

    def _create_heat_wc_and_handle(self):
        self.stack = self.create_stack(
            template=test_template_heat_waitcondition)
        mock_get_status = h_wch.HeatWaitConditionHandle.get_status
        mock_get_status.side_effect = ([['SUCCESS']])

        self.stack.create()

        rsrc = self.stack['wait_condition']
        self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)

        wc_att = rsrc.FnGetAtt('data')
        self.assertEqual(str({}), wc_att)

        handle = self.stack['wait_handle']
        self.assertEqual((handle.CREATE, handle.COMPLETE), handle.state)
        return (rsrc, handle, mock_get_status)

    def test_data(self):
        rsrc, handle, mock_get_status = self._create_heat_wc_and_handle()
        test_metadata = {'data': 'foo', 'reason': 'bar',
                         'status': 'SUCCESS', 'id': '123'}
        ret = handle.handle_signal(details=test_metadata)
        wc_att = rsrc.FnGetAtt('data')
        self.assertEqual('{"123": "foo"}', wc_att)
        self.assertEqual('status:SUCCESS reason:bar', ret)

        test_metadata = {'data': 'dog', 'reason': 'cat',
                         'status': 'SUCCESS', 'id': '456'}
        ret = handle.handle_signal(details=test_metadata)
        wc_att = rsrc.FnGetAtt('data')
        self.assertEqual(json.loads('{"123": "foo", "456": "dog"}'),
                         json.loads(wc_att))
        self.assertEqual('status:SUCCESS reason:cat', ret)
        mock_get_status.assert_called_once()

    def test_data_noid(self):
        rsrc, handle, mock_get_status = self._create_heat_wc_and_handle()
        test_metadata = {'data': 'foo', 'reason': 'bar',
                         'status': 'SUCCESS'}
        ret = handle.handle_signal(details=test_metadata)
        wc_att = rsrc.FnGetAtt('data')
        self.assertEqual('{"1": "foo"}', wc_att)
        self.assertEqual('status:SUCCESS reason:bar', ret)

        test_metadata = {'data': 'dog', 'reason': 'cat',
                         'status': 'SUCCESS'}
        ret = handle.handle_signal(details=test_metadata)
        wc_att = rsrc.FnGetAtt('data')
        self.assertEqual(json.loads('{"1": "foo", "2": "dog"}'),
                         json.loads(wc_att))
        self.assertEqual('status:SUCCESS reason:cat', ret)
        mock_get_status.assert_called_once()

    def test_data_nodata(self):
        rsrc, handle, mock_get_status = self._create_heat_wc_and_handle()
        ret = handle.handle_signal()
        expected = 'status:SUCCESS reason:Signal 1 received'
        self.assertEqual(expected, ret)
        wc_att = rsrc.FnGetAtt('data')
        self.assertEqual('{"1": null}', wc_att)

        handle.handle_signal()
        wc_att = rsrc.FnGetAtt('data')
        self.assertEqual(json.loads('{"1": null, "2": null}'),
                         json.loads(wc_att))
        mock_get_status.assert_called_once()

    def test_data_partial_complete(self):
        rsrc, handle, mock_get_status = self._create_heat_wc_and_handle()
        test_metadata = {'status': 'SUCCESS'}
        ret = handle.handle_signal(details=test_metadata)
        expected = 'status:SUCCESS reason:Signal 1 received'
        self.assertEqual(expected, ret)
        wc_att = rsrc.FnGetAtt('data')
        self.assertEqual('{"1": null}', wc_att)

        test_metadata = {'status': 'SUCCESS'}
        ret = handle.handle_signal(details=test_metadata)
        expected = 'status:SUCCESS reason:Signal 2 received'
        self.assertEqual(expected, ret)
        wc_att = rsrc.FnGetAtt('data')
        self.assertEqual(json.loads('{"1": null, "2": null}'),
                         json.loads(wc_att))
        mock_get_status.assert_called_once()

    def _create_heat_handle(self,
                            template=test_template_heat_waithandle_token):
        self.stack = self.create_stack(template=template, stub_status=False)

        self.stack.create()

        handle = self.stack['wait_handle']
        self.assertEqual((handle.CREATE, handle.COMPLETE), handle.state)
        self.assertIsNotNone(handle.password)
        self.assertEqual(handle.resource_id, handle.data().get('user_id'))
        return handle

    def test_get_status_none_complete(self):
        handle = self._create_heat_handle()

        ret = handle.handle_signal()
        expected = 'status:SUCCESS reason:Signal 1 received'
        self.assertEqual(expected, ret)
        self.assertEqual(['SUCCESS'], handle.get_status())
        md_expected = {'1': {'data': None, 'reason': 'Signal 1 received',
                       'status': 'SUCCESS'}}
        self.assertEqual(md_expected, handle.metadata_get())

    def test_get_status_partial_complete(self):
        handle = self._create_heat_handle()
        test_metadata = {'status': 'SUCCESS'}
        ret = handle.handle_signal(details=test_metadata)
        expected = 'status:SUCCESS reason:Signal 1 received'
        self.assertEqual(expected, ret)
        self.assertEqual(['SUCCESS'], handle.get_status())
        md_expected = {'1': {'data': None, 'reason': 'Signal 1 received',
                       'status': 'SUCCESS'}}
        self.assertEqual(md_expected, handle.metadata_get())

    def test_get_status_failure(self):
        handle = self._create_heat_handle()
        test_metadata = {'status': 'FAILURE'}
        ret = handle.handle_signal(details=test_metadata)
        expected = 'status:FAILURE reason:Signal 1 received'
        self.assertEqual(expected, ret)
        self.assertEqual(['FAILURE'], handle.get_status())
        md_expected = {'1': {'data': None, 'reason': 'Signal 1 received',
                       'status': 'FAILURE'}}
        self.assertEqual(md_expected, handle.metadata_get())

    def test_getatt_token(self):
        handle = self._create_heat_handle()
        self.assertEqual('adomainusertoken', handle.FnGetAtt('token'))

    def test_getatt_endpoint(self):
        self.patchobject(heat_plugin.HeatClientPlugin, 'get_heat_url',
                         return_value='foo/%s' % self.tenant_id)
        handle = self._create_heat_handle()
        expected = ('foo/aprojectid/stacks/test_stack/%s/resources/'
                    'wait_handle/signal'
                    % self.stack_id)
        self.assertEqual(expected, handle.FnGetAtt('endpoint'))

    def test_getatt_curl_cli(self):
        self.patchobject(heat_plugin.HeatClientPlugin, 'get_heat_url',
                         return_value='foo/%s' % self.tenant_id)
        self.patchobject(heat_plugin.HeatClientPlugin, 'get_insecure_option',
                         return_value=False)
        handle = self._create_heat_handle()
        expected = ("curl -i -X POST -H 'X-Auth-Token: adomainusertoken' "
                    "-H 'Content-Type: application/json' "
                    "-H 'Accept: application/json' "
                    "foo/aprojectid/stacks/test_stack/%s/resources/wait_handle"
                    "/signal" % self.stack_id)
        self.assertEqual(expected, handle.FnGetAtt('curl_cli'))

    def test_getatt_curl_cli_insecure_true(self):
        self.patchobject(heat_plugin.HeatClientPlugin, 'get_heat_url',
                         return_value='foo/%s' % self.tenant_id)
        self.patchobject(heat_plugin.HeatClientPlugin, 'get_insecure_option',
                         return_value=True)
        handle = self._create_heat_handle()
        expected = (
            "curl --insecure -i -X POST -H 'X-Auth-Token: adomainusertoken' "
            "-H 'Content-Type: application/json' "
            "-H 'Accept: application/json' "
            "foo/aprojectid/stacks/test_stack/%s/resources/wait_handle"
            "/signal" % self.stack_id)
        self.assertEqual(expected, handle.FnGetAtt('curl_cli'))

    def test_getatt_signal_heat(self):
        handle = self._create_heat_handle(
            template=test_template_heat_waithandle_heat)
        self.assertIsNone(handle.FnGetAtt('token'))
        self.assertIsNone(handle.FnGetAtt('endpoint'))
        self.assertIsNone(handle.FnGetAtt('curl_cli'))
        signal = json.loads(handle.FnGetAtt('signal'))
        self.assertIn('alarm_url', signal)
        self.assertIn('username', signal)
        self.assertIn('password', signal)
        self.assertIn('auth_url', signal)
        self.assertIn('project_id', signal)
        self.assertIn('domain_id', signal)

    def test_getatt_signal_swift(self):
        class mock_swift(object):
            @staticmethod
            def put_container(container, **kwargs):
                pass

            @staticmethod
            def put_object(container, object, contents, **kwargs):
                pass

        mock_tempurl = self.patchobject(swift_plugin.SwiftClientPlugin,
                                        'get_temp_url',
                                        return_value='foo')
        self.patchobject(swift_plugin.SwiftClientPlugin, 'client',
                         return_value=mock_swift)

        handle = self._create_heat_handle(
            template=test_template_heat_waithandle_swift)
        self.assertIsNone(handle.FnGetAtt('token'))
        self.assertIsNone(handle.FnGetAtt('endpoint'))
        self.assertIsNone(handle.FnGetAtt('curl_cli'))
        signal = json.loads(handle.FnGetAtt('signal'))
        self.assertIn('alarm_url', signal)
        mock_tempurl.assert_called_once()

    @mock.patch('zaqarclient.queues.v2.queues.Queue.signed_url')
    def test_getatt_signal_zaqar(self, mock_signed_url):
        handle = self._create_heat_handle(
            template=test_template_heat_waithandle_zaqar)
        self.assertIsNone(handle.FnGetAtt('token'))
        self.assertIsNone(handle.FnGetAtt('endpoint'))
        self.assertIsNone(handle.FnGetAtt('curl_cli'))
        signal = json.loads(handle.FnGetAtt('signal'))
        self.assertIn('queue_id', signal)
        self.assertIn('username', signal)
        self.assertIn('password', signal)
        self.assertIn('auth_url', signal)
        self.assertIn('project_id', signal)
        self.assertIn('domain_id', signal)

    def test_getatt_signal_none(self):
        handle = self._create_heat_handle(
            template=test_template_heat_waithandle_none)
        self.assertIsNone(handle.FnGetAtt('token'))
        self.assertIsNone(handle.FnGetAtt('endpoint'))
        self.assertIsNone(handle.FnGetAtt('curl_cli'))
        self.assertEqual('{}', handle.FnGetAtt('signal'))

    def test_create_update_updatehandle(self):
        self.stack = self.create_stack(
            template=test_template_update_waithandle, stub_status=False)
        self.stack.create()

        handle = self.stack['update_wait_handle']
        self.assertEqual((handle.CREATE, handle.COMPLETE), handle.state)

        self.assertRaises(
            resource.UpdateReplace,
            scheduler.TaskRunner(handle.update, handle.t))
