# 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 tempfile
import uuid

import testtools

from openstack import exceptions
from openstack.orchestration.util import template_format
from openstack.orchestration.v1 import stack
from openstack.tests.unit import base
from openstack.tests.unit.cloud import fakes

ORCHESTRATION_ENDPOINT = (
    f'https://orchestration.example.com/v1/{fakes.PROJECT_ID}'
)
FAKE_TEMPLATE = """heat_template_version: 2014-10-16

parameters:
  length:
    type: number
    default: 10

resources:
  my_rand:
    type: OS::Heat::RandomString
    properties:
      length: {get_param: length}
outputs:
  rand:
    value:
      get_attr: [my_rand, value]
"""
FAKE_TEMPLATE_CONTENT = template_format.parse(FAKE_TEMPLATE)


def make_fake_stack(id, name, description=None, status='CREATE_COMPLETE'):
    return {
        'creation_time': '2017-03-23T23:57:12Z',
        'deletion_time': '2017-03-23T23:57:12Z',
        'description': description,
        'id': id,
        'links': [],
        'parent': None,
        'stack_name': name,
        'stack_owner': None,
        'stack_status': status,
        'stack_user_project_id': fakes.PROJECT_ID,
        'tags': None,
        'updated_time': '2017-03-23T23:57:12Z',
    }


def make_fake_stack_event(
    id, name, status='CREATE_COMPLETED', resource_name='id'
):
    event_id = uuid.uuid4().hex
    self_url = "{endpoint}/stacks/{name}/{id}/resources/{name}/events/{event}"
    resource_url = "{endpoint}/stacks/{name}/{id}/resources/{name}"
    return {
        "resource_name": id if resource_name == 'id' else name,
        "event_time": "2017-03-26T19:38:18",
        "links": [
            {
                "href": self_url.format(
                    endpoint=ORCHESTRATION_ENDPOINT,
                    name=name,
                    id=id,
                    event=event_id,
                ),
                "rel": "self",
            },
            {
                "href": resource_url.format(
                    endpoint=ORCHESTRATION_ENDPOINT, name=name, id=id
                ),
                "rel": "resource",
            },
            {
                "href": f"{ORCHESTRATION_ENDPOINT}/stacks/{name}/{id}",
                "rel": "stack",
            },
        ],
        "logical_resource_id": name,
        "resource_status": status,
        "resource_status_reason": "",
        "physical_resource_id": id,
        "id": event_id,
    }


