import copy
import pytest

from collections import OrderedDict

from flask_restx import fields, Model, OrderedModel, SchemaModel


class ModelTest(object):
    def test_model_as_flat_dict(self):
        model = Model(
            "Person",
            {
                "name": fields.String,
                "age": fields.Integer,
                "birthdate": fields.DateTime,
            },
        )

        assert isinstance(model, dict)
        assert not isinstance(model, OrderedDict)

        assert model.__schema__ == {
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
            },
            "type": "object",
        }

    def test_model_as_ordered_dict(self):
        model = OrderedModel(
            "Person",
            [
                ("name", fields.String),
                ("age", fields.Integer),
                ("birthdate", fields.DateTime),
            ],
        )

        assert isinstance(model, OrderedDict)

        assert model.__schema__ == {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
            },
        }

    def test_model_as_nested_dict(self):
        address = Model(
            "Address",
            {
                "road": fields.String,
            },
        )

        person = Model(
            "Person",
            {
                "name": fields.String,
                "age": fields.Integer,
                "birthdate": fields.DateTime,
                "address": fields.Nested(address),
            },
        )

        assert person.__schema__ == {
            # 'required': ['address'],
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
                "address": {
                    "$ref": "#/definitions/Address",
                },
            },
            "type": "object",
        }

        assert address.__schema__ == {
            "properties": {
                "road": {"type": "string"},
            },
            "type": "object",
        }

    def test_model_as_dict_with_list(self):
        model = Model(
            "Person",
            {
                "name": fields.String,
                "age": fields.Integer,
                "tags": fields.List(fields.String),
            },
        )

        assert model.__schema__ == {
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "tags": {"type": "array", "items": {"type": "string"}},
            },
            "type": "object",
        }

    def test_model_as_nested_dict_with_list(self):
        address = Model(
            "Address",
            {
                "road": fields.String,
            },
        )

        person = Model(
            "Person",
            {
                "name": fields.String,
                "age": fields.Integer,
                "birthdate": fields.DateTime,
                "addresses": fields.List(fields.Nested(address)),
            },
        )

        assert person.__schema__ == {
            # 'required': ['address'],
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
                "addresses": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/Address",
                    },
                },
            },
            "type": "object",
        }

        assert address.__schema__ == {
            "properties": {
                "road": {"type": "string"},
            },
            "type": "object",
        }

    def test_model_with_required(self):
        model = Model(
            "Person",
            {
                "name": fields.String(required=True),
                "age": fields.Integer,
                "birthdate": fields.DateTime(required=True),
            },
        )

        assert model.__schema__ == {
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
            },
            "required": ["birthdate", "name"],
            "type": "object",
        }

    def test_model_as_nested_dict_and_required(self):
        address = Model(
            "Address",
            {
                "road": fields.String,
            },
        )

        person = Model(
            "Person",
            {
                "name": fields.String,
                "age": fields.Integer,
                "birthdate": fields.DateTime,
                "address": fields.Nested(address, required=True),
            },
        )

        assert person.__schema__ == {
            "required": ["address"],
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
                "address": {
                    "$ref": "#/definitions/Address",
                },
            },
            "type": "object",
        }

        assert address.__schema__ == {
            "properties": {
                "road": {"type": "string"},
            },
            "type": "object",
        }

    def test_model_with_discriminator(self):
        model = Model(
            "Person",
            {
                "name": fields.String(discriminator=True),
                "age": fields.Integer,
            },
        )

        assert model.__schema__ == {
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
            },
            "discriminator": "name",
            "required": ["name"],
            "type": "object",
        }

    def test_model_with_discriminator_override_require(self):
        model = Model(
            "Person",
            {
                "name": fields.String(discriminator=True, required=False),
                "age": fields.Integer,
            },
        )

        assert model.__schema__ == {
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
            },
            "discriminator": "name",
            "required": ["name"],
            "type": "object",
        }

    def test_model_deepcopy(self):
        parent = Model(
            "Person",
            {
                "name": fields.String,
                "age": fields.Integer(description="foo"),
            },
        )

        child = parent.inherit(
            "Child",
            {
                "extra": fields.String,
            },
        )

        parent_copy = copy.deepcopy(parent)

        assert parent_copy["age"].description == "foo"

        parent_copy["age"].description = "bar"

        assert parent["age"].description == "foo"
        assert parent_copy["age"].description == "bar"

        child = parent.inherit(
            "Child",
            {
                "extra": fields.String,
            },
        )

        child_copy = copy.deepcopy(child)
        assert child_copy.__parents__[0] == parent

    def test_clone_from_instance(self):
        parent = Model(
            "Parent",
            {
                "name": fields.String,
                "age": fields.Integer,
                "birthdate": fields.DateTime,
            },
        )

        child = parent.clone(
            "Child",
            {
                "extra": fields.String,
            },
        )

        assert child.__schema__ == {
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
                "extra": {"type": "string"},
            },
            "type": "object",
        }

    def test_clone_from_class(self):
        parent = Model(
            "Parent",
            {
                "name": fields.String,
                "age": fields.Integer,
                "birthdate": fields.DateTime,
            },
        )

        child = Model.clone(
            "Child",
            parent,
            {
                "extra": fields.String,
            },
        )

        assert child.__schema__ == {
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
                "extra": {"type": "string"},
            },
            "type": "object",
        }

    def test_clone_from_instance_with_multiple_parents(self):
        grand_parent = Model(
            "GrandParent",
            {
                "grand_parent": fields.String,
            },
        )

        parent = Model(
            "Parent",
            {
                "name": fields.String,
                "age": fields.Integer,
                "birthdate": fields.DateTime,
            },
        )

        child = grand_parent.clone(
            "Child",
            parent,
            {
                "extra": fields.String,
            },
        )

        assert child.__schema__ == {
            "properties": {
                "grand_parent": {"type": "string"},
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
                "extra": {"type": "string"},
            },
            "type": "object",
        }

    def test_clone_from_class_with_multiple_parents(self):
        grand_parent = Model(
            "GrandParent",
            {
                "grand_parent": fields.String,
            },
        )

        parent = Model(
            "Parent",
            {
                "name": fields.String,
                "age": fields.Integer,
                "birthdate": fields.DateTime,
            },
        )

        child = Model.clone(
            "Child",
            grand_parent,
            parent,
            {
                "extra": fields.String,
            },
        )

        assert child.__schema__ == {
            "properties": {
                "grand_parent": {"type": "string"},
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
                "extra": {"type": "string"},
            },
            "type": "object",
        }

    def test_inherit_from_instance(self):
        parent = Model(
            "Parent",
            {
                "name": fields.String,
                "age": fields.Integer,
            },
        )

        child = parent.inherit(
            "Child",
            {
                "extra": fields.String,
            },
        )

        assert parent.__schema__ == {
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
            },
            "type": "object",
        }
        assert child.__schema__ == {
            "allOf": [
                {"$ref": "#/definitions/Parent"},
                {"properties": {"extra": {"type": "string"}}, "type": "object"},
            ]
        }

    def test_inherit_from_class(self):
        parent = Model(
            "Parent",
            {
                "name": fields.String,
                "age": fields.Integer,
            },
        )

        child = Model.inherit(
            "Child",
            parent,
            {
                "extra": fields.String,
            },
        )

        assert parent.__schema__ == {
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
            },
            "type": "object",
        }
        assert child.__schema__ == {
            "allOf": [
                {"$ref": "#/definitions/Parent"},
                {"properties": {"extra": {"type": "string"}}, "type": "object"},
            ]
        }

    def test_inherit_from_class_from_multiple_parents(self):
        grand_parent = Model(
            "GrandParent",
            {
                "grand_parent": fields.String,
            },
        )

        parent = Model(
            "Parent",
            {
                "name": fields.String,
                "age": fields.Integer,
            },
        )

        child = Model.inherit(
            "Child",
            grand_parent,
            parent,
            {
                "extra": fields.String,
            },
        )

        assert child.__schema__ == {
            "allOf": [
                {"$ref": "#/definitions/GrandParent"},
                {"$ref": "#/definitions/Parent"},
                {"properties": {"extra": {"type": "string"}}, "type": "object"},
            ]
        }

    def test_inherit_from_instance_from_multiple_parents(self):
        grand_parent = Model(
            "GrandParent",
            {
                "grand_parent": fields.String,
            },
        )

        parent = Model(
            "Parent",
            {
                "name": fields.String,
                "age": fields.Integer,
            },
        )

        child = grand_parent.inherit(
            "Child",
            parent,
            {
                "extra": fields.String,
            },
        )

        assert child.__schema__ == {
            "allOf": [
                {"$ref": "#/definitions/GrandParent"},
                {"$ref": "#/definitions/Parent"},
                {"properties": {"extra": {"type": "string"}}, "type": "object"},
            ]
        }

    # def test_inherit_inline(self):
    #     parent = Model('Person', {
    #         'name': fields.String,
    #         'age': fields.Integer,
    #     })
    #
    #     child = self.api.inherit('Child', parent, {
    #         'extra': fields.String,
    #     })
    #
    #     Model('Output', {
    #         'child': fields.Nested(child),
    #         'children': fields.List(fields.Nested(child))
    #     })
    #
    #     self.assertIn('Person', Models)
    #     self.assertIn('Child', Models)

    def test_polymorph_inherit_common_ancestor(self):
        class Child1:
            pass

        class Child2:
            pass

        parent = Model(
            "Person",
            {
                "name": fields.String,
                "age": fields.Integer,
            },
        )

        child1 = parent.inherit(
            "Child1",
            {
                "extra1": fields.String,
            },
        )

        child2 = parent.inherit(
            "Child2",
            {
                "extra2": fields.String,
            },
        )

        mapping = {
            Child1: child1,
            Child2: child2,
        }

        output = Model("Output", {"child": fields.Polymorph(mapping)})

        # Should use the common ancestor
        assert output.__schema__ == {
            "properties": {
                "child": {"$ref": "#/definitions/Person"},
            },
            "type": "object",
        }

    def test_validate(self):
        from jsonschema import FormatChecker
        from werkzeug.exceptions import BadRequest

        class IPAddress(fields.Raw):
            __schema_type__ = "string"
            __schema_format__ = "ipv4"

        data = {"ip": "192.168.1"}
        model = Model("MyModel", {"ip": IPAddress()})

        # Test that validate without a FormatChecker does not check if a
        # primitive type conforms to the defined format property
        assert model.validate(data) is None

        # Test that validate with a FormatChecker enforces the check of the
        # format property and throws an error if invalid
        with pytest.raises(BadRequest):
            model.validate(data, format_checker=FormatChecker())


