from unittest import TestCase
from unittest.mock import patch

from samtranslator.model.codedeploy import CodeDeployApplication, CodeDeployDeploymentGroup
from samtranslator.model.exceptions import InvalidResourceException
from samtranslator.model.iam import IAMRole
from samtranslator.model.preferences.deployment_preference import DeploymentPreference
from samtranslator.model.preferences.deployment_preference_collection import (
    CODE_DEPLOY_SERVICE_ROLE_LOGICAL_ID,
    CODEDEPLOY_APPLICATION_LOGICAL_ID,
    DeploymentPreferenceCollection,
)


class TestDeploymentPreferenceCollection(TestCase):
    def setup_method(self, method):
        self.deployment_type_global = "AllAtOnce"
        self.alarms_global = [{"Ref": "alarm1"}, {"Ref": "alarm2"}]
        self.post_traffic_host_global = "post_traffic_function_ref"
        self.pre_traffic_hook_global = "pre_traffic_function_ref"
        self.function_logical_id = "FunctionLogicalId"
        self.condition = "CodeDeployCondition"

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_when_no_global_dict_each_local_deployment_preference_requires_parameters(self):
        with self.assertRaises(InvalidResourceException):
            DeploymentPreferenceCollection().add("", dict())

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_add_when_logical_id_previously_added_raises_value_error(self):
        with self.assertRaises(ValueError):
            deployment_preference_collection = DeploymentPreferenceCollection()
            deployment_preference_collection.add("1", {"Type": "Canary"})
            deployment_preference_collection.add("1", {"Type": "Linear"})

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_codedeploy_application(self):
        expected_codedeploy_application_resource = CodeDeployApplication(CODEDEPLOY_APPLICATION_LOGICAL_ID)
        expected_codedeploy_application_resource.ComputePlatform = "Lambda"

        self.assertEqual(
            DeploymentPreferenceCollection().get_codedeploy_application().to_dict(),
            expected_codedeploy_application_resource.to_dict(),
        )

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_codedeploy_iam_role(self):
        expected_codedeploy_iam_role = IAMRole("CodeDeployServiceRole")
        expected_codedeploy_iam_role.AssumeRolePolicyDocument = {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": ["sts:AssumeRole"],
                    "Effect": "Allow",
                    "Principal": {"Service": ["codedeploy.amazonaws.com"]},
                }
            ],
        }
        expected_codedeploy_iam_role.ManagedPolicyArns = [
            "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda"
        ]

        self.assertEqual(
            DeploymentPreferenceCollection().get_codedeploy_iam_role().to_dict(), expected_codedeploy_iam_role.to_dict()
        )

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_group_with_minimal_parameters(self):
        expected_deployment_group = CodeDeployDeploymentGroup(self.function_logical_id + "DeploymentGroup")
        expected_deployment_group.ApplicationName = {"Ref": CODEDEPLOY_APPLICATION_LOGICAL_ID}
        expected_deployment_group.AutoRollbackConfiguration = {
            "Enabled": True,
            "Events": ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM", "DEPLOYMENT_STOP_ON_REQUEST"],
        }
        expected_deployment_group.DeploymentConfigName = {
            "Fn::Sub": ["CodeDeployDefault.Lambda${ConfigName}", {"ConfigName": self.deployment_type_global}]
        }
        expected_deployment_group.DeploymentStyle = {
            "DeploymentType": "BLUE_GREEN",
            "DeploymentOption": "WITH_TRAFFIC_CONTROL",
        }
        expected_deployment_group.ServiceRoleArn = {"Fn::GetAtt": [CODE_DEPLOY_SERVICE_ROLE_LOGICAL_ID, "Arn"]}

        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, {"Type": self.deployment_type_global})
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertEqual(deployment_group.to_dict(), expected_deployment_group.to_dict())

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_codedeploy_custom_configuration(self):
        deployment_type = "TestDeploymentConfiguration"
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, {"Type": deployment_type})
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertEqual(deployment_type, deployment_group.DeploymentConfigName)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_codedeploy_predifined_configuration(self):
        deployment_type = "Canary10Percent5Minutes"
        expected_deployment_config_name = {
            "Fn::Sub": ["CodeDeployDefault.Lambda${ConfigName}", {"ConfigName": deployment_type}]
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, {"Type": deployment_type})
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        print(deployment_group.DeploymentConfigName)
        self.assertEqual(expected_deployment_config_name, deployment_group.DeploymentConfigName)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_conditional_custom_configuration(self):
        deployment_type = {
            "Fn::If": [
                "IsDevEnv",
                {"Fn::If": ["IsDevEnv1", "AllAtOnce", "TestDeploymentConfiguration"]},
                "Canary10Percent15Minutes",
            ]
        }

        expected_deployment_config_name = {
            "Fn::If": [
                "IsDevEnv",
                {
                    "Fn::If": [
                        "IsDevEnv1",
                        {"Fn::Sub": ["CodeDeployDefault.Lambda${ConfigName}", {"ConfigName": "AllAtOnce"}]},
                        "TestDeploymentConfiguration",
                    ]
                },
                {"Fn::Sub": ["CodeDeployDefault.Lambda${ConfigName}", {"ConfigName": "Canary10Percent15Minutes"}]},
            ]
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, {"Type": deployment_type})
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)
        print(deployment_group.DeploymentConfigName)
        self.assertEqual(expected_deployment_config_name, deployment_group.DeploymentConfigName)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_group_with_all_parameters(self):
        expected_deployment_group = CodeDeployDeploymentGroup(self.function_logical_id + "DeploymentGroup")
        expected_deployment_group.AlarmConfiguration = {
            "Enabled": True,
            "Alarms": [{"Name": {"Ref": "alarm1"}}, {"Name": {"Ref": "alarm2"}}],
        }
        expected_deployment_group.ApplicationName = {"Ref": CODEDEPLOY_APPLICATION_LOGICAL_ID}
        expected_deployment_group.AutoRollbackConfiguration = {
            "Enabled": True,
            "Events": ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM", "DEPLOYMENT_STOP_ON_REQUEST"],
        }
        expected_deployment_group.DeploymentConfigName = {
            "Fn::Sub": ["CodeDeployDefault.Lambda${ConfigName}", {"ConfigName": self.deployment_type_global}]
        }
        expected_deployment_group.DeploymentStyle = {
            "DeploymentType": "BLUE_GREEN",
            "DeploymentOption": "WITH_TRAFFIC_CONTROL",
        }
        expected_deployment_group.ServiceRoleArn = {"Fn::GetAtt": [CODE_DEPLOY_SERVICE_ROLE_LOGICAL_ID, "Arn"]}

        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, self.global_deployment_preference_yaml_dict())
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertEqual(deployment_group.to_dict(), expected_deployment_group.to_dict())

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": ["Alarm1", "Alarm2", "Alarm3"],
        }
        expected_alarm_configuration = {
            "Enabled": True,
            "Alarms": [{"Name": "Alarm1"}, {"Name": "Alarm2"}, {"Name": "Alarm3"}],
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms_intrinsic_if(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": {"Fn::If": ["MyCondition", ["Alarm1", "Alarm2"], ["Alarm3"]]},
        }
        expected_alarm_configuration = {
            "Fn::If": [
                "MyCondition",
                {"Enabled": True, "Alarms": [{"Name": "Alarm1"}, {"Name": "Alarm2"}]},
                {"Enabled": True, "Alarms": [{"Name": "Alarm3"}]},
            ],
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms_intrinsic_if_empty_then(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": {"Fn::If": ["MyCondition", [], ["Alarm3"]]},
        }
        expected_alarm_configuration = {
            "Fn::If": ["MyCondition", {}, {"Enabled": True, "Alarms": [{"Name": "Alarm3"}]}],
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms_intrinsic_if_noref_then(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": {"Fn::If": ["MyCondition", [{"Ref": "AWS::NoValue"}], ["Alarm3"]]},
        }
        expected_alarm_configuration = {
            "Fn::If": ["MyCondition", {}, {"Enabled": True, "Alarms": [{"Name": "Alarm3"}]}],
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms_intrinsic_if_empty_else(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": {"Fn::If": ["MyCondition", ["Alarm1", "Alarm2"], []]},
        }
        expected_alarm_configuration = {
            "Fn::If": ["MyCondition", {"Enabled": True, "Alarms": [{"Name": "Alarm1"}, {"Name": "Alarm2"}]}, {}],
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms_intrinsic_if_noref_else(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": {"Fn::If": ["MyCondition", ["Alarm1", "Alarm2"], [{"Ref": "AWS::NoValue"}]]},
        }
        expected_alarm_configuration = {
            "Fn::If": ["MyCondition", {"Enabled": True, "Alarms": [{"Name": "Alarm1"}, {"Name": "Alarm2"}]}, {}],
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms_ref_novalue(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": {"Ref": "AWS::NoValue"},
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertIsNone(deployment_group.AlarmConfiguration)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms_empty(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": [],
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id)

        self.assertIsNone(deployment_group.AlarmConfiguration)

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms_not_list(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": "Alarm1",
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        with self.assertRaises(InvalidResourceException) as e:
            deployment_preference_collection.deployment_group(self.function_logical_id)
        self.assertEqual(
            e.exception.message,
            f"Resource with id [{self.function_logical_id}] is invalid. Alarms must be a list",
        )

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms_intrinsic_if_missing_arg(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": {"Fn::If": ["MyCondition", ["Alarm1", "Alarm2"]]},
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        with self.assertRaises(InvalidResourceException) as e:
            deployment_preference_collection.deployment_group(self.function_logical_id)
        self.assertEqual(
            e.exception.message,
            f"Resource with id [{self.function_logical_id}] is invalid. Fn::If requires 3 arguments",
        )

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_deployment_preference_with_alarms_intrinsic_if_not_list(self):
        deployment_preference = {
            "Type": "TestDeploymentConfiguration",
            "Alarms": {"Fn::If": "Alarm1"},
        }
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, deployment_preference)
        with self.assertRaises(InvalidResourceException) as e:
            deployment_preference_collection.deployment_group(self.function_logical_id)
        self.assertEqual(
            e.exception.message,
            f"Resource with id [{self.function_logical_id}] is invalid. Fn::If requires 3 arguments",
        )

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_update_policy_with_minimal_parameters(self):
        expected_update_policy = {
            "CodeDeployLambdaAliasUpdate": {
                "ApplicationName": {"Ref": CODEDEPLOY_APPLICATION_LOGICAL_ID},
                "DeploymentGroupName": {"Ref": self.function_logical_id + "DeploymentGroup"},
            }
        }

        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, {"Type": "CANARY"})
        update_policy = deployment_preference_collection.update_policy(self.function_logical_id)

        self.assertEqual(expected_update_policy, update_policy.to_dict())

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_update_policy_with_all_parameters(self):
        expected_update_polcy = {
            "CodeDeployLambdaAliasUpdate": {
                "ApplicationName": {"Ref": CODEDEPLOY_APPLICATION_LOGICAL_ID},
                "DeploymentGroupName": {"Ref": self.function_logical_id + "DeploymentGroup"},
                "BeforeAllowTrafficHook": self.pre_traffic_hook_global,
                "AfterAllowTrafficHook": self.post_traffic_host_global,
            }
        }

        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add(self.function_logical_id, self.global_deployment_preference_yaml_dict())
        update_policy = deployment_preference_collection.update_policy(self.function_logical_id)

        self.assertEqual(expected_update_polcy, update_policy.to_dict())

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_any_enabled_true_if_one_of_three_enabled(self):
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add("1", {"Type": "LINEAR"})
        deployment_preference_collection.add("2", {"Type": "LINEAR", "Enabled": False})
        deployment_preference_collection.add("3", {"Type": "CANARY", "Enabled": False})

        self.assertTrue(deployment_preference_collection.any_enabled())

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_any_enabled_true_if_all_of_three_enabled(self):
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add("1", {"Type": "LINEAR"})
        deployment_preference_collection.add("2", {"Type": "LINEAR", "Enabled": True})
        deployment_preference_collection.add("3", {"Type": "CANARY", "Enabled": True})

        self.assertTrue(deployment_preference_collection.any_enabled())

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_any_enabled_false_if_all_of_three_disabled(self):
        deployment_preference_collection = DeploymentPreferenceCollection()
        deployment_preference_collection.add("1", {"Type": "Linear", "Enabled": False})
        deployment_preference_collection.add("2", {"Type": "LINEAR", "Enabled": False})
        deployment_preference_collection.add("3", {"Type": "CANARY", "Enabled": False})

        self.assertFalse(deployment_preference_collection.any_enabled())

    @patch("boto3.session.Session.region_name", "ap-southeast-1")
    def test_enabled_logical_ids_returns_one_if_one_of_three_enabled(self):
        deployment_preference_collection = DeploymentPreferenceCollection()
        enabled_logical_id = "1"
        deployment_preference_collection.add(enabled_logical_id, {"Type": "LINEAR"})
        deployment_preference_collection.add("2", {"Type": "LINEAR", "Enabled": False})
        deployment_preference_collection.add("3", {"Type": "CANARY", "Enabled": False})

        enabled_logical_ids = deployment_preference_collection.enabled_logical_ids()
        self.assertEqual(1, len(enabled_logical_ids))
        self.assertEqual(enabled_logical_id, enabled_logical_ids[0])

    def global_deployment_preference_yaml_dict(self):
        deployment_preference_yaml_dict = dict()
        deployment_preference_yaml_dict["Type"] = self.deployment_type_global
        deployment_preference_yaml_dict["Hooks"] = {
            "PreTraffic": self.pre_traffic_hook_global,
            "PostTraffic": self.post_traffic_host_global,
        }
        deployment_preference_yaml_dict["Alarms"] = self.alarms_global
        return deployment_preference_yaml_dict

    def global_deployment_preference(self):
        return DeploymentPreference(
            self.deployment_type_global,
            self.pre_traffic_hook_global,
            self.post_traffic_host_global,
            self.alarms_global,
            True,
        )
