import datetime as dt
from collections import OrderedDict

import pytest

from marshmallow import EXCLUDE, Schema, fields
from marshmallow.warnings import RemovedInMarshmallow4Warning
from tests.base import User


class TestUnordered:
    class UnorderedSchema(Schema):
        name = fields.Str()
        email = fields.Str()

        class Meta:
            ordered = False

    def test_unordered_dump_returns_dict(self):
        schema = self.UnorderedSchema()
        u = User("steve", email="steve@steve.steve")
        result = schema.dump(u)
        assert not isinstance(result, OrderedDict)
        assert type(result) is dict

    def test_unordered_load_returns_dict(self):
        schema = self.UnorderedSchema()
        result = schema.load({"name": "steve", "email": "steve@steve.steve"})
        assert not isinstance(result, OrderedDict)
        assert type(result) is dict


class KeepOrder(Schema):
    class Meta:
        ordered = True

    name = fields.String(allow_none=True)
    email = fields.Email(allow_none=True)
    age = fields.Integer()
    created = fields.DateTime()
    id = fields.Integer(allow_none=True)
    homepage = fields.Url()
    birthdate = fields.Date()


class OrderedMetaSchema(Schema):
    id = fields.Int(allow_none=True)
    email = fields.Email(allow_none=True)

    class Meta:
        fields = ("name", "email", "age", "created", "id", "homepage", "birthdate")
        ordered = True


class OrderedNestedOnly(Schema):
    class Meta:
        ordered = True

    user = fields.Nested(KeepOrder)


class TestFieldOrdering:
    def test_ordered_option_is_deprecate(self):
        with pytest.warns(RemovedInMarshmallow4Warning):

            class MySchema(Schema):
                class Meta:
                    ordered = True

        with pytest.warns(RemovedInMarshmallow4Warning):

            class MySchema(Schema):
                class Meta:
                    ordered = False

    @pytest.mark.parametrize("with_meta", (False, True))
    def test_ordered_option_is_inherited(self, user, with_meta):
        class ParentUnordered(Schema):
            class Meta:
                ordered = False

        # KeepOrder is before ParentUnordered in MRO,
        # so ChildOrderedSchema will be ordered
        class ChildOrderedSchema(KeepOrder, ParentUnordered):
            if with_meta:

                class Meta:
                    pass

        schema = ChildOrderedSchema()
        assert schema.opts.ordered is True
        assert schema.dict_class == OrderedDict

        data = schema.dump(user)
        keys = list(data)
        assert keys == [
            "name",
            "email",
            "age",
            "created",
            "id",
            "homepage",
            "birthdate",
        ]

        # KeepOrder is before ParentUnordered in MRO,
        # so ChildOrderedSchema will be ordered
        class ChildUnorderedSchema(ParentUnordered, KeepOrder):
            class Meta:
                pass

        schema = ChildUnorderedSchema()
        assert schema.opts.ordered is False

    def test_ordering_is_off_by_default(self):
        class DummySchema(Schema):
            pass

        schema = DummySchema()
        assert schema.ordered is False

    def test_declared_field_order_is_maintained_on_dump(self, user):
        ser = KeepOrder()
        data = ser.dump(user)
        keys = list(data)
        assert keys == [
            "name",
            "email",
            "age",
            "created",
            "id",
            "homepage",
            "birthdate",
        ]

    def test_declared_field_order_is_maintained_on_load(self, serialized_user):
        schema = KeepOrder(unknown=EXCLUDE)
        data = schema.load(serialized_user)
        keys = list(data)
        assert keys == [
            "name",
            "email",
            "age",
            "created",
            "id",
            "homepage",
            "birthdate",
        ]

    def test_nested_field_order_with_only_arg_is_maintained_on_dump(self, user):
        schema = OrderedNestedOnly()
        data = schema.dump({"user": user})
        user_data = data["user"]
        keys = list(user_data)
        assert keys == [
            "name",
            "email",
            "age",
            "created",
            "id",
            "homepage",
            "birthdate",
        ]

    def test_nested_field_order_with_only_arg_is_maintained_on_load(self):
        schema = OrderedNestedOnly()
        data = schema.load(
            {
                "user": {
                    "name": "Foo",
                    "email": "Foo@bar.com",
                    "age": 42,
                    "created": dt.datetime.now().isoformat(),
                    "id": 123,
                    "homepage": "http://foo.com",
                    "birthdate": dt.datetime.now().date().isoformat(),
                }
            }
        )
        user_data = data["user"]
        keys = list(user_data)
        assert keys == [
            "name",
            "email",
            "age",
            "created",
            "id",
            "homepage",
            "birthdate",
        ]

    def test_nested_field_order_with_exclude_arg_is_maintained(self, user):
        class HasNestedExclude(Schema):
            user = fields.Nested(KeepOrder, exclude=("birthdate",))

        ser = HasNestedExclude()
        data = ser.dump({"user": user})
        user_data = data["user"]
        keys = list(user_data)
        assert keys == ["name", "email", "age", "created", "id", "homepage"]

    def test_meta_fields_order_is_maintained_on_dump(self, user):
        ser = OrderedMetaSchema()
        data = ser.dump(user)
        keys = list(data)
        assert keys == [
            "name",
            "email",
            "age",
            "created",
            "id",
            "homepage",
            "birthdate",
        ]

    def test_meta_fields_order_is_maintained_on_load(self, serialized_user):
        schema = OrderedMetaSchema(unknown=EXCLUDE)
        data = schema.load(serialized_user)
        keys = list(data)
        assert keys == [
            "name",
            "email",
            "age",
            "created",
            "id",
            "homepage",
            "birthdate",
        ]


class TestIncludeOption:
    class AddFieldsSchema(Schema):
        name = fields.Str()

        class Meta:
            include = {"from": fields.Str()}

    def test_fields_are_added(self):
        s = self.AddFieldsSchema()
        in_data = {"name": "Steve", "from": "Oskosh"}
        result = s.load({"name": "Steve", "from": "Oskosh"})
        assert result == in_data

    def test_included_fields_ordered_after_declared_fields(self):
        class AddFieldsOrdered(Schema):
            name = fields.Str()
            email = fields.Str()

            class Meta:
                include = {
                    "from": fields.Str(),
                    "in": fields.Str(),
                    "@at": fields.Str(),
                }

        s = AddFieldsOrdered()
        in_data = {
            "name": "Steve",
            "from": "Oskosh",
            "email": "steve@steve.steve",
            "in": "VA",
            "@at": "Charlottesville",
        }
        # declared fields, then "included" fields
        expected_fields = ["name", "email", "from", "in", "@at"]
        assert list(AddFieldsOrdered._declared_fields.keys()) == expected_fields

        result = s.load(in_data)
        assert list(result.keys()) == expected_fields

    def test_added_fields_are_inherited(self):
        class AddFieldsChild(self.AddFieldsSchema):
            email = fields.Str()

        s = AddFieldsChild()
        assert "email" in s._declared_fields
        assert "from" in s._declared_fields
        assert isinstance(s._declared_fields["from"], fields.Str)


class TestManyOption:
    class ManySchema(Schema):
        foo = fields.Str()

        class Meta:
            many = True

    def test_many_by_default(self):
        test = self.ManySchema()
        assert test.load([{"foo": "bar"}]) == [{"foo": "bar"}]

    def test_explicit_single(self):
        test = self.ManySchema(many=False)
        assert test.load({"foo": "bar"}) == {"foo": "bar"}
