###################################################################################################################### 
#  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.                                           # 
#                                                                                                                    # 
#  Licensed under the Apache License Version 2.0 (the "License"). You may not use this file except in compliance     # 
#  with the License. A copy of the License is located at                                                             # 
#                                                                                                                    # 
#      http://www.apache.org/licenses/                                                                               # 
#                                                                                                                    # 
#  or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES # 
#  OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions    # 
#  and limitations under the License.                                                                                # 
######################################################################################################################
import inspect
import time
import unittest
from types import FunctionType

import actions.ec2_resize_instance_action as r
import handlers.ec2_tag_event_handler
import testing.tags
from testing.console_logger import ConsoleLogger
from testing.ec2 import Ec2
from testing.stack import Stack
from tests.action_tests import get_resource_stack, get_task_runner, region, tasklist_tagname, template_path

TESTED_ACTION = "Ec2ResizeInstance"
TEST_RESOURCES_TEMPLATE = "test_resources.template"

KEEP_AND_USE_EXISTING_ACTION_STACK = False
KEEP_AND_USE_EXISTING_RESOURCES_STACK = False

TEST_INSTANCE_TYPES = ["t2.micro", "t2.small", "t2.medium", "t2.large"]


class TestAction(unittest.TestCase):
    logger = None
    resource_stack = None
    task_runner = None
    ec2 = None
    instance_id = None
    original_tags = None

    def __init__(self, method_name):
        unittest.TestCase.__init__(self, method_name)

    @classmethod
    def get_methods(cls):
        return [x for x, y in list(cls.__dict__.items()) if type(y) == FunctionType and x.startswith("test_")]

    @classmethod
    def setUpClass(cls):

        cls.logger = ConsoleLogger()

        cls.resource_stack = get_resource_stack(TESTED_ACTION,
                                                create_resource_stack_func=cls.create_resource_stack,
                                                use_existing=KEEP_AND_USE_EXISTING_RESOURCES_STACK,
                                                region_name=region())

        assert (cls.resource_stack is not None)

        cls.ec2 = Ec2(region())
        cls.instance_id = cls.resource_stack.stack_outputs["InstanceId"]
        if KEEP_AND_USE_EXISTING_ACTION_STACK:
            cls.ec2.start_instance(cls.instance_id)

        testing.tags.set_ec2_tag_to_delete(ec2_client=cls.ec2, resource_ids=[cls.instance_id])

        cls.task_runner = get_task_runner(TESTED_ACTION, KEEP_AND_USE_EXISTING_ACTION_STACK)

        tags = cls.ec2.get_instance_tags(cls.instance_id)
        cls.original_tags = {t: tags[t] for t in tags if not t.startswith("aws:")}

    @classmethod
    def create_resource_stack(cls, resource_stack_name):
        try:
            cls.logger.test("Creating test resources stack {}", resource_stack_name)
            ami = Ec2(region()).latest_aws_linux_image["ImageId"]
            resource_stack = Stack(resource_stack_name, region=region())
            resource_stack.create_stack(template_file=template_path(__file__, TEST_RESOURCES_TEMPLATE),
                                        timeout=1200,
                                        iam_capability=True,
                                        params={
                                            "InstanceAmi": ami,
                                            "InstanceType": TEST_INSTANCE_TYPES[0]
                                        })
            return resource_stack
        except Exception as ex:
            cls.logger.test("Error creating stack {}, {}", resource_stack_name, ex)
            return None

    @classmethod
    def restore_tags(cls):
        cls.ec2.restore_instance_tags(cls.instance_id, cls.original_tags)

    def reset_instance_type(self, start_type):
        org_size = self.ec2.get_instance(instance_id=self.instance_id)["InstanceType"]
        if org_size != start_type:
            self.ec2.stop_instance(self.instance_id)
            self.ec2.resize_instance(self.instance_id, start_type)

    def do_test_resize(self,
                       test_method,
                       resize_mode,
                       start_type,
                       expected_type,
                       resized_types=None,
                       scaling_range=None,
                       unavailable_types=None,
                       tags=None,
                       assumed_type=None,
                       try_next_in_range=None,
                       running=True):

        if not running:
            self.ec2.stop_instance(self.instance_id)

        self.reset_instance_type(start_type)

        if running:
            time.sleep(10)
            self.ec2.start_instance(self.instance_id)

        org_size = self.ec2.get_instance(instance_id=self.instance_id)["InstanceType"]

        try:

            instance_tags = {tasklist_tagname(TESTED_ACTION): test_method}
            if tags:
                instance_tags.update(tags)

            self.ec2.create_tags(self.instance_id, tags=instance_tags)

            parameters = {
                r.PARAM_RESIZED_INSTANCE_TAGS: testing.tags.common_placeholder_tags(placeholders=[
                    r.TAG_PLACEHOLDER_NEW_INSTANCE_TYPE,
                    r.TAG_PLACEHOLDER_ORG_INSTANCE_TYPE]),

                r.PARAM_RESIZE_MODE: resize_mode
            }

            if resize_mode == r.RESIZE_BY_SPECIFIED_TYPE:
                parameters[r.PARAM_INSTANCE_TYPES] = resized_types
            else:
                parameters[r.PARAM_SCALING_RANGE] = scaling_range
                parameters[r.PARAM_TAGFILTER_SCALE_UP] = "scaling=up"
                parameters[r.PARAM_TAGFILTER_SCALE_DOWN] = "scaling=down"
                if assumed_type is not None:
                    parameters[r.PARAM_ASSUMED_TYPE] = assumed_type
                parameters[r.PARAM_TRY_NEXT_IN_RANGE] = try_next_in_range

            if unavailable_types is not None:
                parameters[r.PARAM_TEST_UNAVAILABLE_TYPES] = unavailable_types if isinstance(unavailable_types, list) else [
                    unavailable_types]

            events = {
                handlers.ec2_tag_event_handler.EC2_TAG_EVENT_SOURCE: {
                    handlers.TAG_CHANGE_EVENT: [
                        handlers.ec2_tag_event_handler.EC2_CHANGED_INSTANCE_TAGS_EVENT
                    ]
                }
            }

            self.logger.test("Running task")
            self.task_runner.run(parameters,
                                 task_name=test_method,
                                 complete_check_polling_interval=15,
                                 events=events)

            self.assertTrue(self.task_runner.success(expected_executed_tasks=1), "Task executed successfully")
            self.logger.test("[X] Task completed")

            # test instance type here
            instance = self.ec2.get_instance(self.instance_id)
            new_type = instance["InstanceType"]
            self.assertEqual(expected_type, new_type, "Expected instance type")

            self.assertTrue(self.ec2.get_instance_status(self.instance_id) == "running" if running else "stopped", "Instance state")
            self.assertEqual(expected_type, new_type, "Expected instance state")

            instance_tags = self.ec2.get_instance_tags(self.instance_id)
            if not self.task_runner.executed_tasks[0].ActionResult.get("not-resized", False):
                self.assertTrue(testing.tags.verify_placeholder_tags(instance_tags,
                                                                     action_placeholders={
                                                                         r.TAG_PLACEHOLDER_NEW_INSTANCE_TYPE: new_type,
                                                                         r.TAG_PLACEHOLDER_ORG_INSTANCE_TYPE: org_size
                                                                     }),
                                "All placeholder tags set on resized instance")
                self.logger.test("[X] Instance placeholder tags created")

            if resize_mode == r.RESIZE_BY_STEP:
                self.assertFalse("scaling" in instance_tags, 'Scaling tags removed')
                self.logger.test("[X] Scaling filter tags removed")

        finally:
            self.restore_tags()

    def test_resize_running(self):
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_SPECIFIED_TYPE,
                            start_type=TEST_INSTANCE_TYPES[0],
                            resized_types=[TEST_INSTANCE_TYPES[1]],
                            expected_type=TEST_INSTANCE_TYPES[1])

    def test_resize_stopped(self):

        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_SPECIFIED_TYPE,
                            start_type=TEST_INSTANCE_TYPES[0],
                            resized_types=[TEST_INSTANCE_TYPES[1]],
                            expected_type=TEST_INSTANCE_TYPES[1],
                            running=False)

    def test_no_resize(self):
        current_size = self.ec2.get_instance(self.instance_id)["InstanceType"]
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_SPECIFIED_TYPE,
                            start_type=current_size,
                            resized_types=[current_size],
                            expected_type=current_size)

    def test_alternative_type(self):

        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_SPECIFIED_TYPE,
                            start_type=TEST_INSTANCE_TYPES[0],
                            resized_types=TEST_INSTANCE_TYPES[1:],
                            unavailable_types=TEST_INSTANCE_TYPES[1:3],
                            expected_type=TEST_INSTANCE_TYPES[3])

    def test_no_alternative_avail_keep_org(self):

        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_SPECIFIED_TYPE,
                            start_type=TEST_INSTANCE_TYPES[0],
                            resized_types=TEST_INSTANCE_TYPES[1:],
                            unavailable_types=TEST_INSTANCE_TYPES[1:],
                            expected_type=TEST_INSTANCE_TYPES[0])

    def test_step_up(self):
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "up"},
                            start_type=TEST_INSTANCE_TYPES[0],
                            scaling_range=TEST_INSTANCE_TYPES,
                            expected_type=TEST_INSTANCE_TYPES[1])

    def test_step_up_already_at_largest(self):
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "up"},
                            start_type=TEST_INSTANCE_TYPES[-1],
                            scaling_range=TEST_INSTANCE_TYPES,
                            expected_type=TEST_INSTANCE_TYPES[-1])

    def test_step_up_alternative_type(self):

        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "up"},
                            start_type=TEST_INSTANCE_TYPES[0],
                            try_next_in_range=True,
                            scaling_range=TEST_INSTANCE_TYPES,
                            unavailable_types=TEST_INSTANCE_TYPES[1:2],
                            expected_type=TEST_INSTANCE_TYPES[2])

    def test_step_up_no_alternative_avail(self):

        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "up"},
                            start_type=TEST_INSTANCE_TYPES[0],
                            try_next_in_range=True,
                            scaling_range=TEST_INSTANCE_TYPES,
                            unavailable_types=TEST_INSTANCE_TYPES[1:],
                            expected_type=TEST_INSTANCE_TYPES[0])

    def test_step_up_no_next(self):
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "up"},
                            start_type=TEST_INSTANCE_TYPES[0],
                            try_next_in_range=False,
                            scaling_range=TEST_INSTANCE_TYPES,
                            unavailable_types=TEST_INSTANCE_TYPES[1:2],
                            expected_type=TEST_INSTANCE_TYPES[0])

    def test_step_up_assumed(self):
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "up"},
                            start_type="t2.nano",
                            assumed_type=TEST_INSTANCE_TYPES[0],
                            scaling_range=TEST_INSTANCE_TYPES,
                            expected_type=TEST_INSTANCE_TYPES[1])

    def test_up_not_assumed(self):
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "up"},
                            start_type="t2.nano",
                            scaling_range=TEST_INSTANCE_TYPES,
                            expected_type="t2.nano")

    def test_step_down(self):
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "down"},
                            start_type=TEST_INSTANCE_TYPES[1],
                            scaling_range=TEST_INSTANCE_TYPES,
                            expected_type=TEST_INSTANCE_TYPES[0])

    def test_step_down_already_at_smallest(self):
        smallest_size = TEST_INSTANCE_TYPES[0]
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "down"},
                            start_type=smallest_size,
                            scaling_range=TEST_INSTANCE_TYPES,
                            expected_type=smallest_size)

    def test_step_down_alternative_type(self):
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "down"},
                            start_type=TEST_INSTANCE_TYPES[2],
                            try_next_in_range=True,
                            scaling_range=TEST_INSTANCE_TYPES,
                            unavailable_types=TEST_INSTANCE_TYPES[1:2],
                            expected_type=TEST_INSTANCE_TYPES[0])

    def test_step_down_no_next(self):
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "down"},
                            start_type=TEST_INSTANCE_TYPES[2],
                            try_next_in_range=False,
                            scaling_range=TEST_INSTANCE_TYPES,
                            unavailable_types=TEST_INSTANCE_TYPES[1:2],
                            expected_type=TEST_INSTANCE_TYPES[2])

    def test_step_down_no_available(self):
        self.do_test_resize(test_method=inspect.stack()[0][3],
                            resize_mode=r.RESIZE_BY_STEP,
                            tags={"scaling": "down"},
                            start_type=TEST_INSTANCE_TYPES[2],
                            try_next_in_range=False,
                            scaling_range=TEST_INSTANCE_TYPES,
                            unavailable_types=TEST_INSTANCE_TYPES[0:2],
                            expected_type=TEST_INSTANCE_TYPES[2])

    @classmethod
    def tearDownClass(cls):

        if cls.resource_stack is not None and not KEEP_AND_USE_EXISTING_RESOURCES_STACK:
            cls.resource_stack.delete_stack()

        if cls.task_runner is not None:
            cls.task_runner.cleanup(KEEP_AND_USE_EXISTING_ACTION_STACK)

    def setUp(self):
        pass

    def tearDown(self):
        pass


if __name__ == '__main__':
    unittest.main()