from unittest import TestCase
from mock import patch, Mock
from samtranslator.model.sam_resources import SamFunction
from samtranslator.model.lambda_ import LambdaAlias, LambdaVersion, LambdaFunction
from samtranslator.model.exceptions import InvalidResourceException
from samtranslator.model.update_policy import UpdatePolicy
from samtranslator.model.preferences.deployment_preference import DeploymentPreference


class TestVersionsAndAliases(TestCase):


    def setUp(self):

        self.intrinsics_resolver_mock = Mock()
        self.intrinsics_resolver_mock.resolve = Mock()

        self.code_uri = "s3://bucket/key?versionId=version"
        self.func_dict = {
            "Type": "AWS::Serverless::Function",
            "Properties": {
                "CodeUri": self.code_uri,
                "Runtime": "nodejs4.3",
                "Handler": "index.handler"
            }
        }
        self.sam_func = SamFunction.from_dict(logical_id="foo", resource_dict=self.func_dict)
        self.lambda_func = self._make_lambda_function(self.sam_func.logical_id)
        self.lambda_version = self._make_lambda_version("VersionLogicalId", self.sam_func)

    @patch.object(SamFunction, "_get_resolved_alias_name")
    def test_sam_function_with_alias(self, get_resolved_alias_name_mock):
        alias_name = "AliasName"
        func = {
            "Type": "AWS::Serverless::Function",
            "Properties": {
                "CodeUri": self.code_uri,
                "Runtime": "nodejs4.3",
                "Handler": "index.handler",
                "AutoPublishAlias": alias_name
            }
        }

        sam_func = SamFunction.from_dict(logical_id="foo", resource_dict=func)

        kwargs = {}
        kwargs["managed_policy_map"] = {"a": "b"}
        kwargs["event_resources"] = []
        kwargs["intrinsics_resolver"] = self.intrinsics_resolver_mock
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = {"S3Bucket": "bucket", "S3Key": "key", "S3ObjectVersion": "version"}
        get_resolved_alias_name_mock.return_value = alias_name

        resources = sam_func.to_cloudformation(**kwargs)

        # Function, Version, Alias, IAM Role
        self.assertEquals(len(resources), 4)

        aliases = [r.to_dict() for r in resources if r.resource_type == LambdaAlias.resource_type]
        versions = [r.to_dict() for r in resources if r.resource_type == LambdaVersion.resource_type]
        self.assertEquals(len(aliases), 1)
        self.assertEquals(len(versions), 1)

        alias = aliases[0].values()[0]["Properties"]
        self.assertEquals(alias["Name"], alias_name)
        # We don't need to do any deeper validation here because there is a separate SAM template -> CFN template conversion test
        # that will care of validating all properties & connections

        sam_func._get_resolved_alias_name.assert_called_once_with("AutoPublishAlias", alias_name, self.intrinsics_resolver_mock)

    def test_sam_function_with_alias_cannot_be_list(self):

        # Alias cannot be a list
        with self.assertRaises(InvalidResourceException):
            self.func_dict["Properties"]["AutoPublishAlias"] = ["a", "b"]
            SamFunction.from_dict(logical_id="foo", resource_dict=self.func_dict)

    @patch.object(SamFunction, "_get_resolved_alias_name")
    def test_sam_function_with_deployment_preference(self, get_resolved_alias_name_mock):
        deploy_preference_dict = {"Type": "LINEAR"}
        alias_name = "AliasName"
        func = {
            "Type": "AWS::Serverless::Function",
            "Properties": {
                "CodeUri": self.code_uri,
                "Runtime": "nodejs4.3",
                "Handler": "index.handler",
                "AutoPublishAlias": alias_name,
                "DeploymentPreference": deploy_preference_dict
            }
        }

        sam_func = SamFunction.from_dict(logical_id="foo", resource_dict=func)

        kwargs = dict()
        kwargs["managed_policy_map"] = {"a": "b"}
        kwargs["event_resources"] = []
        kwargs["intrinsics_resolver"] = self.intrinsics_resolver_mock
        deployment_preference_collection = self._make_deployment_preference_collection()
        kwargs['deployment_preference_collection'] = deployment_preference_collection
        get_resolved_alias_name_mock.return_value = alias_name

        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = {"S3Bucket": "bucket", "S3Key": "key",
                                                                             "S3ObjectVersion": "version"}
        resources = sam_func.to_cloudformation(**kwargs)

        deployment_preference_collection.update_policy.assert_called_once_with(self.sam_func.logical_id)
        deployment_preference_collection.add.assert_called_once_with(self.sam_func.logical_id,
                                                                     deploy_preference_dict)

        aliases = [r.to_dict() for r in resources if r.resource_type == LambdaAlias.resource_type]

        self.assertTrue("UpdatePolicy" in aliases[0].values()[0])
        self.assertEqual(aliases[0].values()[0]["UpdatePolicy"], self.update_policy().to_dict())

    @patch.object(SamFunction, "_get_resolved_alias_name")
    def test_sam_function_with_deployment_preference_missing_collection_raises_error(self, get_resolved_alias_name_mock):
        alias_name = "AliasName"
        deploy_preference_dict = {"Type": "LINEAR"}
        func = {
            "Type": "AWS::Serverless::Function",
            "Properties": {
                "CodeUri": self.code_uri,
                "Runtime": "nodejs4.3",
                "Handler": "index.handler",
                "AutoPublishAlias": alias_name,
                "DeploymentPreference": deploy_preference_dict
            }
        }

        sam_func = SamFunction.from_dict(logical_id="foo", resource_dict=func)

        kwargs = dict()
        kwargs["managed_policy_map"] = {"a": "b"}
        kwargs["event_resources"] = []
        kwargs["intrinsics_resolver"] = self.intrinsics_resolver_mock
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = {"S3Bucket": "bucket", "S3Key": "key",
                                                                             "S3ObjectVersion": "version"}
        get_resolved_alias_name_mock.return_value = alias_name

        with self.assertRaises(ValueError):
            sam_func.to_cloudformation(**kwargs)

    @patch.object(SamFunction, "_get_resolved_alias_name")
    def test_sam_function_with_disabled_deployment_preference_does_not_add_update_policy(self, get_resolved_alias_name_mock):
        alias_name = "AliasName"
        enabled = False
        deploy_preference_dict = {"Enabled": enabled}
        func = {
            "Type": "AWS::Serverless::Function",
            "Properties": {
                "CodeUri": self.code_uri,
                "Runtime": "nodejs4.3",
                "Handler": "index.handler",
                "AutoPublishAlias": alias_name,
                "DeploymentPreference": deploy_preference_dict
            }
        }

        sam_func = SamFunction.from_dict(logical_id="foo", resource_dict=func)

        kwargs = dict()
        kwargs["managed_policy_map"] = {"a": "b"}
        kwargs["event_resources"] = []
        kwargs["intrinsics_resolver"] = self.intrinsics_resolver_mock
        preference_collection = self._make_deployment_preference_collection()
        preference_collection.get.return_value = DeploymentPreference.from_dict(sam_func.logical_id,
                                                                                deploy_preference_dict)

        kwargs['deployment_preference_collection'] = preference_collection
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = enabled
        get_resolved_alias_name_mock.return_value = alias_name

        resources = sam_func.to_cloudformation(**kwargs)

        preference_collection.add.assert_called_once_with(sam_func.logical_id, deploy_preference_dict)
        preference_collection.get.assert_called_once_with(sam_func.logical_id)
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(enabled)
        aliases = [r.to_dict() for r in resources if r.resource_type == LambdaAlias.resource_type]

        self.assertTrue("UpdatePolicy" not in aliases[0].values()[0])

    def test_sam_function_cannot_be_with_deployment_preference_without_alias(self):
        with self.assertRaises(InvalidResourceException):
            func = {
                "Type": "AWS::Serverless::Function",
                "Properties": {
                    "CodeUri": self.code_uri,
                    "Runtime": "nodejs4.3",
                    "Handler": "index.handler",
                    "DeploymentPreference": {"Type": "LINEAR"}
                }
            }

            sam_func = SamFunction.from_dict(logical_id="foo", resource_dict=func)

            kwargs = dict()
            kwargs["intrinsics_resolver"] = self.intrinsics_resolver_mock
            kwargs['deployment_preference_collection'] = self._make_deployment_preference_collection()
            sam_func.to_cloudformation(**kwargs)

    def test_sam_function_without_alias_allows_disabled_deployment_preference(self):
        enabled = False
        deploy_preference_dict = {"Enabled": enabled}
        func = {
            "Type": "AWS::Serverless::Function",
            "Properties": {
                "CodeUri": self.code_uri,
                "Runtime": "nodejs4.3",
                "Handler": "index.handler",
                "DeploymentPreference": deploy_preference_dict
            }
        }

        sam_func = SamFunction.from_dict(logical_id="foo", resource_dict=func)

        kwargs = dict()
        kwargs["managed_policy_map"] = {"a": "b"}
        kwargs["event_resources"] = []
        kwargs["intrinsics_resolver"] = self.intrinsics_resolver_mock
        preference_collection = self._make_deployment_preference_collection()
        preference_collection.get.return_value = DeploymentPreference.from_dict(sam_func.logical_id,
                                                                                deploy_preference_dict)

        kwargs['deployment_preference_collection'] = preference_collection
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = enabled
        resources = sam_func.to_cloudformation(**kwargs)

        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(enabled)
        # Function, IAM Role
        self.assertEquals(len(resources), 2)

    @patch.object(SamFunction, "_get_resolved_alias_name")
    def test_sam_function_with_deployment_preference_instrinsic_ref_enabled_boolean_parameter(self, get_resolved_alias_name_mock):
        alias_name = "AliasName"
        enabled = {"Ref": "MyEnabledFlag"}
        deploy_preference_dict = {"Type": "LINEAR", "Enabled": enabled}
        func = {
            "Type": "AWS::Serverless::Function",
            "Properties": {
                "CodeUri": self.code_uri,
                "Runtime": "nodejs4.3",
                "Handler": "index.handler",
                "AutoPublishAlias": alias_name,
                "DeploymentPreference": deploy_preference_dict
            }
        }

        sam = SamFunction.from_dict(logical_id="foo", resource_dict=func)

        kwargs = dict()
        kwargs["managed_policy_map"] = {"a": "b"}
        kwargs["event_resources"] = []
        kwargs["intrinsics_resolver"] = self.intrinsics_resolver_mock
        deployment_preference_collection = self._make_deployment_preference_collection()
        kwargs['deployment_preference_collection'] = deployment_preference_collection
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = True
        get_resolved_alias_name_mock.return_value = alias_name

        resources = sam.to_cloudformation(**kwargs)

        deployment_preference_collection.update_policy.assert_called_once_with(self.sam_func.logical_id)
        deployment_preference_collection.add.assert_called_once_with(self.sam_func.logical_id,
                                                                     deploy_preference_dict)
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(enabled)

        aliases = [r.to_dict() for r in resources if r.resource_type == LambdaAlias.resource_type]

        self.assertTrue("UpdatePolicy" in aliases[0].values()[0])
        self.assertEqual(aliases[0].values()[0]["UpdatePolicy"], self.update_policy().to_dict())

    @patch.object(SamFunction, "_get_resolved_alias_name")
    def test_sam_function_with_deployment_preference_instrinsic_ref_enabled_dict_parameter(self, get_resolved_alias_name_mock):
        alias_name = "AliasName"
        enabled = {"Ref": "MyEnabledFlag"}
        deploy_preference_dict = {"Type": "LINEAR", "Enabled": enabled}
        func = {
            "Type": "AWS::Serverless::Function",
            "Properties": {
                "CodeUri": self.code_uri,
                "Runtime": "nodejs4.3",
                "Handler": "index.handler",
                "AutoPublishAlias": alias_name,
                "DeploymentPreference": deploy_preference_dict
            }
        }

        sam_func = SamFunction.from_dict(logical_id="foo", resource_dict=func)

        kwargs = dict()
        kwargs["managed_policy_map"] = {"a": "b"}
        kwargs["event_resources"] = []
        kwargs["intrinsics_resolver"] = self.intrinsics_resolver_mock
        deployment_preference_collection = self._make_deployment_preference_collection()
        kwargs['deployment_preference_collection'] = deployment_preference_collection
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = {"key": "value"}
        get_resolved_alias_name_mock.return_value = alias_name

        with self.assertRaises(InvalidResourceException):
            sam_func.to_cloudformation(**kwargs)

    @patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
    def test_version_creation(self, LogicalIdGeneratorMock):
        generator_mock = LogicalIdGeneratorMock.return_value
        id_val = "SomeLogicalId"
        generator_mock.gen.return_value = id_val

        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code
        version = self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)

        self.assertEqual(version.logical_id, id_val)
        self.assertEqual(version.Description, None)
        self.assertEqual(version.FunctionName, {"Ref": self.lambda_func.logical_id})
        self.assertEqual(version.get_resource_attribute("DeletionPolicy"), "Retain")

        expected_prefix = self.sam_func.logical_id + "Version"
        LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
        generator_mock.gen.assert_called_once_with()
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

    # Test without S3 object version
    @patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
    def test_version_creation_without_s3_object_version(self, LogicalIdGeneratorMock):
        generator_mock = LogicalIdGeneratorMock.return_value
        id_val = "SomeLogicalId"
        generator_mock.gen.return_value = id_val

        del self.lambda_func.Code["S3ObjectVersion"]
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code
        version = self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)

        self.assertEqual(version.logical_id, id_val)

        expected_prefix = self.sam_func.logical_id + "Version"
        LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
        generator_mock.gen.assert_called_once_with()
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

    def test_version_creation_error(self):
        # Empty code dictionary
        self.lambda_func.Code = {}
        with self.assertRaises(ValueError):
            self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)

    @patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
    def test_version_creation_intrinsic_function_in_code_s3key(self, LogicalIdGeneratorMock):
        # One of the properties of Code dictionary is an intrinsic function
        generator_mock = LogicalIdGeneratorMock.return_value
        id_val = "SomeLogicalId"
        generator_mock.gen.return_value = id_val

        self.lambda_func.Code = {
            "S3Bucket": "bucket",
            "S3Key": {"Ref": "keyparameter"},
            "S3ObjectVersion": "version"
        }
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code

        version = self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)
        self.assertEqual(version.logical_id, id_val)

        expected_prefix = self.sam_func.logical_id + "Version"
        LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

    @patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
    def test_version_creation_intrinsic_function_in_code_s3bucket(self, LogicalIdGeneratorMock):
        generator_mock = LogicalIdGeneratorMock.return_value
        id_val = "SomeLogicalId"
        generator_mock.gen.return_value = id_val

        self.lambda_func.Code = {
            "S3Bucket": {"Ref": "bucketparameter"},
            "S3Key": "key",
            "S3ObjectVersion": "version"
        }
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code

        version = self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)
        self.assertEqual(version.logical_id, id_val)

        expected_prefix = self.sam_func.logical_id + "Version"
        LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

    @patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
    def test_version_creation_intrinsic_function_in_code_s3version(self, LogicalIdGeneratorMock):
        generator_mock = LogicalIdGeneratorMock.return_value
        id_val = "SomeLogicalId"
        generator_mock.gen.return_value = id_val

        self.lambda_func.Code = {
            "S3Bucket": "bucket",
            "S3Key": "key",
            "S3ObjectVersion": {"Ref": "versionparameter"}
        }
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code

        version = self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)
        self.assertEqual(version.logical_id, id_val)

        expected_prefix = self.sam_func.logical_id + "Version"
        LogicalIdGeneratorMock.assert_called_once_with(expected_prefix, self.lambda_func.Code)
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_once_with(self.lambda_func.Code)

    @patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
    def test_version_logical_id_changes(self, LogicalIdGeneratorMock):
        generator_mock = LogicalIdGeneratorMock.return_value
        id_val = "SomeLogicalId"
        generator_mock.gen.return_value = id_val
        prefix = self.sam_func.logical_id + "Version"

        # Test that logicalId changes with changes to CodeSha
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code
        self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)

        LogicalIdGeneratorMock.assert_called_once_with(prefix, self.lambda_func.Code)
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(self.lambda_func.Code)

        # Modify Code of the lambda function
        self.lambda_func.Code["S3ObjectVersion"] = "new object version"
        new_code = self.lambda_func.Code.copy()
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = new_code
        self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)
        LogicalIdGeneratorMock.assert_called_with(prefix, new_code)
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(new_code)

    @patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator")
    def test_version_logical_id_changes_with_intrinsic_functions(self, LogicalIdGeneratorMock):
        generator_mock = LogicalIdGeneratorMock.return_value
        id_val = "SomeLogicalId"
        generator_mock.gen.return_value = id_val
        prefix = self.sam_func.logical_id + "Version"

        self.lambda_func.Code = {
            "S3Bucket": "bucket",
            "S3Key": {
                "Ref": "someparam"
            }
        }

        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = self.lambda_func.Code
        self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)

        LogicalIdGeneratorMock.assert_called_once_with(prefix, self.lambda_func.Code)
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(self.lambda_func.Code)

        # Now, just let the intrinsics resolver return a different value. Let's make sure the new value gets wired up properly
        new_code = {"S3Bucket": "bucket", "S3Key": "some new value"}
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = new_code
        self.sam_func._construct_version(self.lambda_func, self.intrinsics_resolver_mock)
        LogicalIdGeneratorMock.assert_called_with(prefix, new_code)
        self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(self.lambda_func.Code)

    def test_alias_creation(self):
        name = "aliasname"

        alias = self.sam_func._construct_alias(name, self.lambda_func, self.lambda_version)

        expected_logical_id = "%sAlias%s" % (self.lambda_func.logical_id, name)
        self.assertEqual(alias.logical_id, expected_logical_id)
        self.assertEqual(alias.Name, name)
        self.assertEqual(alias.FunctionName, {"Ref": self.lambda_func.logical_id})
        self.assertEqual(alias.FunctionVersion, {"Fn::GetAtt": [self.lambda_version.logical_id, "Version"]})


    def test_alias_creation_error(self):
        with self.assertRaises(ValueError):
            self.sam_func._construct_alias(None, self.lambda_func, self.lambda_version)

    def test_get_resolved_alias_name_must_work(self):

        property_name = "something"
        alias_value = {"Ref": "param1"}
        alias_name = "AliasName"
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = alias_name

        result = self.sam_func._get_resolved_alias_name(property_name, alias_value, self.intrinsics_resolver_mock)
        self.assertEquals(alias_name, result)

    def test_get_resolved_alias_name_must_error_if_intrinsics_are_not_resolved(self):

        property_name = "something"
        expected_exception_msg = "Resource with id [{}] is invalid. '{}' must be a string or a Ref to a template parameter"\
            .format(self.sam_func.logical_id, property_name)

        alias_value = {"Ref": "param1"}
        # Unresolved
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = {"Ref": "param1"}

        with self.assertRaises(InvalidResourceException) as raises_assert:
            self.sam_func._get_resolved_alias_name(property_name, alias_value, self.intrinsics_resolver_mock)

        ex = raises_assert.exception
        self.assertEquals(expected_exception_msg, ex.message)

    def test_get_resolved_alias_name_must_error_if_intrinsics_are_not_resolved_with_list(self):

        property_name = "something"
        expected_exception_msg = "Resource with id [{}] is invalid. '{}' must be a string or a Ref to a template parameter"\
            .format(self.sam_func.logical_id, property_name)

        alias_value = ["Ref", "param1"]
        # Unresolved
        self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = ["Ref", "param1"]

        with self.assertRaises(InvalidResourceException) as raises_assert:
            self.sam_func._get_resolved_alias_name(property_name, alias_value, self.intrinsics_resolver_mock)

        ex = raises_assert.exception
        self.assertEquals(expected_exception_msg, ex.message)


    def _make_lambda_function(self, logical_id):
        func = LambdaFunction(logical_id)
        func.Code = {
            "S3Bucket": "bucket",
            "S3Key": "key",
            "S3ObjectVersion": "version"
        }
        return func

    def _make_lambda_version(self, logical_id, func):
        version = LambdaVersion(logical_id)
        version.FunctionName = {"Ref": func.logical_id}
        return version

    def update_policy(self):
        return UpdatePolicy("app_name", "deploy_group_name", None, None)

    def _make_deployment_preference_collection(self):
        deployment_preference_collection = Mock()
        deployment_preference_collection.update_policy.return_value = self.update_policy()

        return deployment_preference_collection


class TestSupportedResourceReferences(TestCase):

    def test_must_not_break_support(self):

        func = SamFunction("LogicalId")
        self.assertEquals(2, len(func.referable_properties))
        self.assertEquals(func.referable_properties["Alias"], "AWS::Lambda::Alias")
        self.assertEquals(func.referable_properties["Version"], "AWS::Lambda::Version")