class TestStack(base.TestCase):
    def setUp(self):
        super().setUp()
        self.stack_id = self.getUniqueString('id')
        self.stack_name = self.getUniqueString('name')
        self.stack_tag = self.getUniqueString('tag')
        self.stack = make_fake_stack(self.stack_id, self.stack_name)

    def _compare_stacks(self, exp, real):
        self.assertDictEqual(
            stack.Stack(**exp).to_dict(computed=False),
            real.to_dict(computed=False),
        )

    def test_list_stacks(self):
        fake_stacks = [
            self.stack,
            make_fake_stack(
                self.getUniqueString('id'), self.getUniqueString('name')
            ),
        ]
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks',
                    json={"stacks": fake_stacks},
                ),
            ]
        )
        stacks = self.cloud.list_stacks()
        [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)]

        self.assert_calls()

    def test_list_stacks_filters(self):
        fake_stacks = [
            self.stack,
            make_fake_stack(
                self.getUniqueString('id'), self.getUniqueString('name')
            ),
        ]
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=self.get_mock_url(
                        'orchestration',
                        'public',
                        append=['stacks'],
                        qs_elements=['name=a', 'status=b'],
                    ),
                    json={"stacks": fake_stacks},
                ),
            ]
        )
        stacks = self.cloud.list_stacks(name='a', status='b')
        [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)]

        self.assert_calls()

    def test_list_stacks_exception(self):
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks',
                    status_code=404,
                )
            ]
        )
        with testtools.ExpectedException(exceptions.NotFoundException):
            self.cloud.list_stacks()
        self.assert_calls()

    def test_search_stacks(self):
        fake_stacks = [
            self.stack,
            make_fake_stack(
                self.getUniqueString('id'), self.getUniqueString('name')
            ),
        ]
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks',
                    json={"stacks": fake_stacks},
                ),
            ]
        )
        stacks = self.cloud.search_stacks()
        [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)]
        self.assert_calls()

    def test_search_stacks_filters(self):
        fake_stacks = [
            self.stack,
            make_fake_stack(
                self.getUniqueString('id'),
                self.getUniqueString('name'),
                status='CREATE_FAILED',
            ),
        ]
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks',
                    json={"stacks": fake_stacks},
                ),
            ]
        )
        filters = {'status': 'FAILED'}
        stacks = self.cloud.search_stacks(filters=filters)
        [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)]
        self.assert_calls()

    def test_search_stacks_exception(self):
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks',
                    status_code=404,
                )
            ]
        )
        with testtools.ExpectedException(exceptions.NotFoundException):
            self.cloud.search_stacks()

    def test_delete_stack(self):
        resolve = 'resolve_outputs=False'
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}?{resolve}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    json={"stack": self.stack},
                ),
                dict(
                    method='DELETE',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}',
                ),
            ]
        )
        self.assertTrue(self.cloud.delete_stack(self.stack_name))
        self.assert_calls()

    def test_delete_stack_not_found(self):
        resolve = 'resolve_outputs=False'
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/stack_name?{resolve}',
                    status_code=404,
                ),
            ]
        )
        self.assertFalse(self.cloud.delete_stack('stack_name'))
        self.assert_calls()

    def test_delete_stack_exception(self):
        resolve = 'resolve_outputs=False'
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    json={"stack": self.stack},
                ),
                dict(
                    method='DELETE',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}',
                    status_code=400,
                    reason="ouch",
                ),
            ]
        )
        with testtools.ExpectedException(exceptions.BadRequestException):
            self.cloud.delete_stack(self.stack_id)
        self.assert_calls()

    def test_delete_stack_by_name_wait(self):
        marker_event = make_fake_stack_event(
            self.stack_id,
            self.stack_name,
            status='CREATE_COMPLETE',
            resource_name='name',
        )
        marker_qs = 'marker={e_id}&sort_dir=asc'.format(
            e_id=marker_event['id']
        )
        resolve = 'resolve_outputs=False'
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}?{resolve}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    json={"stack": self.stack},
                ),
                dict(
                    method='GET',
                    uri='{endpoint}/stacks/{name}/events?{qs}'.format(
                        endpoint=ORCHESTRATION_ENDPOINT,
                        name=self.stack_name,
                        qs='limit=1&sort_dir=desc',
                    ),
                    complete_qs=True,
                    json={"events": [marker_event]},
                ),
                dict(
                    method='DELETE',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}',
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/events?{marker_qs}',
                    complete_qs=True,
                    json={
                        "events": [
                            make_fake_stack_event(
                                self.stack_id,
                                self.stack_name,
                                status='DELETE_COMPLETE',
                                resource_name='name',
                            ),
                        ]
                    },
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}?{resolve}',
                    status_code=404,
                ),
            ]
        )

        self.assertTrue(self.cloud.delete_stack(self.stack_name, wait=True))
        self.assert_calls()

    def test_delete_stack_by_id_wait(self):
        marker_event = make_fake_stack_event(
            self.stack_id,
            self.stack_name,
            status='CREATE_COMPLETE',
            resource_name='name',
        )
        marker_qs = 'marker={e_id}&sort_dir=asc'.format(
            e_id=marker_event['id']
        )
        resolve = 'resolve_outputs=False'
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    json={"stack": self.stack},
                ),
                dict(
                    method='GET',
                    uri='{endpoint}/stacks/{id}/events?{qs}'.format(
                        endpoint=ORCHESTRATION_ENDPOINT,
                        id=self.stack_id,
                        qs='limit=1&sort_dir=desc',
                    ),
                    complete_qs=True,
                    json={"events": [marker_event]},
                ),
                dict(
                    method='DELETE',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}',
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}/events?{marker_qs}',
                    complete_qs=True,
                    json={
                        "events": [
                            make_fake_stack_event(
                                self.stack_id,
                                self.stack_name,
                                status='DELETE_COMPLETE',
                            ),
                        ]
                    },
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}',
                    status_code=404,
                ),
            ]
        )

        self.assertTrue(self.cloud.delete_stack(self.stack_id, wait=True))
        self.assert_calls()

    def test_delete_stack_wait_failed(self):
        failed_stack = self.stack.copy()
        failed_stack['stack_status'] = 'DELETE_FAILED'
        marker_event = make_fake_stack_event(
            self.stack_id, self.stack_name, status='CREATE_COMPLETE'
        )
        marker_qs = 'marker={e_id}&sort_dir=asc'.format(
            e_id=marker_event['id']
        )
        resolve = 'resolve_outputs=False'
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    json={"stack": self.stack},
                ),
                dict(
                    method='GET',
                    uri='{endpoint}/stacks/{id}/events?{qs}'.format(
                        endpoint=ORCHESTRATION_ENDPOINT,
                        id=self.stack_id,
                        qs='limit=1&sort_dir=desc',
                    ),
                    complete_qs=True,
                    json={"events": [marker_event]},
                ),
                dict(
                    method='DELETE',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}',
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}/events?{marker_qs}',
                    complete_qs=True,
                    json={
                        "events": [
                            make_fake_stack_event(
                                self.stack_id,
                                self.stack_name,
                                status='DELETE_COMPLETE',
                            ),
                        ]
                    },
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?resolve_outputs=False',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}',
                    json={"stack": failed_stack},
                ),
            ]
        )

        with testtools.ExpectedException(exceptions.SDKException):
            self.cloud.delete_stack(self.stack_id, wait=True)

        self.assert_calls()

    def test_create_stack(self):
        test_template = tempfile.NamedTemporaryFile(delete=False)
        test_template.write(FAKE_TEMPLATE.encode('utf-8'))
        test_template.close()
        self.register_uris(
            [
                dict(
                    method='POST',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks',
                    json={"stack": self.stack},
                    validate=dict(
                        json={
                            'disable_rollback': False,
                            'parameters': {},
                            'stack_name': self.stack_name,
                            'tags': self.stack_tag,
                            'template': FAKE_TEMPLATE_CONTENT,
                            'timeout_mins': 60,
                        }
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}'
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}',
                    json={"stack": self.stack},
                ),
            ]
        )

        self.cloud.create_stack(
            self.stack_name,
            tags=self.stack_tag,
            template_file=test_template.name,
        )

        self.assert_calls()

    def test_create_stack_wait(self):
        test_template = tempfile.NamedTemporaryFile(delete=False)
        test_template.write(FAKE_TEMPLATE.encode('utf-8'))
        test_template.close()

        self.register_uris(
            [
                dict(
                    method='POST',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks',
                    json={"stack": self.stack},
                    validate=dict(
                        json={
                            'disable_rollback': False,
                            'parameters': {},
                            'stack_name': self.stack_name,
                            'tags': self.stack_tag,
                            'template': FAKE_TEMPLATE_CONTENT,
                            'timeout_mins': 60,
                        }
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/events?sort_dir=asc',
                    json={
                        "events": [
                            make_fake_stack_event(
                                self.stack_id,
                                self.stack_name,
                                status='CREATE_COMPLETE',
                                resource_name='name',
                            ),
                        ]
                    },
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}'
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}',
                    json={"stack": self.stack},
                ),
            ]
        )
        self.cloud.create_stack(
            self.stack_name,
            tags=self.stack_tag,
            template_file=test_template.name,
            wait=True,
        )

        self.assert_calls()

    def test_update_stack(self):
        test_template = tempfile.NamedTemporaryFile(delete=False)
        test_template.write(FAKE_TEMPLATE.encode('utf-8'))
        test_template.close()

        self.register_uris(
            [
                dict(
                    method='PUT',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}',
                    validate=dict(
                        json={
                            'disable_rollback': False,
                            'parameters': {},
                            'tags': self.stack_tag,
                            'template': FAKE_TEMPLATE_CONTENT,
                            'timeout_mins': 60,
                        }
                    ),
                    json={},
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}'
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}',
                    json={"stack": self.stack},
                ),
            ]
        )
        self.cloud.update_stack(
            self.stack_name,
            tags=self.stack_tag,
            template_file=test_template.name,
        )

        self.assert_calls()

    def test_update_stack_wait(self):
        marker_event = make_fake_stack_event(
            self.stack_id,
            self.stack_name,
            status='CREATE_COMPLETE',
            resource_name='name',
        )
        marker_qs = 'marker={e_id}&sort_dir=asc'.format(
            e_id=marker_event['id']
        )
        test_template = tempfile.NamedTemporaryFile(delete=False)
        test_template.write(FAKE_TEMPLATE.encode('utf-8'))
        test_template.close()

        self.register_uris(
            [
                dict(
                    method='GET',
                    uri='{endpoint}/stacks/{name}/events?{qs}'.format(
                        endpoint=ORCHESTRATION_ENDPOINT,
                        name=self.stack_name,
                        qs='limit=1&sort_dir=desc',
                    ),
                    json={"events": [marker_event]},
                ),
                dict(
                    method='PUT',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}',
                    validate=dict(
                        json={
                            'disable_rollback': False,
                            'parameters': {},
                            'tags': self.stack_tag,
                            'template': FAKE_TEMPLATE_CONTENT,
                            'timeout_mins': 60,
                        }
                    ),
                    json={},
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/events?{marker_qs}',
                    json={
                        "events": [
                            make_fake_stack_event(
                                self.stack_id,
                                self.stack_name,
                                status='UPDATE_COMPLETE',
                                resource_name='name',
                            ),
                        ]
                    },
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}'
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}',
                    json={"stack": self.stack},
                ),
            ]
        )
        self.cloud.update_stack(
            self.stack_name,
            tags=self.stack_tag,
            template_file=test_template.name,
            wait=True,
        )

        self.assert_calls()

    def test_get_stack(self):
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}'
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}',
                    json={"stack": self.stack},
                ),
            ]
        )

        res = self.cloud.get_stack(self.stack_name)
        self.assertIsNotNone(res)
        self.assertEqual(self.stack['stack_name'], res['name'])
        self.assertEqual(self.stack['stack_status'], res['stack_status'])
        self.assertEqual('CREATE_COMPLETE', res['status'])

        self.assert_calls()

    def test_get_stack_in_progress(self):
        in_progress = self.stack.copy()
        in_progress['stack_status'] = 'CREATE_IN_PROGRESS'
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}',
                    status_code=302,
                    headers=dict(
                        location=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}'
                    ),
                ),
                dict(
                    method='GET',
                    uri=f'{ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}',
                    json={"stack": in_progress},
                ),
            ]
        )

        res = self.cloud.get_stack(self.stack_name)
        self.assertIsNotNone(res)
        self.assertEqual(in_progress['stack_name'], res.name)
        self.assertEqual(in_progress['stack_status'], res['stack_status'])
        self.assertEqual('CREATE_IN_PROGRESS', res['status'])

        self.assert_calls()
