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

from samtranslator.intrinsics.actions import Action, FindInMapAction, GetAttAction, RefAction, SubAction
from samtranslator.intrinsics.resource_refs import SupportedResourceReferences
from samtranslator.model.exceptions import InvalidDocumentException


class TestAction(TestCase):
    def test_can_handle_input(self):
        class MyAction(Action):
            intrinsic_name = "foo"

        input = {"foo": ["something"]}

        action = MyAction()
        self.assertTrue(action.can_handle(input))

    def test_can_handle_invalid_type(self):
        class MyAction(Action):
            intrinsic_name = "foo"

        input = {"bar": "something"}
        action = MyAction()
        self.assertFalse(action.can_handle(input))

    def test_can_handle_invalid_input(self):
        class MyAction(Action):
            intrinsic_name = "foo"

        input = "string input"
        action = MyAction()
        self.assertFalse(action.can_handle(input))

    def test_can_handle_invalid_dict(self):
        class MyAction(Action):
            intrinsic_name = "foo"

        # Intrinsic functions can be only in dict of length 1
        input = {"foo": "some value", "bar": "some other value"}
        action = MyAction()
        self.assertFalse(action.can_handle(input))

    def test_can_handle_empty_input(self):
        class MyAction(Action):
            intrinsic_name = "foo"

        # Intrinsic functions can be only in dict of length 1
        input = None
        action = MyAction()
        self.assertFalse(action.can_handle(input))

    def test_parse_resource_references_with_one_property(self):
        input = "LogicalId.Property"
        expected = ("LogicalId", "Property")

        self.assertEqual(expected, Action._parse_resource_reference(input))

    def test_parse_resource_references_with_multiple_properties(self):
        input = "LogicalId.Property1.Property2.Property3"
        expected = ("LogicalId", "Property1.Property2.Property3")

        self.assertEqual(expected, Action._parse_resource_reference(input))

    def test_parse_resource_references_with_other_special_characters(self):
        input = "some logical id . some value"
        expected = ("some logical id ", " some value")

        self.assertEqual(expected, Action._parse_resource_reference(input))

    def test_parse_resource_references_with_empty_property(self):
        # Just a dot at the end! This is equivalent of no property
        input = "LogicalId."
        expected = (None, None)

        self.assertEqual(expected, Action._parse_resource_reference(input))

    def test_parse_resource_references_with_empty_logical_id(self):
        # Just a dot at the beginning! This is equivalent of no LogicalId
        input = ".Property"
        expected = (None, None)

        self.assertEqual(expected, Action._parse_resource_reference(input))

    def test_parse_resource_references_with_no_property(self):
        input = "LogicalId"
        expected = (None, None)

        self.assertEqual(expected, Action._parse_resource_reference(input))

    def test_parse_resource_references_not_string(self):
        input = {"not a": "string"}
        expected = (None, None)

        self.assertEqual(expected, Action._parse_resource_reference(input))


class TestRefCanResolveParameterRefs(TestCase):
    def test_can_resolve_ref(self):
        parameters = {"key": "value"}
        input = {"Ref": "key"}

        ref = RefAction()
        self.assertEqual(parameters["key"], ref.resolve_parameter_refs(input, parameters))

    def test_unknown_ref(self):
        parameters = {"key": "value"}
        input = {"Ref": "someotherkey"}
        expected = {"Ref": "someotherkey"}

        ref = RefAction()
        self.assertEqual(expected, ref.resolve_parameter_refs(input, parameters))

    def test_must_ignore_invalid_value(self):
        parameters = {"key": "value"}
        input = {"Ref": ["invalid value"]}
        expected = {"Ref": ["invalid value"]}

        ref = RefAction()
        self.assertEqual(expected, ref.resolve_parameter_refs(input, parameters))

    @patch.object(RefAction, "can_handle")
    def test_return_value_if_cannot_handle(self, can_handle_mock):
        parameters = {"key": "value"}
        input = {"Ref": "key"}
        expected = {"Ref": "key"}

        ref = RefAction()
        can_handle_mock.return_value = False  # Simulate failure to handle the input. Result should be same as input
        self.assertEqual(expected, ref.resolve_parameter_refs(input, parameters))


