from unittest import TestCase
from mock import Mock, MagicMock, patch, call

from samtranslator.plugins import BasePlugin
from samtranslator.model.function_policies import PolicyTypes, PolicyEntry
from samtranslator.model.exceptions import InvalidResourceException
from samtranslator.plugins.policies.policy_templates_plugin import PolicyTemplatesForFunctionPlugin
from samtranslator.policy_template_processor.exceptions import InsufficientParameterValues, InvalidParameterValues


class TestPolicyTemplatesForFunctionPlugin(TestCase):

    def setUp(self):
        self._policy_template_processor_mock = Mock()
        self.plugin = PolicyTemplatesForFunctionPlugin(self._policy_template_processor_mock)

    def test_plugin_must_setup_correct_name(self):
        # Name is the class name
        expected_name = "PolicyTemplatesForFunctionPlugin"

        self.assertEqual(self.plugin.name, expected_name)

    def test_plugin_must_be_instance_of_base_plugin_class(self):
        self.assertTrue(isinstance(self.plugin, BasePlugin))

    def test_must_only_support_function_resource(self):
        function_type = "AWS::Serverless::Function"
        self.assertTrue(self.plugin._is_supported(function_type))

    def test_must_not_support_non_function_resources(self):
        resource_type = "AWS::Serverless::Api"
        self.assertFalse(self.plugin._is_supported(resource_type))

    @patch("samtranslator.plugins.policies.policy_templates_plugin.FunctionPolicies")
    def test_on_before_transform_resource_must_work_on_every_policy_template(self, function_policies_class_mock):
        is_supported_mock = Mock()
        self.plugin._is_supported = is_supported_mock
        is_supported_mock.return_value = True

        function_policies_obj_mock = MagicMock()
        function_policies_class_mock.return_value = function_policies_obj_mock
        function_policies_class_mock.POLICIES_PROPERTY_NAME = "Policies"

        template1 = {
            "MyTemplate1": {
                "Param1": "value1"
            }
        }
        template2 = {
            "MyTemplate2": {
                "Param2": "value2"
            }
        }
        resource_properties = {
            "Policies": [template1, template2]
        }

        policies = [
            PolicyEntry(data=template1, type=PolicyTypes.POLICY_TEMPLATE),
            PolicyEntry(data=template2, type=PolicyTypes.POLICY_TEMPLATE),
        ]

        # Setup to return all the policies
        function_policies_obj_mock.__len__.return_value = 2
        function_policies_obj_mock.get.return_value = iter(policies)

        # These are the values returned on every call to `convert` method
        self._policy_template_processor_mock.convert.side_effect = [
            {"Statement1": {"key1": "value1"}},
            {"Statement2": {"key2": "value2"}},
        ]

        expected = [{"Statement1": {"key1": "value1"}}, {"Statement2": {"key2": "value2"}}]
        self.plugin.on_before_transform_resource("logicalId", "resource_type", resource_properties)

        # This will overwrite the resource_properties input array
        self.assertEqual(expected, resource_properties["Policies"])
        function_policies_obj_mock.get.assert_called_once_with()
        self._policy_template_processor_mock.convert.assert_has_calls([
            call("MyTemplate1", {"Param1": "value1"}),
            call("MyTemplate2", {"Param2": "value2"})
        ])

    @patch("samtranslator.plugins.policies.policy_templates_plugin.FunctionPolicies")
    def test_on_before_transform_resource_must_skip_non_policy_templates(self, function_policies_class_mock):
        is_supported_mock = Mock()
        self.plugin._is_supported = is_supported_mock
        is_supported_mock.return_value = True

        function_policies_obj_mock = MagicMock()
        function_policies_class_mock.return_value = function_policies_obj_mock
        function_policies_class_mock.POLICIES_PROPERTY_NAME = "Policies"

        template1 = {
            "MyTemplate1": {
                "Param1": "value1"
            }
        }
        template2 = {
            "MyTemplate2": {
                "Param2": "value2"
            }
        }
        regular_policy = {"regular policy": "something"}
        resource_properties = {
            "Policies": [template1, regular_policy, template2]
        }

        policies = [
            PolicyEntry(data=template1, type=PolicyTypes.POLICY_TEMPLATE),
            PolicyEntry(data=regular_policy, type=PolicyTypes.MANAGED_POLICY),
            PolicyEntry(data=template2, type=PolicyTypes.POLICY_TEMPLATE),
        ]

        # Setup to return all the policies
        function_policies_obj_mock.__len__.return_value = 3
        function_policies_obj_mock.get.return_value = iter(policies)

        # These are the values returned on every call to `convert` method
        self._policy_template_processor_mock.convert.side_effect = [
            {"Statement1": {"key1": "value1"}},
            {"Statement2": {"key2": "value2"}},
        ]

        expected = [{"Statement1": {"key1": "value1"}}, {"regular policy": "something"}, {"Statement2": {"key2": "value2"}}]
        self.plugin.on_before_transform_resource("logicalId", "resource_type", resource_properties)

        # This will overwrite the resource_properties input array
        self.assertEqual(expected, resource_properties["Policies"])
        function_policies_obj_mock.get.assert_called_once_with()
        self._policy_template_processor_mock.convert.assert_has_calls([
            call("MyTemplate1", {"Param1": "value1"}),
            call("MyTemplate2", {"Param2": "value2"})
        ])


    @patch("samtranslator.plugins.policies.policy_templates_plugin.FunctionPolicies")
    def test_on_before_transform_must_raise_on_insufficient_parameter_values(self, function_policies_class_mock):
        is_supported_mock = Mock()
        self.plugin._is_supported = is_supported_mock
        is_supported_mock.return_value = True

        function_policies_obj_mock = MagicMock()
        function_policies_class_mock.return_value = function_policies_obj_mock

        template1 = {
            "MyTemplate1": {
                "Param1": "value1"
            }
        }
        resource_properties = {
            "Policies": template1
        }

        policies = [
            PolicyEntry(data=template1, type=PolicyTypes.POLICY_TEMPLATE)
        ]

        # Setup to return all the policies
        function_policies_obj_mock.__len__.return_value = 1
        function_policies_obj_mock.get.return_value = iter(policies)

        # These are the values returned on every call to `convert` method
        self._policy_template_processor_mock.convert.side_effect = InsufficientParameterValues("message")

        with self.assertRaises(InvalidResourceException):
            self.plugin.on_before_transform_resource("logicalId", "resource_type", resource_properties)

        # Make sure the input was not changed
        self.assertEqual(resource_properties, {"Policies": {"MyTemplate1": { "Param1": "value1"}}})

    @patch("samtranslator.plugins.policies.policy_templates_plugin.FunctionPolicies")
    def test_on_before_transform_must_raise_on_invalid_parameter_values(self, function_policies_class_mock):
        is_supported_mock = Mock()
        self.plugin._is_supported = is_supported_mock
        is_supported_mock.return_value = True

        function_policies_obj_mock = MagicMock()
        function_policies_class_mock.return_value = function_policies_obj_mock

        template1 = {
            "MyTemplate1": {
                "Param1": "value1"
            }
        }
        resource_properties = {
            "Policies": template1
        }

        policies = [
            PolicyEntry(data=template1, type=PolicyTypes.POLICY_TEMPLATE)
        ]

        # Setup to return all the policies
        function_policies_obj_mock.__len__.return_value = 1
        function_policies_obj_mock.get.return_value = iter(policies)

        self._policy_template_processor_mock.convert.side_effect = InvalidParameterValues("message")

        with self.assertRaises(InvalidResourceException):
            self.plugin.on_before_transform_resource("logicalId", "resource_type", resource_properties)

        # Make sure the input was not changed
        self.assertEqual(resource_properties, {"Policies": {"MyTemplate1": { "Param1": "value1"}}})

    @patch("samtranslator.plugins.policies.policy_templates_plugin.FunctionPolicies")
    def test_on_before_transform_must_bubble_exception(self, function_policies_class_mock):
        is_supported_mock = Mock()
        self.plugin._is_supported = is_supported_mock
        is_supported_mock.return_value = True

        function_policies_obj_mock = MagicMock()
        function_policies_class_mock.return_value = function_policies_obj_mock

        template1 = {
            "MyTemplate1": {
                "Param1": "value1"
            }
        }
        resource_properties = {
            "Policies": template1
        }

        policies = [
            PolicyEntry(data=template1, type=PolicyTypes.POLICY_TEMPLATE)
        ]

        # Setup to return all the policies
        function_policies_obj_mock.__len__.return_value = 1
        function_policies_obj_mock.get.return_value = iter(policies)

        self._policy_template_processor_mock.convert.side_effect = TypeError('message')

        with self.assertRaises(TypeError):
            self.plugin.on_before_transform_resource("logicalId", "resource_type", resource_properties)

        # Make sure the input was not changed
        self.assertEqual(resource_properties, {"Policies": {"MyTemplate1": { "Param1": "value1"}}})

    def test_on_before_transform_resource_must_skip_unsupported_resources(self):

        is_supported_mock = Mock()
        data_mock = Mock()

        self.plugin._is_supported = is_supported_mock
        is_supported_mock.return_value = False

        self.plugin.on_before_transform_resource(data_mock, data_mock, data_mock)

        # Make sure none of the data elements were accessed, because the method returned immediately
        self.assertEqual([], data_mock.method_calls)

    @patch("samtranslator.plugins.policies.policy_templates_plugin.FunctionPolicies")
    def test_on_before_transform_resource_must_skip_function_with_no_policies(self, function_policies_class_mock):
        is_supported_mock = Mock()
        self.plugin._is_supported = is_supported_mock
        is_supported_mock.return_value = True

        function_policies_obj_mock = MagicMock()
        function_policies_class_mock.return_value = function_policies_obj_mock

        # Setup to return NO policies
        function_policies_obj_mock.__len__.return_value = 0

        self.plugin.on_before_transform_resource("logicalId", "resource_type", {})

        # Since length was zero, get() should never be called
        function_policies_obj_mock.get.assert_not_called()
