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

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


class TestPolicyTemplatesForResourcePlugin(TestCase):
    def setUp(self):
        self._policy_template_processor_mock = Mock()
        self.plugin = PolicyTemplatesForResourcePlugin(self._policy_template_processor_mock)

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

        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.ResourcePolicies")
    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.ResourcePolicies")
    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.ResourcePolicies")
    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.ResourcePolicies")
    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.ResourcePolicies")
    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.ResourcePolicies")
    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()