class TestRefCanResolveResourceRefs(TestCase):
    def setUp(self):
        self.supported_resource_refs_mock = Mock()
        self.ref = RefAction()

    @patch.object(RefAction, "_parse_resource_reference")
    def test_must_replace_refs(self, _parse_resource_reference_mock):
        resolved_value = "SomeOtherValue"
        input = {"Ref": "LogicalId.Property"}
        expected = {"Ref": resolved_value}
        _parse_resource_reference_mock.return_value = ("LogicalId", "Property")
        self.supported_resource_refs_mock.get.return_value = resolved_value

        output = self.ref.resolve_resource_refs(input, self.supported_resource_refs_mock)

        self.assertEqual(expected, output)
        self.supported_resource_refs_mock.get.assert_called_once_with("LogicalId", "Property")
        _parse_resource_reference_mock.assert_called_once_with("LogicalId.Property")

    @patch.object(RefAction, "_parse_resource_reference")
    def test_handle_unsupported_references(self, _parse_resource_reference_mock):
        input = {"Ref": "LogicalId.Property"}
        expected = {"Ref": "LogicalId.Property"}

        _parse_resource_reference_mock.return_value = ("LogicalId", "Property")
        self.supported_resource_refs_mock.get.return_value = None

        output = self.ref.resolve_resource_refs(input, self.supported_resource_refs_mock)
        self.assertEqual(expected, output)
        self.supported_resource_refs_mock.get.assert_called_once_with("LogicalId", "Property")
        _parse_resource_reference_mock.assert_called_once_with("LogicalId.Property")

    @patch.object(RefAction, "_parse_resource_reference")
    def test_handle_unparsable_reference_value(self, _parse_resource_reference_mock):
        input = {"Ref": "some value"}
        expected = {"Ref": "some value"}

        _parse_resource_reference_mock.return_value = (None, None)

        output = self.ref.resolve_resource_refs(input, self.supported_resource_refs_mock)

        self.assertEqual(expected, output)
        self.supported_resource_refs_mock.get.assert_not_called()
        _parse_resource_reference_mock.assert_called_once_with("some value")

    @patch.object(RefAction, "can_handle")
    def test_return_value_if_cannot_handle(self, can_handle_mock):
        input = {"Ref": "key"}
        expected = {"Ref": "key"}

        ref = RefAction()
        can_handle_mock.return_value = False  # Simulate failure to handle the input. Result should be same as input
        self.assertEqual(expected, ref.resolve_resource_refs(input, self.supported_resource_refs_mock))


class TestRefCanResolveResourceIdRefs(TestCase):
    def setUp(self):
        self.supported_resource_id_refs_mock = Mock()
        self.ref = RefAction()

    def test_must_replace_refs(self):
        resolved_value = "NewLogicalId"
        input = {"Ref": "LogicalId"}
        expected = {"Ref": resolved_value}
        self.supported_resource_id_refs_mock.get.return_value = resolved_value

        output = self.ref.resolve_resource_id_refs(input, self.supported_resource_id_refs_mock)

        self.assertEqual(expected, output)
        self.supported_resource_id_refs_mock.get.assert_called_once_with("LogicalId")

    def test_handle_unsupported_references(self):
        input = {"Ref": "OtherLogicalId.Property"}
        expected = {"Ref": "OtherLogicalId.Property"}

        self.supported_resource_id_refs_mock.get.return_value = None

        output = self.ref.resolve_resource_id_refs(input, self.supported_resource_id_refs_mock)
        self.assertEqual(expected, output)
        self.supported_resource_id_refs_mock.get.assert_not_called()

    @patch.object(RefAction, "can_handle")
    def test_return_value_if_cannot_handle(self, can_handle_mock):
        input = {"Ref": "key"}
        expected = {"Ref": "key"}

        ref = RefAction()
        can_handle_mock.return_value = False  # Simulate failure to handle the input. Result should be same as input
        self.assertEqual(expected, ref.resolve_resource_id_refs(input, self.supported_resource_id_refs_mock))


