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

from samtranslator.model.exceptions import InvalidTemplateException
from samtranslator.model.intrinsics import is_intrinsic_if, is_intrinsic_no_value
from samtranslator.model.resource_policies import PolicyEntry, PolicyTypes, ResourcePolicies


class TestResourcePolicies(TestCase):
    def setUp(self):
        self.policy_template_processor_mock = Mock()
        self.is_policy_template_mock = Mock()

        self.function_policies = ResourcePolicies({}, self.policy_template_processor_mock)
        self.function_policies._is_policy_template = self.is_policy_template_mock

    @patch.object(ResourcePolicies, "_get_policies")
    def test_initialization_must_ingest_policies_from_resource_properties(self, get_policies_mock):
        resource_properties = {}
        dummy_policy_results = ["some", "policy", "statements"]
        expected_length = 3

        get_policies_mock.return_value = dummy_policy_results
        function_policies = ResourcePolicies(resource_properties, self.policy_template_processor_mock)

        get_policies_mock.assert_called_once_with(resource_properties)
        self.assertEqual(expected_length, len(function_policies))

    @patch.object(ResourcePolicies, "_get_policies")
    def test_get_must_yield_results_on_every_call(self, get_policies_mock):
        resource_properties = {}  # Just some input
        dummy_policy_results = ["some", "policy", "statements"]
        expected_results = ["some", "policy", "statements"]

        # Setup _get_policies to return these dummy values for testing
        get_policies_mock.return_value = dummy_policy_results
        function_policies = ResourcePolicies(resource_properties, self.policy_template_processor_mock)

        # `list()` will implicitly call the `get()` repeatedly because it is a generator
        self.assertEqual(list(function_policies.get()), expected_results)

    @patch.object(ResourcePolicies, "_get_policies")
    def test_get_must_yield_no_results_with_no_policies(self, get_policies_mock):
        resource_properties = {}  # Just some input
        dummy_policy_results = []
        expected_result = []

        # Setup _get_policies to return these dummy values for testing
        get_policies_mock.return_value = dummy_policy_results
        function_policies = ResourcePolicies(resource_properties, self.policy_template_processor_mock)

        # `list()` will implicitly call the `get()` repeatedly because it is a generator
        self.assertEqual(list(function_policies.get()), expected_result)

    def test_contains_policies_must_work_for_valid_input(self):
        resource_properties = {"Policies": "some managed policy"}

        self.assertTrue(self.function_policies._contains_policies(resource_properties))

    def test_contains_policies_must_ignore_resources_without_policies(self):
        resource_properties = {"some key": "value"}

        self.assertFalse(self.function_policies._contains_policies(resource_properties))

    def test_contains_policies_must_ignore_non_dict_resources(self):
        resource_properties = "some value"

        self.assertFalse(self.function_policies._contains_policies(resource_properties))

    def test_contains_policies_must_ignore_none_resources(self):
        resource_properties = None

        self.assertFalse(self.function_policies._contains_policies(resource_properties))

    def test_contains_policies_must_ignore_lowercase_property_name(self):
        # Property names are case sensitive
        resource_properties = {"policies": "some managed policy"}

        self.assertFalse(self.function_policies._contains_policies(resource_properties))

    def test_get_type_must_work_for_managed_policy(self):
        policy = "managed policy is a string"
        expected = PolicyTypes.MANAGED_POLICY

        result = self.function_policies._get_type(policy)
        self.assertEqual(result, expected)

    @patch("samtranslator.model.resource_policies.is_intrinsic")
    def test_get_type_must_work_for_managed_policy_with_intrinsics(self, is_intrinsic_mock):
        policy = {"Ref": "somevalue"}
        expected = PolicyTypes.MANAGED_POLICY
        is_intrinsic_mock.return_value = True

        result = self.function_policies._get_type(policy)
        self.assertEqual(result, expected)

    def test_get_type_must_work_for_policy_statements(self):
        policy = {"Statement": "policy statements have a 'Statement' key"}
        expected = PolicyTypes.POLICY_STATEMENT

        result = self.function_policies._get_type(policy)
        self.assertEqual(result, expected)

    def test_get_type_must_work_for_policy_templates(self):
        policy = {"PolicyTemplate": "some template"}
        self.is_policy_template_mock.return_value = True
        expected = PolicyTypes.POLICY_TEMPLATE

        result = self.function_policies._get_type(policy)
        self.assertEqual(result, expected)

    def test_get_type_must_ignore_invalid_policy(self):
        policy = {"not-sure-what-this-is": "value"}
        # This is also not a policy template
        self.is_policy_template_mock.return_value = False
        expected = PolicyTypes.UNKNOWN

        result = self.function_policies._get_type(policy)
        self.assertEqual(result, expected)

    def test_get_type_must_ignore_invalid_policy_value_list(self):
        policy = ["invalid", "policy"]
        expected = PolicyTypes.UNKNOWN

        self.is_policy_template_mock.return_value = False

        result = self.function_policies._get_type(policy)
        self.assertEqual(result, expected)
        self.is_policy_template_mock.assert_called_once_with(policy)

    def test_get_policies_must_return_all_policies(self):
        policies = [
            "managed policy 1",
            {"Ref": "some managed policy"},
            {"Statement": "policy statement"},
            {"PolicyTemplate": "some value"},
            ["unknown", "policy"],
        ]
        resource_properties = {"Policies": policies}
        self.is_policy_template_mock.side_effect = [True, False]  # Return True for policy template, False for the list

        expected = [
            PolicyEntry(data="managed policy 1", type=PolicyTypes.MANAGED_POLICY),
            PolicyEntry(data={"Ref": "some managed policy"}, type=PolicyTypes.MANAGED_POLICY),
            PolicyEntry(data={"Statement": "policy statement"}, type=PolicyTypes.POLICY_STATEMENT),
            PolicyEntry(data={"PolicyTemplate": "some value"}, type=PolicyTypes.POLICY_TEMPLATE),
            PolicyEntry(data=["unknown", "policy"], type=PolicyTypes.UNKNOWN),
        ]

        result = self.function_policies._get_policies(resource_properties)
        self.assertEqual(result, expected)

    def test_get_policies_must_ignore_if_resource_does_not_contain_policy(self):
        resource_properties = {}
        expected = []

        result = self.function_policies._get_policies(resource_properties)
        self.assertEqual(result, expected)

    def test_get_policies_must_ignore_if_policies_is_empty(self):
        resource_properties = {"Policies": []}
        expected = []

        result = self.function_policies._get_policies(resource_properties)
        self.assertEqual(result, expected)

    def test_get_policies_must_work_for_single_policy_string(self):
        resource_properties = {"Policies": "single managed policy"}
        expected = [PolicyEntry(data="single managed policy", type=PolicyTypes.MANAGED_POLICY)]

        result = self.function_policies._get_policies(resource_properties)
        self.assertEqual(result, expected)

    def test_get_policies_must_work_for_single_dict_with_managed_policy_intrinsic(self):
        resource_properties = {"Policies": {"Ref": "some managed policy"}}
        expected = [PolicyEntry(data={"Ref": "some managed policy"}, type=PolicyTypes.MANAGED_POLICY)]

        result = self.function_policies._get_policies(resource_properties)
        self.assertEqual(result, expected)

    def test_get_policies_must_work_for_single_dict_with_policy_statement(self):
        resource_properties = {"Policies": {"Statement": "some policy statement"}}
        expected = [PolicyEntry(data={"Statement": "some policy statement"}, type=PolicyTypes.POLICY_STATEMENT)]

        result = self.function_policies._get_policies(resource_properties)
        self.assertEqual(result, expected)

    def test_get_policies_must_work_for_single_dict_of_policy_template(self):
        resource_properties = {"Policies": {"PolicyTemplate": "some template"}}
        self.is_policy_template_mock.return_value = True
        expected = [PolicyEntry(data={"PolicyTemplate": "some template"}, type=PolicyTypes.POLICY_TEMPLATE)]

        result = self.function_policies._get_policies(resource_properties)
        self.assertEqual(result, expected)
        self.is_policy_template_mock.assert_called_once_with(resource_properties["Policies"])

    def test_get_policies_must_work_for_single_dict_of_invalid_policy_template(self):
        resource_properties = {"Policies": {"InvalidPolicyTemplate": "some template"}}
        self.is_policy_template_mock.return_value = False  # Invalid policy template
        expected = [PolicyEntry(data={"InvalidPolicyTemplate": "some template"}, type=PolicyTypes.UNKNOWN)]

        result = self.function_policies._get_policies(resource_properties)
        self.assertEqual(result, expected)
        self.is_policy_template_mock.assert_called_once_with({"InvalidPolicyTemplate": "some template"})

    def test_get_policies_must_work_for_unknown_policy_types(self):
        resource_properties = {"Policies": [1, 2, 3]}
        expected = [
            PolicyEntry(data=1, type=PolicyTypes.UNKNOWN),
            PolicyEntry(data=2, type=PolicyTypes.UNKNOWN),
            PolicyEntry(data=3, type=PolicyTypes.UNKNOWN),
        ]

        self.is_policy_template_mock.return_value = False

        result = self.function_policies._get_policies(resource_properties)
        self.assertEqual(result, expected)

    def test_is_policy_template_must_detect_valid_policy_templates(self):
        template_name = "template_name"
        policy = {template_name: {"Param1": "foo"}}

        self.policy_template_processor_mock.has.return_value = True
        function_policies = ResourcePolicies({}, self.policy_template_processor_mock)

        self.assertTrue(function_policies._is_policy_template(policy))

        self.policy_template_processor_mock.has.assert_called_once_with(template_name)

    def test_is_policy_template_must_ignore_non_dict_policies(self):
        policy = [1, 2, 3]

        self.policy_template_processor_mock.has.return_value = True
        function_policies = ResourcePolicies({}, self.policy_template_processor_mock)

        self.assertFalse(function_policies._is_policy_template(policy))

        self.policy_template_processor_mock.has.assert_not_called()

    def test_is_policy_template_must_ignore_none_policies(self):
        policy = None

        function_policies = ResourcePolicies({}, self.policy_template_processor_mock)
        self.assertFalse(function_policies._is_policy_template(policy))

    def test_is_policy_template_must_ignore_dict_with_two_keys(self):
        template_name = "template_name"
        policy = {template_name: {"param1": "foo"}, "A": "B"}

        self.policy_template_processor_mock.has.return_value = True

        function_policies = ResourcePolicies({}, self.policy_template_processor_mock)
        self.assertFalse(function_policies._is_policy_template(policy))

    def test_is_policy_template_must_ignore_non_policy_templates(self):
        template_name = "template_name"
        policy = {template_name: {"param1": "foo"}}

        self.policy_template_processor_mock.has.return_value = False

        function_policies = ResourcePolicies({}, self.policy_template_processor_mock)
        self.assertFalse(function_policies._is_policy_template(policy))

        self.policy_template_processor_mock.has.assert_called_once_with(template_name)

    def test_is_policy_template_must_return_false_without_the_processor(self):
        policy = {"template_name": {"param1": "foo"}}

        function_policies_obj = ResourcePolicies({}, None)  # No policy template processor

        self.assertFalse(function_policies_obj._is_policy_template(policy))
        self.policy_template_processor_mock.has.assert_not_called()

    def test_is_intrinsic_if_must_return_true_for_if(self):
        policy = {"Fn::If": "some value"}

        self.assertTrue(is_intrinsic_if(policy))

    def test_is_intrinsic_if_must_return_false_for_others(self):
        too_many_keys = {"Fn::If": "some value", "Fn::And": "other value"}

        not_if = {"Fn::Or": "some value"}

        self.assertFalse(is_intrinsic_if(too_many_keys))
        self.assertFalse(is_intrinsic_if(not_if))
        self.assertFalse(is_intrinsic_if(None))

    def test_is_intrinsic_no_value_must_return_true_for_no_value(self):
        policy = {"Ref": "AWS::NoValue"}

        self.assertTrue(is_intrinsic_no_value(policy))

    def test_is_intrinsic_no_value_must_return_false_for_other_value(self):
        bad_key = {"sRefs": "AWS::NoValue"}

        bad_value = {"Ref": "SWA::NoValue"}

        too_many_keys = {"Ref": "AWS::NoValue", "feR": "SWA::NoValue"}

        self.assertFalse(is_intrinsic_no_value(bad_key))
        self.assertFalse(is_intrinsic_no_value(bad_value))
        self.assertFalse(is_intrinsic_no_value(None))
        self.assertFalse(is_intrinsic_no_value(too_many_keys))

    def test_get_type_with_intrinsic_if_must_return_managed_policy_type(self):
        managed_policy = {"Fn::If": ["SomeCondition", "some managed policy arn", "other managed policy arn"]}

        no_value_if = {"Fn::If": ["SomeCondition", {"Ref": "AWS::NoValue"}, "other managed policy arn"]}

        no_value_else = {"Fn::If": ["SomeCondition", "other managed policy arn", {"Ref": "AWS::NoValue"}]}

        expected_managed_policy = PolicyTypes.MANAGED_POLICY

        self.assertTrue(expected_managed_policy, self.function_policies._get_type(managed_policy))
        self.assertTrue(expected_managed_policy, self.function_policies._get_type(no_value_if))
        self.assertTrue(expected_managed_policy, self.function_policies._get_type(no_value_else))

    def test_get_type_with_intrinsic_if_must_return_policy_statement_type(self):
        policy_statement = {
            "Fn::If": ["SomeCondition", {"Statement": "then statement"}, {"Statement": "else statement"}]
        }

        no_value_if = {"Fn::If": ["SomeCondition", {"Ref": "AWS::NoValue"}, {"Statement": "else statement"}]}

        no_value_else = {"Fn::If": ["SomeCondition", {"Statement": "then statement"}, {"Ref": "AWS::NoValue"}]}
        expected_managed_policy = PolicyTypes.POLICY_STATEMENT

        self.assertTrue(expected_managed_policy, self.function_policies._get_type(policy_statement))
        self.assertTrue(expected_managed_policy, self.function_policies._get_type(no_value_if))
        self.assertTrue(expected_managed_policy, self.function_policies._get_type(no_value_else))

    def test_get_type_with_intrinsic_if_must_return_policy_template_type(self):
        policy_template = {
            "Fn::If": [
                "SomeCondition",
                {"template_name_one": {"Param1": "foo"}},
                {"template_name_one": {"Param1": "foo"}},
            ]
        }
        no_value_if = {"Fn::If": ["SomeCondition", {"Ref": "AWS::NoValue"}, {"template_name_one": {"Param1": "foo"}}]}
        no_value_else = {"Fn::If": ["SomeCondition", {"template_name_one": {"Param1": "foo"}}, {"Ref": "AWS::NoValue"}]}

        expected_managed_policy = PolicyTypes.POLICY_TEMPLATE
        self.policy_template_processor_mock.has.return_value = True
        function_policies = ResourcePolicies({}, self.policy_template_processor_mock)

        self.assertTrue(expected_managed_policy, function_policies._get_type(policy_template))
        self.assertTrue(expected_managed_policy, function_policies._get_type(no_value_if))
        self.assertTrue(expected_managed_policy, function_policies._get_type(no_value_else))

    def test_get_type_with_intrinsic_if_must_raise_exception_for_bad_policy(self):
        policy_too_few_values = {"Fn::If": ["condition", "then"]}

        policy_too_many_values = {"Fn::If": ["condition", "then", "else", "extra"]}

        self.assertRaises(InvalidTemplateException, self.function_policies._get_type, policy_too_few_values)
        self.assertRaises(InvalidTemplateException, self.function_policies._get_type, policy_too_many_values)

    def test_get_type_with_intrinsic_if_must_raise_exception_for_different_policy_types(self):
        policy_one = {"Fn::If": ["condition", "then", {"Statement": "else"}]}
        policy_two = {"Fn::If": ["condition", {"Statement": "then"}, "else"]}

        self.assertRaises(InvalidTemplateException, self.function_policies._get_type, policy_one)
        self.assertRaises(InvalidTemplateException, self.function_policies._get_type, policy_two)
