from unittest import TestCase
from unittest.mock import ANY, Mock, patch

from samtranslator.policy_template_processor.exceptions import InsufficientParameterValues, InvalidParameterValues
from samtranslator.policy_template_processor.template import Template


class TestTemplateObject(TestCase):
    def test_init_must_check_for_existence_of_all_parameters(self):
        template_name = "template_name"
        parameters = {}
        template_definition = {"key": "value"}

        template = Template(template_name, parameters, template_definition)

        self.assertEqual(template.name, template_name)
        self.assertEqual(template.parameters, parameters)
        self.assertEqual(template.definition, template_definition)

    def test_from_dict_must_return_object(self):
        template_name = "template_name"
        parameters = {"A": "B"}
        template_definition = {"key": "value"}

        template_dict = {"Parameters": parameters, "Definition": template_definition}

        template = Template.from_dict(template_name, template_dict)

        self.assertTrue(isinstance(template, Template))
        self.assertEqual(template.name, template_name)
        self.assertEqual(template.parameters, parameters)
        self.assertEqual(template.definition, template_definition)

    def test_from_dict_must_work_when_parameters_is_absent(self):
        template_name = "template_name"
        template_definition = {"key": "value"}

        template_dict = {"Definition": template_definition}

        template = Template.from_dict(template_name, template_dict)

        self.assertEqual(template.parameters, {})  # Defaults to {}
        self.assertEqual(template.definition, template_definition)

    def test_from_dict_must_work_when_template_definition_is_absent(self):
        template_name = "template_name"
        parameters = {"key": "value"}

        template_dict = {"Parameters": parameters}

        template = Template.from_dict(template_name, template_dict)

        self.assertEqual(template.parameters, parameters)
        self.assertEqual(template.definition, {})  # Defaults to {}

    def test_missing_parameter_values_must_work_when_input_has_less_keys(self):
        template_parameters = {"param1": {"Description": "foo"}, "param2": {"Description": "bar"}}
        parameter_values = {"param1": "value1"}
        expected = ["param2"]

        template = Template("name", template_parameters, {})
        result = template.missing_parameter_values(parameter_values)

        self.assertEqual(expected, result)

    def test_missing_parameter_values_must_work_when_input_has_all_keys(self):
        template_parameters = {"param1": {"Description": "foo"}, "param2": {"Description": "bar"}}
        parameter_values = {"param1": "value1", "param2": "value3"}
        expected = []

        template = Template("name", template_parameters, {})
        result = template.missing_parameter_values(parameter_values)

        self.assertEqual(expected, result)

    def test_missing_parameter_values_must_work_when_input_has_more_keys(self):
        template_parameters = {"param1": {"Description": "foo"}, "param2": {"Description": "bar"}}
        parameter_values = {"param1": "value1", "param2": "value2", "newparam": "new value"}
        expected = []  # We do a set-difference. So new keys won't make it here

        template = Template("name", template_parameters, {})
        result = template.missing_parameter_values(parameter_values)

        self.assertEqual(expected, result)

    def test_missing_parameter_values_must_raise_on_invalid_input(self):
        template_parameters = {"param1": {"Description": "foo"}, "param2": {"Description": "bar"}}
        parameter_values = [1, 2, 3]

        template = Template("name", template_parameters, {})

        with self.assertRaises(InvalidParameterValues):
            template.missing_parameter_values(parameter_values)

    def test_is_valid_parameter_values_must_work(self):
        parameter_values = {"a": "b"}
        self.assertTrue(Template._is_valid_parameter_values(parameter_values))

    def test_is_valid_parameter_values_must_fail_for_none_value(self):
        parameter_values = None
        self.assertFalse(Template._is_valid_parameter_values(parameter_values))

    def test_is_valid_parameter_values_must_fail_for_non_dict(self):
        parameter_values = [1, 2, 3]
        self.assertFalse(Template._is_valid_parameter_values(parameter_values))

    @patch("samtranslator.policy_template_processor.template.IntrinsicsResolver")
    def test_to_statement_must_work_with_valid_inputs(self, intrinsics_resolver_mock):
        parameter_values = {"param1": "b"}
        template_parameters = {"param1": {"Description": "something"}}
        template_definition = {"Statement": {"key": "value"}}
        expected = "some result"

        resolver_instance_mock = Mock()
        intrinsics_resolver_mock.return_value = resolver_instance_mock
        resolver_instance_mock.resolve_parameter_refs.return_value = expected

        template = Template("name", template_parameters, template_definition)
        result = template.to_statement(parameter_values)

        self.assertEqual(expected, result)
        intrinsics_resolver_mock.assert_called_once_with({"___SAM_POLICY_PARAMETER_param1": "b"}, {"Ref": ANY})
        resolver_instance_mock.resolve_parameter_refs.assert_called_once_with({"Statement": {"key": "value"}})

    @patch("samtranslator.policy_template_processor.template.IntrinsicsResolver")
    def test_to_statement_must_exclude_extra_parameter_values(self, intrinsics_resolver_mock):
        parameter_values = {"param1": "b", "key1": "value1", "key2": "value2"}
        template_parameters = {"param1": {"Description": "something"}}
        template_definition = {"Statement": {"key": "value"}}

        resolver_instance_mock = Mock()
        intrinsics_resolver_mock.return_value = resolver_instance_mock
        resolver_instance_mock.resolve_parameter_refs.return_value = "some result"

        template = Template("name", template_parameters, template_definition)
        template.to_statement(parameter_values)

        # Intrinsics resolver must be called only with the parameters declared in the template
        expected_parameter_values = {"___SAM_POLICY_PARAMETER_param1": "b"}
        intrinsics_resolver_mock.assert_called_once_with(expected_parameter_values, ANY)

    @patch("samtranslator.policy_template_processor.template.IntrinsicsResolver")
    def test_to_statement_must_raise_with_missing_parameters(self, intrinsics_resolver_mock):
        parameter_values = {"key1": "value1", "key2": "value2"}
        template_parameters = {"param1": {"Description": "something"}}
        template_definition = {"Statement": {"key": "value"}}

        template = Template("name", template_parameters, template_definition)
        with self.assertRaises(InsufficientParameterValues):
            template.to_statement(parameter_values)

    @patch("samtranslator.policy_template_processor.template.IntrinsicsResolver")
    def test_to_statement_must_fail_for_invalid_parameter_values(self, intrinsics_resolver_mock):
        parameter_values = None

        template = Template("name", {}, {})

        with self.assertRaises(InvalidParameterValues):
            template.to_statement(parameter_values)