class TestSubCanResolveParameterRefs(TestCase):
    def test_must_resolve_string_value(self):
        parameters = {"key1": "value1"}
        input = {"Fn::Sub": "Hello ${key1}"}
        expected = {"Fn::Sub": "Hello value1"}

        sub = SubAction()
        result = sub.resolve_parameter_refs(input, parameters)

        self.assertEqual(expected, result)

    def test_must_resolve_array_value(self):
        parameters = {"key1": "value1"}
        input = {"Fn::Sub": ["Hello ${key1} ${a}", {"a": "b"}]}

        expected = {"Fn::Sub": ["Hello value1 ${a}", {"a": "b"}]}

        sub = SubAction()
        result = sub.resolve_parameter_refs(input, parameters)

        self.assertEqual(expected, result)

    @patch.object(SubAction, "can_handle")
    def test_return_value_if_cannot_handle(self, can_handle_mock):
        parameters = {"key": "value"}
        input = {"Fn::Sub": "${key}"}
        expected = {"Fn::Sub": "${key}"}

        sub = SubAction()
        can_handle_mock.return_value = False  # Simulate failure to handle the input. Result should be same as input
        self.assertEqual(expected, sub.resolve_parameter_refs(input, parameters))

    def test_sub_all_refs_multiple_references(self):
        parameters = {"key1": "value1", "key2": "value2"}
        input = {"Fn::Sub": "hello ${key1} ${key2} ${key1}${key2} ${unknown} ${key1.attr}"}
        expected = {"Fn::Sub": "hello value1 value2 value1value2 ${unknown} ${key1.attr}"}

        sub = SubAction()
        result = sub.resolve_parameter_refs(input, parameters)

        self.assertEqual(expected, result)

    def test_sub_all_refs_with_literals(self):
        parameters = {"key1": "value1", "key2": "value2"}
        input = {"Fn::Sub": "hello ${key1} ${key2} ${!key1} ${!key2}"}
        expected = {
            # ${! is the prefix for literal. These won't be substituted
            "Fn::Sub": "hello value1 value2 ${!key1} ${!key2}"
        }

        sub = SubAction()
        result = sub.resolve_parameter_refs(input, parameters)

        self.assertEqual(expected, result)

    def test_sub_all_refs_with_list_input(self):
        parameters = {"key1": "value1", "key2": "value2"}
        input = {"Fn::Sub": ["key1", "key2"]}
        expected = {"Fn::Sub": ["key1", "key2"]}

        sub = SubAction()
        result = sub.resolve_parameter_refs(input, parameters)

        self.assertEqual(expected, result)

    def test_sub_all_refs_with_dict_input(self):
        parameters = {"key1": "value1", "key2": "value2"}
        input = {"Fn::Sub": {"a": "key1", "b": "key2"}}
        expected = {"Fn::Sub": {"a": "key1", "b": "key2"}}

        sub = SubAction()
        result = sub.resolve_parameter_refs(input, parameters)

        self.assertEqual(expected, result)

    def test_sub_all_refs_with_pseudo_parameters(self):
        parameters = {"key1": "value1", "AWS::Region": "ap-southeast-1"}
        input = {"Fn::Sub": "hello ${AWS::Region} ${key1}"}
        expected = {"Fn::Sub": "hello ap-southeast-1 value1"}

        sub = SubAction()
        result = sub.resolve_parameter_refs(input, parameters)

        self.assertEqual(expected, result)


