# -*- coding: utf-8 -*-
#
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights
# Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License.
# A copy of the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
# OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the
# License.
#
import unittest
import datetime
import decimal

from six import text_type
from mock import patch

from ask_sdk_model_runtime import DefaultSerializer, SerializationException

from . import data

try:
    import mock
except ImportError:
    from unittest import mock

try:
    long_type = long
except NameError:
    long_type = int

unicode_type = text_type


class TestSerialization(unittest.TestCase):
    def setUp(self):
        self.test_serializer = DefaultSerializer()

    def test_none_obj_serialization(self):
        test_obj = None
        self.assertIsNone(self.test_serializer.serialize(test_obj), \
            "Default Serializer serialized None object incorrectly")

    def test_primitive_obj_serialization(self):
        test_obj = "test"
        self.assertEqual(self.test_serializer.serialize(test_obj), test_obj, \
            "Default Serializer serialized str object incorrectly")

        test_obj = 123
        self.assertEqual(self.test_serializer.serialize(test_obj), test_obj, \
            "Default Serializer serialized int object incorrectly")

        test_obj = u"test"
        self.assertEqual(self.test_serializer.serialize(test_obj), test_obj, \
            "Default Serializer serialized unicode object incorrectly")

        test_obj = b"test"
        self.assertEqual(self.test_serializer.serialize(test_obj), test_obj, \
            "Default Serializer serialized bytes object incorrectly")

        test_obj = False
        self.assertEqual(self.test_serializer.serialize(test_obj), test_obj, \
            "Default Serializer serialized bool object incorrectly")

    def test_list_obj_serialization(self):
        test_obj_inst = data.ModelTestObject2(int_var=123)
        test_list_obj = ["test", 123, test_obj_inst]

        expected_list = ["test", 123, {"var4Int": 123}]
        self.assertEqual(self.test_serializer.serialize(test_list_obj), expected_list, \
            "Default Serializer serialized list object incorrectly")

    def test_tuple_obj_serialization(self):
        test_obj_inst = data.ModelTestObject2(int_var=123)
        test_tuple_obj = ("test", 123, test_obj_inst)

        expected_tuple = ("test", 123, {"var4Int": 123})
        self.assertEqual(self.test_serializer.serialize(test_tuple_obj), expected_tuple, \
            "Default Serializer serialized tuple object incorrectly")

    def test_datetime_obj_serialization(self):
        test_obj = datetime.datetime(2018, 1, 1, 10, 20, 30)
        expected_datetime = "2018-01-01T10:20:30"
        self.assertEqual(self.test_serializer.serialize(test_obj), expected_datetime, \
            "Default Serializer serialized datetime object incorrectly")

    def test_date_obj_serialization(self):
        test_obj = datetime.date(2018, 1, 1)
        expected_date = "2018-01-01"
        self.assertEqual(self.test_serializer.serialize(test_obj), expected_date, \
            "Default Serializer serialized datetime object incorrectly")

    def test_dict_obj_serialization(self):
        test_obj_inst = data.ModelTestObject2(int_var=123)
        test_dict_obj = {
            "test_str": "test",
            "test_int": 123,
            "test_obj": test_obj_inst
        }

        expected_dict = {
            "test_str": "test",
            "test_obj": {
                "var4Int": 123
            },
            "test_int": 123,
        }
        self.assertEqual(self.test_serializer.serialize(test_dict_obj), expected_dict, \
            "Default Serializer serialized dict object incorrectly")

    def test_model_obj_serialization(self):
        test_model_obj_2 = data.ModelTestObject2(int_var=123)
        test_model_obj_1 = data.ModelTestObject1(
            str_var="test", datetime_var=datetime.datetime(
                2018, 1, 1, 10, 20, 30), obj_var=test_model_obj_2)

        expected_serialized_obj = {
            "var1": "test",
            "var2Time": "2018-01-01T10:20:30",
            "var3Object": {
                "var4Int": 123
            }
        }
        self.assertEqual(self.test_serializer.serialize(test_model_obj_1), expected_serialized_obj, \
            "Default Serializer serialized model object incorrectly")

    def test_model_obj_without_attrmap_serialization(self):
        test_obj_inst = data.ModelTestObject3(str_var="test", int_var=123)
        expected_dict = {
            "str_var": "test",
            "int_var": 123
        }
        self.assertEqual(self.test_serializer.serialize(test_obj_inst), expected_dict, \
            "Default Serializer serialized object without attribute_map incorrectly")

    def test_model_obj_with_incomplete_attrmap_serialization(self):
        test_obj_inst = data.ModelTestObject4(str_var="test", float_var=3.14)
        expected_dict = {
            "str_var": "test",
            "floatingValue": 3.14
        }
        self.assertEqual(self.test_serializer.serialize(test_obj_inst), expected_dict, \
            "Default Serializer serialized object with incomplete attribute map incorrectly")

    def test_enum_obj_serialization(self):
        test_model_obj_2 = data.ModelTestObject2(int_var=123)
        test_enum_obj = data.ModelEnumObject("ENUM_VAL_1")
        test_model_obj_1 = data.ModelTestObject1(
            str_var="test", datetime_var=datetime.datetime(
                2018, 1, 1, 10, 20, 30), obj_var=test_model_obj_2,
            enum_var=test_enum_obj)

        expected_serialized_obj = {
            "var1": "test",
            "var2Time": "2018-01-01T10:20:30",
            "var6Enum": "ENUM_VAL_1",
            "var3Object": {
                "var4Int": 123
            }
        }
        self.assertEqual(self.test_serializer.serialize(test_model_obj_1), expected_serialized_obj, \
            "Default Serializer serialized enum object incorrectly")

    def test_decimal_obj_without_decimals_serialization(self):
        test_decimal_obj = decimal.Decimal(10)
        expected_obj = 10
        actual_obj = self.test_serializer.serialize(test_decimal_obj)

        self.assertEqual(actual_obj, expected_obj, (
            "Default Serializer serialized decimal object containing no "
            "decimals incorrectly"))
        self.assertEqual(type(actual_obj), int, (
            "Default Serializer serialized decimal object containing no "
            "decimals to incorrect type"))

    def test_decimal_obj_with_decimals_serialization(self):
        test_decimal_obj = decimal.Decimal(10.5)
        expected_obj = 10.5
        actual_obj = self.test_serializer.serialize(test_decimal_obj)

        self.assertEqual(actual_obj, expected_obj, (
            "Default Serializer serialized decimal object containing "
            "decimals incorrectly"))
        self.assertEqual(type(actual_obj), float, (
            "Default Serializer serialized decimal object containing "
            "decimals to incorrect type"))