class ModelSchemaTestCase(object):
    def test_model_schema(self):
        address = SchemaModel(
            "Address",
            {
                "properties": {
                    "road": {"type": "string"},
                },
                "type": "object",
            },
        )

        person = SchemaModel(
            "Person",
            {
                # 'required': ['address'],
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "birthdate": {"type": "string", "format": "date-time"},
                    "address": {
                        "$ref": "#/definitions/Address",
                    },
                },
                "type": "object",
            },
        )

        assert person.__schema__ == {
            # 'required': ['address'],
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
                "address": {
                    "$ref": "#/definitions/Address",
                },
            },
            "type": "object",
        }

        assert address.__schema__ == {
            "properties": {
                "road": {"type": "string"},
            },
            "type": "object",
        }


class ModelDeprecattionsTest(object):
    def test_extend_is_deprecated(self):
        parent = Model(
            "Parent",
            {
                "name": fields.String,
                "age": fields.Integer,
                "birthdate": fields.DateTime,
            },
        )

        with pytest.warns(DeprecationWarning):
            child = parent.extend(
                "Child",
                {
                    "extra": fields.String,
                },
            )

        assert child.__schema__ == {
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "birthdate": {"type": "string", "format": "date-time"},
                "extra": {"type": "string"},
            },
            "type": "object",
        }