class TestSubInternalMethods(TestCase):
    @patch.object(SubAction, "_sub_all_refs")
    def test_handle_sub_value_must_call_handler_on_string(self, sub_all_refs_mock):
        input = "sub string"
        expected = "result"
        handler_mock = Mock()
        sub_all_refs_mock.return_value = expected

        sub = SubAction()
        result = sub._handle_sub_value(input, handler_mock)

        self.assertEqual(expected, result)
        sub_all_refs_mock.assert_called_once_with(input, handler_mock)

    @patch.object(SubAction, "_sub_all_refs")
    def test_handle_sub_value_must_call_handler_on_array(self, sub_all_refs_mock):
        input = ["sub string", {"a": "b"}]
        expected = ["sub string", {"a": "b"}]
        handler_mock = Mock()
        sub_all_refs_mock.return_value = expected[0]

        sub = SubAction()
        result = sub._handle_sub_value(input, handler_mock)

        self.assertEqual(expected, result)
        sub_all_refs_mock.assert_called_once_with(input[0], handler_mock)

    @patch.object(SubAction, "_sub_all_refs")
    def test_handle_sub_value_must_skip_no_string(self, sub_all_refs_mock):
        input = [{"a": "b"}]
        expected = [{"a": "b"}]
        handler_mock = Mock()

        sub = SubAction()
        result = sub._handle_sub_value(input, handler_mock)

        self.assertEqual(expected, result)
        handler_mock.assert_not_called()
        sub_all_refs_mock.assert_not_called()

    @patch.object(SubAction, "_sub_all_refs")
    def test_must_skip_invalid_input_empty_list(self, sub_all_refs_mock):
        input = []
        expected = []
        handler_mock = Mock()

        sub = SubAction()
        result = sub._handle_sub_value(input, handler_mock)

        self.assertEqual(expected, result)
        handler_mock.assert_not_called()
        sub_all_refs_mock.assert_not_called()

    @patch.object(SubAction, "_sub_all_refs")
    def test_must_skip_invalid_input_dict(self, sub_all_refs_mock):
        input = {"a": "b"}
        expected = {"a": "b"}
        handler_mock = Mock()

        sub = SubAction()
        result = sub._handle_sub_value(input, handler_mock)

        self.assertEqual(expected, result)
        handler_mock.assert_not_called()
        sub_all_refs_mock.assert_not_called()


class TestSubCanResolveResourceRefs(TestCase):
    def setUp(self):
        self.supported_resource_refs = SupportedResourceReferences()
        self.supported_resource_refs.add("id1", "prop1", "value1")
        self.supported_resource_refs.add("id1", "prop2", "value2")
        self.supported_resource_refs.add("id2", "prop3", "value3")

        self.input_sub_value = "Hello ${id1.prop1} ${id1.prop2}${id2.prop3} ${id1.prop1.arn} ${id1.prop2.arn.name.foo} ${!id1.prop1} ${unknown} ${some.arn} World"
        self.expected_output_sub_value = "Hello ${value1} ${value2}${value3} ${value1.arn} ${value2.arn.name.foo} ${!id1.prop1} ${unknown} ${some.arn} World"

    def test_must_resolve_string_value(self):
        input = {"Fn::Sub": self.input_sub_value}
        expected = {"Fn::Sub": self.expected_output_sub_value}

        sub = SubAction()
        result = sub.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, result)

    def test_must_resolve_array_value(self):
        input = {"Fn::Sub": [self.input_sub_value, {"unknown": "a"}]}

        expected = {"Fn::Sub": [self.expected_output_sub_value, {"unknown": "a"}]}

        sub = SubAction()
        result = sub.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, result)

    def test_sub_all_refs_with_list_input(self):
        parameters = {"key1": "value1", "key2": "value2"}
        input = {"Fn::Sub": ["key1", "key2"]}
        expected = {"Fn::Sub": ["key1", "key2"]}

        sub = SubAction()
        result = sub.resolve_resource_refs(input, parameters)

        self.assertEqual(expected, result)

    def test_sub_all_refs_with_dict_input(self):
        parameters = {"key1": "value1", "key2": "value2"}
        input = {"Fn::Sub": {"a": "key1", "b": "key2"}}
        expected = {"Fn::Sub": {"a": "key1", "b": "key2"}}

        sub = SubAction()
        result = sub.resolve_resource_refs(input, parameters)

        self.assertEqual(expected, result)

    @patch.object(SubAction, "can_handle")
    def test_return_value_if_cannot_handle(self, can_handle_mock):
        parameters = {"key": "value"}
        input = {"Fn::Sub": "${key}"}
        expected = {"Fn::Sub": "${key}"}

        sub = SubAction()
        can_handle_mock.return_value = False  # Simulate failure to handle the input. Result should be same as input
        self.assertEqual(expected, sub.resolve_resource_refs(input, parameters))