class TestDeserialization(unittest.TestCase):
    def setUp(self):
        self.test_serializer = DefaultSerializer()

    def test_none_obj_deserialization(self):
        test_payload = None
        test_obj_type = str
        self.assertIsNone(self.test_serializer.deserialize(
            test_payload, test_obj_type), "Default "
            "Serializer deserialized None object incorrectly")

    def test_str_obj_deserialization(self):
        test_payload = "test"
        test_obj_type = str
        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), test_payload, \
                "Default Serializer deserialized string object incorrectly")

    def test_unicode_obj_deserialization(self):
        test_payload = u"√"
        test_obj_type = unicode_type
        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), u"\u221a", \
                "Default Serializer deserialized unicode string object incorrectly")

    def test_int_obj_deserialization(self):
        test_payload = 123
        test_obj_type = int
        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), test_payload, \
                "Default Serializer deserialized int object incorrectly")

    def test_long_obj_deserialization(self):
        test_payload = 123
        test_obj_type = long_type
        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), long_type(test_payload), \
                "Default Serializer deserialized long object incorrectly")

    def test_primitive_obj_deserialization_raising_unicode_exception(self):
        test_serializer = DefaultSerializer()
        mocked_primitive_type = mock.Mock(
            side_effect=UnicodeEncodeError('hitchhiker', u"", 42, 43, 'the universe and everything else'))

        test_serializer.PRIMITIVE_TYPES = [mocked_primitive_type]

        test_payload = u"√"
        test_obj_type = mocked_primitive_type
        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(test_serializer.deserialize(
                test_payload, test_obj_type), u"\u221a", \
                "Default Serializer deserialized primitive type which raises UnicodeEncodeError incorrectly")

    def test_primitive_obj_deserialization_raising_type_error(self):
        test_serializer = DefaultSerializer()
        mocked_primitive_type = mock.Mock(side_effect=TypeError())

        test_serializer.PRIMITIVE_TYPES = [mocked_primitive_type]

        test_payload = "test"
        test_obj_type = mocked_primitive_type
        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(test_serializer.deserialize(
                test_payload, test_obj_type), test_payload, \
                "Default Serializer deserialized primitive type which raises TypeError incorrectly")

    def test_primitive_obj_deserialization_raising_value_error(self):
        test_payload = "test"
        test_obj_type = int

        with self.assertRaises(SerializationException) as exc:
            with patch("json.loads") as mock_json_loader:
                mock_json_loader.return_value = test_payload
                self.test_serializer.deserialize(test_payload, test_obj_type)

        self.assertIn("Failed to parse test into 'int' object", str(exc.exception), \
            "Default Serializer didn't throw SerializationException when invalid primitive type is deserialized")

    def test_datetime_obj_serialization(self):
        # payload in iso8601 format
        test_payload = "2018-01-01T10:20:30"
        test_obj_type = datetime.datetime

        expected_obj = datetime.datetime(2018, 1, 1, 10, 20, 30)
        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(test_payload, test_obj_type), expected_obj, \
                "Default Serializer deserialized datetime object incorrectly")

    def test_date_obj_serialization(self):
        # payload in iso8601 format
        test_payload = "2018-01-01"
        test_obj_type = datetime.date

        expected_obj = datetime.date(2018, 1, 1)
        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(test_payload, test_obj_type), expected_obj, \
                "Default Serializer deserialized date object incorrectly")

    def test_datetime_obj_deserialization_raising_value_error(self):
        test_payload = "abc-wx-yzT25:80:90"
        test_obj_type = datetime.datetime

        with self.assertRaises(SerializationException) as exc:
            with patch("json.loads") as mock_json_loader:
                mock_json_loader.return_value = test_payload
                self.test_serializer.deserialize(test_payload, test_obj_type)

        self.assertIn("Failed to parse abc-wx-yzT25:80:90 into 'datetime' object", str(exc.exception), \
            "Default Serializer didn't throw SerializationException when invalid datetime type is deserialized")

    def test_datetime_obj_deserialization_raising_import_error(self):
        test_payload = "abc-wx-yzT25:80:90"
        test_obj_type = datetime.datetime

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            with mock.patch('dateutil.parser.parse') as parse_class:
                parse_class.side_effect = ImportError
                self.assertEqual(self.test_serializer.deserialize(
                    test_payload, test_obj_type), test_payload, \
                    "Default Serializer didn't return datetime correctly for import errors")
                parse_class.assert_called_once_with(test_payload)

    def test_obj_type_deserialization(self):
        test_payload = "test"
        test_obj_type = object

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), test_payload, \
                "Default Serializer deserialization of object returned other than the object itself")

    def test_native_type_mapping_deserialization(self):
        test_payload = "test"
        test_obj_type = "str"

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), test_payload, (
                "Default Serializer deserialization of object with object_type of string class under native mapping "
                "not deserialized correctly"))

    def test_polymorphic_list_obj_deserialization(self):
        test_payload = ["test", 123, "2018-01-01T10:20:30"]
        test_obj_type = "list[str, long, datetime]"

        deserialized_datetime_obj = datetime.datetime(2018, 1, 1, 10, 20, 30)
        expected_obj = ["test", 123, deserialized_datetime_obj]

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), expected_obj, (
                "Default Serializer deserialized list containing poly type object incorrectly"))

    def test_similar_list_obj_deserialization(self):
        test_payload = ["test", "test1", "2018-01-01T10:20:30"]
        test_obj_type = "list[str]"
        expected_obj = ["test", "test1", "2018-01-01T10:20:30"]

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), expected_obj, (
                "Default Serializer deserialized list object containing similar type objects incorrectly"))

    def test_dict_obj_deserialization(self):
        test_payload = {
            "test_key": ["test_val_1", "test_val_2"],
            "test_date_str": ["2018-01-01T10:20:30"]
        }
        test_obj_type = "dict(str, list[str])"

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), test_payload, (
                "Default Serializer deserialized dict object incorrectly"))

    def test_model_obj_deserialization(self):
        test_payload = {
            "var1": "test",
            "var2Time": "2018-01-01T10:20:30",
            "var3Object": {
                "var4Int": 123
            },
            "var6Enum": "ENUM_VAL_1"
        }
        test_obj_type = data.ModelTestObject1
        expected_datetime_obj = datetime.datetime(2018, 1, 1, 10, 20, 30)
        expected_sub_obj = data.ModelTestObject2(int_var=123)
        expected_enum_obj = data.ModelEnumObject("ENUM_VAL_1")
        expected_obj = data.ModelTestObject1(
            str_var="test", datetime_var=expected_datetime_obj, obj_var=expected_sub_obj, enum_var=expected_enum_obj)

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), expected_obj, (
                "Default Serializer deserialized model object incorrectly"))

    def test_model_obj_with_additional_params_in_payload_deserialization(self):
        test_payload = {
            "var4Int": 123,
            "add_param_1": "Test"
        }
        test_obj_type = data.ModelTestObject2
        expected_obj = data.ModelTestObject2(int_var=123)
        expected_obj.add_param_1 = "Test"

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), expected_obj, (
                "Default Serializer deserialized model object incorrectly when payload has additional parameters"))

    def test_model_obj_without_attrmap_deserialization(self):
        test_payload = {
            "str_var": "Test",
            "int_var": 123
        }
        test_obj_type = data.ModelTestObject3
        expected_obj = data.ModelTestObject3(str_var="Test", int_var=123)

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), expected_obj, (
                "Default Serializer deserialized model object without attribute map incorrectly"))

    def test_model_obj_with_incomplete_attrmap_deserialization(self):
        test_payload = {
            "str_var": "Test",
            "floatingValue": 3.14
        }
        test_obj_type = data.ModelTestObject4
        expected_obj = data.ModelTestObject4(str_var="Test", float_var=3.14)

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), expected_obj, (
                "Default Serializer deserialized model object with incomplete attribute map incorrectly"))

    def test_invalid_model_obj_deserialization(self):
        test_payload = {
            "var_1": "some value"
        }
        test_obj_type = data.InvalidModelObject

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), test_payload, (
                "Default Serializer didn't provide payload back when an invalid model object type "
                "(without attribute map and swagger type dict) is passed"))

    def test_invalid_model_obj_type_deserialization(self):
        test_payload = {
            "var_1": "some value"
        }
        test_obj_type = "InvalidModelObject"

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            with self.assertRaises(SerializationException) as exc:
                self.test_serializer.deserialize(test_payload, test_obj_type)

            self.assertIn("Unable to resolve class {} from installed modules".format(test_obj_type), str(exc.exception), (
                "Default Serializer didn't throw SerializationException when deserialization is called with invalid "
                "object type"))

    def test_invalid_json_deserialization(self):
        test_payload = {
            "var_1": "some value"
        }
        test_obj_type = "str"

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.side_effect = Exception
            with self.assertRaises(SerializationException) as exc:
                self.test_serializer.deserialize(test_payload, test_obj_type)

        self.assertIn("Couldn't parse response body", str(exc.exception), \
            "Default Serializer didn't throw SerializationException when invalid json is deserialized")

    def test_parent_model_obj_with_discriminator_deserialization(self):
        test_payload = {
            "ChildType": 'ChildType1',
            "var1": "Some string",
            "var3Object": {
                "var4Int": 123
            },
            "testVar": "test string"
        }
        test_obj_type = data.ModelAbstractParentObject
        expected_sub_obj = data.ModelTestObject2(int_var=123)
        expected_obj = data.ModelChildObject1(
            str_var="Some string", obj_var=expected_sub_obj, test_var="test string")

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), expected_obj, (
                "Default Serializer deserialized model object incorrectly when object type is parent class "
                "with discriminator"))

    def test_child_discriminator_model_obj_deserialization(self):
        test_payload = {
            "ChildType": 'ChildType2',
            "var1": "Some string",
            "var3Object": {
                "var4Int": 123
            },
            "testIntVar": 456
        }
        test_obj_type = data.ModelChildObject2
        expected_sub_obj = data.ModelTestObject2(int_var=123)
        expected_obj = data.ModelChildObject2(
            str_var="Some string", obj_var=expected_sub_obj, test_int_var=456)

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            self.assertEqual(self.test_serializer.deserialize(
                test_payload, test_obj_type), expected_obj, (
                "Default Serializer deserialized model object incorrectly when object type is parent class "
                "with discriminator"))

    def test_parent_model_obj_with_invalid_discriminator_deserialization(self):
        test_payload = {
            "ChildType": 'InvalidType',
            "var1": "Some string",
            "var3Object": {
                "var4Int": 123
            },
            "testVar": "test string"
        }
        test_obj_type = data.ModelAbstractParentObject

        with patch("json.loads") as mock_json_loader:
            mock_json_loader.return_value = test_payload
            with self.assertRaises(SerializationException) as exc:
                self.test_serializer.deserialize(test_payload, test_obj_type)

            self.assertIn("Couldn't resolve object by discriminator type", str(exc.exception), (
                "Default Serializer didn't throw SerializationException when deserialization is called with invalid "
                "discriminator type in payload and parent model"))