class TestSubCanResolveResourceIdRefs(TestCase):
    def setUp(self):
        self.supported_resource_id_refs = {}
        self.supported_resource_id_refs["id1"] = "newid1"
        self.supported_resource_id_refs["id2"] = "newid2"
        self.supported_resource_id_refs["id3"] = "newid3"

        self.input_sub_value = (
            "Hello ${id1} ${id2}${id3} ${id1.arn} ${id2.arn.name.foo} ${!id1.prop1} ${unknown} ${some.arn} World"
        )
        self.expected_output_sub_value = "Hello ${newid1} ${newid2}${newid3} ${newid1.arn} ${newid2.arn.name.foo} ${!id1.prop1} ${unknown} ${some.arn} World"

    def test_must_resolve_string_value(self):
        input = {"Fn::Sub": self.input_sub_value}
        expected = {"Fn::Sub": self.expected_output_sub_value}

        sub = SubAction()
        result = sub.resolve_resource_id_refs(input, self.supported_resource_id_refs)

        self.assertEqual(expected, result)

    def test_must_resolve_array_value(self):
        input = {"Fn::Sub": [self.input_sub_value, {"unknown": "a"}]}

        expected = {"Fn::Sub": [self.expected_output_sub_value, {"unknown": "a"}]}

        sub = SubAction()
        result = sub.resolve_resource_id_refs(input, self.supported_resource_id_refs)

        self.assertEqual(expected, result)

    def test_sub_all_refs_with_list_input(self):
        parameters = {"key1": "value1", "key2": "value2"}
        input = {"Fn::Sub": ["key1", "key2"]}
        expected = {"Fn::Sub": ["key1", "key2"]}

        sub = SubAction()
        result = sub.resolve_resource_id_refs(input, parameters)

        self.assertEqual(expected, result)

    def test_sub_all_refs_with_dict_input(self):
        parameters = {"key1": "value1", "key2": "value2"}
        input = {"Fn::Sub": {"a": "key1", "b": "key2"}}
        expected = {"Fn::Sub": {"a": "key1", "b": "key2"}}

        sub = SubAction()
        result = sub.resolve_resource_id_refs(input, parameters)

        self.assertEqual(expected, result)

    @patch.object(SubAction, "can_handle")
    def test_return_value_if_cannot_handle(self, can_handle_mock):
        parameters = {"key": "value"}
        input = {"Fn::Sub": "${key}"}
        expected = {"Fn::Sub": "${key}"}

        sub = SubAction()
        can_handle_mock.return_value = False  # Simulate failure to handle the input. Result should be same as input
        self.assertEqual(expected, sub.resolve_resource_id_refs(input, parameters))


class TestGetAttCanResolveParameterRefs(TestCase):
    def test_must_do_nothing(self):
        # Parameter references are not resolved by GetAtt
        input = "foo"
        expected = "foo"
        supported_resource_refs = Mock()
        self.assertEqual(expected, GetAttAction().resolve_parameter_refs(input, supported_resource_refs))

        supported_resource_refs.assert_not_called()


class TestGetAttCanResolveResourceRefs(TestCase):
    def setUp(self):
        self.supported_resource_refs = SupportedResourceReferences()
        self.supported_resource_refs.add("id1", "prop1", "value1")

    def test_must_resolve_simple_refs(self):
        input = {"Fn::GetAtt": ["id1.prop1", "Arn"]}

        expected = {"Fn::GetAtt": ["value1", "Arn"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, output)

    def test_must_resolve_refs_with_many_attributes(self):
        input = {"Fn::GetAtt": ["id1.prop1", "Arn1", "Arn2", "Arn3"]}

        expected = {"Fn::GetAtt": ["value1", "Arn1", "Arn2", "Arn3"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, output)

    def test_must_resolve_with_splitted_resource_refs(self):
        input = {
            # Reference to resource `id1.prop1` is split into different elements
            "Fn::GetAtt": ["id1", "prop1", "Arn1", "Arn2", "Arn3"]
        }

        expected = {"Fn::GetAtt": ["value1", "Arn1", "Arn2", "Arn3"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, output)

    def test_must_ignore_refs_without_attributes(self):
        input = {
            # No actual attributes. But since we have two entries in the array, we try to resolve them
            "Fn::GetAtt": ["id1", "prop1"]
        }

        expected = {"Fn::GetAtt": ["value1"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, output)

    def test_must_ignore_refs_without_attributes_in_concatenated_form(self):
        input = {
            # No actual attributes. with just only one entry in array, the value never gets resolved
            "Fn::GetAtt": ["id1.prop1"]
        }

        expected = {"Fn::GetAtt": ["id1.prop1"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, output)

    def test_must_ignore_invalid_value_array(self):
        input = {
            # No actual attributes
            "Fn::GetAtt": ["id1"]
        }

        expected = {"Fn::GetAtt": ["id1"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, output)

    def test_must_ignore_invalid_value_type(self):
        input = {
            # No actual attributes
            "Fn::GetAtt": {"a": "b"}
        }

        expected = {"Fn::GetAtt": {"a": "b"}}

        getatt = GetAttAction()
        output = getatt.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, output)

    def test_must_ignore_missing_properties_with_dot_after(self):
        input = {"Fn::GetAtt": ["id1.", "foo"]}
        expected = {"Fn::GetAtt": ["id1.", "foo"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, output)

    def test_must_ignore_missing_properties_with_dot_before(self):
        input = {"Fn::GetAtt": [".id1", "foo"]}
        expected = {"Fn::GetAtt": [".id1", "foo"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_refs(input, self.supported_resource_refs)

        self.assertEqual(expected, output)

    @patch.object(GetAttAction, "can_handle")
    def test_return_value_if_cannot_handle(self, can_handle_mock):
        input = {"Fn::GetAtt": ["id1", "prop1"]}
        expected = {"Fn::GetAtt": ["id1", "prop1"]}

        getatt = GetAttAction()
        can_handle_mock.return_value = False  # Simulate failure to handle the input. Result should be same as input
        self.assertEqual(expected, getatt.resolve_resource_refs(input, self.supported_resource_refs))


class TestGetAttCanResolveResourceIdRefs(TestCase):
    def setUp(self):
        self.supported_resource_id_refs = {}
        self.supported_resource_id_refs["id1"] = "value1"

    def test_must_resolve_simple_refs(self):
        input = {"Fn::GetAtt": ["id1", "Arn"]}

        expected = {"Fn::GetAtt": ["value1", "Arn"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs)

        self.assertEqual(expected, output)

    def test_must_resolve_refs_with_many_attributes(self):
        input = {"Fn::GetAtt": ["id1", "Arn1", "Arn2", "Arn3"]}

        expected = {"Fn::GetAtt": ["value1", "Arn1", "Arn2", "Arn3"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs)

        self.assertEqual(expected, output)

    def test_must_ignore_invalid_value_array(self):
        input = {
            # No actual attributes
            "Fn::GetAtt": ["id1"]
        }

        expected = {"Fn::GetAtt": ["id1"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs)

        self.assertEqual(expected, output)

    def test_must_ignore_invalid_value_type(self):
        input = {
            # No actual attributes
            "Fn::GetAtt": {"a": "b"}
        }

        expected = {"Fn::GetAtt": {"a": "b"}}

        getatt = GetAttAction()
        output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs)

        self.assertEqual(expected, output)

    def test_must_ignore_non_string_types(self):
        input = {"Fn::GetAtt": ["a", {"c": "d"}]}
        expected = {"Fn::GetAtt": ["a", {"c": "d"}]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs)

        self.assertEqual(expected, output)

    def test_must_ignore_missing_properties_with_dot_before(self):
        input = {"Fn::GetAtt": [".id1", "foo"]}
        expected = {"Fn::GetAtt": [".id1", "foo"]}

        getatt = GetAttAction()
        output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs)

        self.assertEqual(expected, output)

    @patch.object(GetAttAction, "can_handle")
    def test_return_value_if_cannot_handle(self, can_handle_mock):
        input = {"Fn::GetAtt": ["id1", "Arn"]}
        expected = {"Fn::GetAtt": ["id1", "Arn"]}

        getatt = GetAttAction()
        can_handle_mock.return_value = False  # Simulate failure to handle the input. Result should be same as input
        self.assertEqual(expected, getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs))


class TestFindInMapCanResolveParameterRefs(TestCase):
    def setUp(self):
        self.ref = FindInMapAction()

    @patch.object(FindInMapAction, "can_handle")
    def test_cannot_handle(self, can_handle_mock):
        input = {"Fn::FindInMap": ["a", "b", "c"]}
        can_handle_mock.return_value = False
        output = self.ref.resolve_parameter_refs(input, {})

        self.assertEqual(input, output)

    def test_value_not_list(self):
        input = {"Fn::FindInMap": "a string"}
        with self.assertRaises(InvalidDocumentException):
            self.ref.resolve_parameter_refs(input, {})

    def test_value_not_list_of_length_three(self):
        input = {"Fn::FindInMap": ["a string"]}

        with self.assertRaises(InvalidDocumentException):
            self.ref.resolve_parameter_refs(input, {})

    def test_mapping_not_string(self):
        mappings = {"MapA": {"TopKey1": {"SecondKey2": "value3"}, "TopKey2": {"SecondKey1": "value4"}}}
        input = {"Fn::FindInMap": [["MapA"], "TopKey2", "SecondKey1"]}
        output = self.ref.resolve_parameter_refs(input, mappings)

        self.assertEqual(input, output)

    def test_top_level_key_not_string(self):
        mappings = {"MapA": {"TopKey1": {"SecondKey2": "value3"}, "TopKey2": {"SecondKey1": "value4"}}}
        input = {"Fn::FindInMap": ["MapA", ["TopKey2"], "SecondKey1"]}
        output = self.ref.resolve_parameter_refs(input, mappings)

        self.assertEqual(input, output)

    def test_second_level_key_not_string(self):
        mappings = {"MapA": {"TopKey1": {"SecondKey2": "value3"}, "TopKey2": {"SecondKey1": "value4"}}}
        input = {"Fn::FindInMap": ["MapA", "TopKey1", ["SecondKey2"]]}
        output = self.ref.resolve_parameter_refs(input, mappings)

        self.assertEqual(input, output)

    def test_mapping_not_found(self):
        mappings = {"MapA": {"TopKey1": {"SecondKey2": "value3"}, "TopKey2": {"SecondKey1": "value4"}}}
        input = {"Fn::FindInMap": ["MapB", "TopKey2", "SecondKey1"]}
        output = self.ref.resolve_parameter_refs(input, mappings)

        self.assertEqual(input, output)

    def test_top_level_key_not_found(self):
        mappings = {"MapA": {"TopKey1": {"SecondKey2": "value3"}, "TopKey2": {"SecondKey1": "value4"}}}
        input = {"Fn::FindInMap": ["MapA", "TopKey3", "SecondKey1"]}
        output = self.ref.resolve_parameter_refs(input, mappings)

        self.assertEqual(input, output)

    def test_second_level_key_not_found(self):
        mappings = {"MapA": {"TopKey1": {"SecondKey2": "value3"}, "TopKey2": {"SecondKey1": "value4"}}}
        input = {"Fn::FindInMap": ["MapA", "TopKey1", "SecondKey1"]}
        output = self.ref.resolve_parameter_refs(input, mappings)

        self.assertEqual(input, output)

    def test_one_level_find_in_mappings(self):
        mappings = {"MapA": {"TopKey1": {"SecondKey2": "value3"}, "TopKey2": {"SecondKey1": "value4"}}}
        input = {"Fn::FindInMap": ["MapA", "TopKey2", "SecondKey1"]}
        expected = "value4"
        output = self.ref.resolve_parameter_refs(input, mappings)

        self.assertEqual(expected, output)

    def test_nested_find_in_mappings(self):
        mappings = {"MapA": {"TopKey1": {"SecondKey2": "value3"}, "TopKey2": {"SecondKey1": "TopKey1"}}}
        input = {"Fn::FindInMap": ["MapA", {"Fn::FindInMap": ["MapA", "TopKey2", "SecondKey1"]}, "SecondKey2"]}
        expected = "value3"
        output = self.ref.resolve_parameter_refs(input, mappings)

        self.assertEqual(expected, output)

    def test_nested_find_in_multiple_mappings(self):
        mappings = {"MapA": {"ATopKey1": {"ASecondKey2": "value3"}}, "MapB": {"BTopKey1": {"BSecondKey2": "ATopKey1"}}}
        input = {"Fn::FindInMap": ["MapA", {"Fn::FindInMap": ["MapB", "BTopKey1", "BSecondKey2"]}, "ASecondKey2"]}
        expected = "value3"
        output = self.ref.resolve_parameter_refs(input, mappings)

        self.assertEqual(expected, output)
