import datetime as dt
import decimal
import math
import random
from collections import OrderedDict
from typing import NamedTuple

import pytest
import simplejson as json

from marshmallow import (
    EXCLUDE,
    INCLUDE,
    RAISE,
    Schema,
    class_registry,
    fields,
    utils,
    validates,
    validates_schema,
)
from marshmallow.exceptions import (
    RegistryError,
    StringNotCollectionError,
    ValidationError,
)
from marshmallow.warnings import (
    ChangedInMarshmallow4Warning,
    RemovedInMarshmallow4Warning,
)
from tests.base import (
    Blog,
    BlogOnlySchema,
    BlogSchema,
    BlogSchemaExclude,
    BlogSchemaMeta,
    BlogUserMetaSchema,
    ExtendedUserSchema,
    User,
    UserAdditionalSchema,
    UserExcludeSchema,
    UserFloatStringSchema,
    UserIntSchema,
    UserMetaSchema,
    UserRelativeUrlSchema,
    UserSchema,
    mockjson,
)

random.seed(1)


# Run tests with both verbose serializer and 'meta' option serializer
@pytest.mark.parametrize("SchemaClass", [UserSchema, UserMetaSchema])
def test_serializing_basic_object(SchemaClass, user):
    s = SchemaClass()
    data = s.dump(user)
    assert data["name"] == user.name
    assert math.isclose(data["age"], 42.3)
    assert data["registered"]


def test_serializer_dump(user):
    s = UserSchema()
    result = s.dump(user)
    assert result["name"] == user.name


def test_load_resets_errors():
    class MySchema(Schema):
        email = fields.Email()

    schema = MySchema()
    with pytest.raises(ValidationError) as excinfo:
        schema.load({"name": "Joe", "email": "notvalid"})
    errors = excinfo.value.messages
    assert len(errors["email"]) == 1
    assert "Not a valid email address." in errors["email"][0]
    with pytest.raises(ValidationError) as excinfo:
        schema.load({"name": "Joe", "email": "__invalid"})
    errors = excinfo.value.messages
    assert len(errors["email"]) == 1
    assert "Not a valid email address." in errors["email"][0]


def test_load_validation_error_stores_input_data_and_valid_data():
    class MySchema(Schema):
        always_valid = fields.DateTime()
        always_invalid = fields.Raw(validate=[lambda v: False])

    schema = MySchema()
    input_data = {
        "always_valid": dt.datetime.now(dt.timezone.utc).isoformat(),
        "always_invalid": 24,
    }
    with pytest.raises(ValidationError) as excinfo:
        schema.load(input_data)
    err = excinfo.value
    # err.data is the raw input data
    assert err.data == input_data
    assert "always_valid" in err.valid_data
    # err.valid_data contains valid, deserialized data
    assert isinstance(err.valid_data["always_valid"], dt.datetime)
    # excludes invalid data
    assert "always_invalid" not in err.valid_data


def test_load_resets_error_fields():
    class MySchema(Schema):
        email = fields.Email()
        name = fields.Str()

    schema = MySchema()
    with pytest.raises(ValidationError) as excinfo:
        schema.load({"name": "Joe", "email": "not-valid"})
    exc = excinfo.value
    assert len(exc.messages.keys()) == 1

    with pytest.raises(ValidationError) as excinfo:
        schema.load({"name": 12, "email": "mick@stones.com"})
    exc = excinfo.value


def test_errored_fields_do_not_appear_in_output():
    class MyField(fields.Field):
        # Make sure validation fails during serialization
        def _serialize(self, val, attr, obj):
            raise ValidationError("oops")

    class MySchema(Schema):
        foo = MyField(validate=lambda x: False)

    sch = MySchema()
    with pytest.raises(ValidationError) as excinfo:
        sch.load({"foo": 2})
    data, errors = excinfo.value.valid_data, excinfo.value.messages

    assert "foo" in errors
    assert "foo" not in data


def test_load_many_stores_error_indices():
    s = UserSchema()
    data = [
        {"name": "Mick", "email": "mick@stones.com"},
        {"name": "Keith", "email": "invalid-email", "homepage": "invalid-homepage"},
    ]
    with pytest.raises(ValidationError) as excinfo:
        s.load(data, many=True)
    errors = excinfo.value.messages
    assert 0 not in errors
    assert 1 in errors
    assert "email" in errors[1]
    assert "homepage" in errors[1]


def test_dump_many():
    s = UserSchema()
    u1, u2 = User("Mick"), User("Keith")
    data = s.dump([u1, u2], many=True)
    assert len(data) == 2
    assert data[0] == s.dump(u1)


@pytest.mark.parametrize("value", [[], {}, [1], {1: 1}])
def test_boolean_can_dump_unhashable(value):
    class MySchema(Schema):
        has_items = fields.Boolean()

    schema = MySchema()
    data = schema.dump({"has_items": value})
    assert data["has_items"] is bool(value)


def test_multiple_errors_can_be_stored_for_a_given_index():
    class MySchema(Schema):
        foo = fields.Str(validate=lambda x: len(x) > 3)
        bar = fields.Int(validate=lambda x: x > 3)

    sch = MySchema()
    valid = {"foo": "loll", "bar": 42}
    invalid = {"foo": "lol", "bar": 3}
    errors = sch.validate([valid, invalid], many=True)

    assert 1 in errors
    assert len(errors[1]) == 2
    assert "foo" in errors[1]
    assert "bar" in errors[1]


def test_dump_returns_a_dict(user):
    s = UserSchema()
    result = s.dump(user)
    assert type(result) is dict


def test_dumps_returns_a_string(user):
    s = UserSchema()
    result = s.dumps(user)
    assert type(result) is str


def test_dumping_single_object_with_collection_schema(user):
    s = UserSchema(many=True)
    result = s.dump(user, many=False)
    assert type(result) is dict
    assert result == UserSchema().dump(user)


def test_loading_single_object_with_collection_schema():
    s = UserSchema(many=True)
    in_data = {"name": "Mick", "email": "mick@stones.com"}
    result = s.load(in_data, many=False)
    assert type(result) is User
    assert result.name == UserSchema().load(in_data).name


def test_dumps_many():
    s = UserSchema()
    u1, u2 = User("Mick"), User("Keith")
    json_result = s.dumps([u1, u2], many=True)
    data = json.loads(json_result)
    assert len(data) == 2
    assert data[0] == s.dump(u1)


def test_load_returns_an_object():
    s = UserSchema()
    result = s.load({"name": "Monty"})
    assert type(result) is User


def test_load_many():
    s = UserSchema()
    in_data = [{"name": "Mick"}, {"name": "Keith"}]
    result = s.load(in_data, many=True)
    assert type(result) is list
    assert type(result[0]) is User
    assert result[0].name == "Mick"


@pytest.mark.parametrize("val", (None, False, 1, 1.2, object(), [], set(), "lol"))
def test_load_invalid_input_type(val):
    class Sch(Schema):
        name = fields.Str()

    with pytest.raises(ValidationError) as e:
        Sch().load(val)
    assert e.value.messages == {"_schema": ["Invalid input type."]}
    assert e.value.valid_data == {}


# regression test for https://github.com/marshmallow-code/marshmallow/issues/906
@pytest.mark.parametrize("val", (None, False, 1, 1.2, object(), {}, {"1": 2}, "lol"))
def test_load_many_invalid_input_type(val):
    class Sch(Schema):
        name = fields.Str()

    with pytest.raises(ValidationError) as e:
        Sch(many=True).load(val)
    assert e.value.messages == {"_schema": ["Invalid input type."]}
    assert e.value.valid_data == []


@pytest.mark.parametrize("val", ([], set()))
def test_load_many_empty_collection(val):
    class Sch(Schema):
        name = fields.Str()

    assert Sch(many=True).load(val) == []


@pytest.mark.parametrize("val", (False, 1, 1.2, object(), {}, {"1": 2}, "lol"))
def test_load_many_in_nested_invalid_input_type(val):
    class Inner(Schema):
        name = fields.String()

    class Outer(Schema):
        list1 = fields.List(fields.Nested(Inner))
        list2 = fields.Nested(Inner, many=True)

    with pytest.raises(ValidationError) as e:
        Outer().load({"list1": val, "list2": val})
    # TODO: Error messages should be identical (#779)
    assert e.value.messages == {
        "list1": ["Not a valid list."],
        "list2": ["Invalid type."],
    }


@pytest.mark.parametrize("val", ([], set()))
def test_load_many_in_nested_empty_collection(val):
    class Inner(Schema):
        name = fields.String()

    class Outer(Schema):
        list1 = fields.List(fields.Nested(Inner))
        list2 = fields.Nested(Inner, many=True)

    assert Outer().load({"list1": val, "list2": val}) == {"list1": [], "list2": []}


def test_loads_returns_a_user():
    s = UserSchema()
    result = s.loads(json.dumps({"name": "Monty"}))
    assert type(result) is User


def test_loads_many():
    s = UserSchema()
    in_data = [{"name": "Mick"}, {"name": "Keith"}]
    in_json_data = json.dumps(in_data)
    result = s.loads(in_json_data, many=True)
    assert type(result) is list
    assert result[0].name == "Mick"


def test_loads_deserializes_from_json():
    user_dict = {"name": "Monty", "age": "42.3"}
    user_json = json.dumps(user_dict)
    result = UserSchema().loads(user_json)
    assert isinstance(result, User)
    assert result.name == "Monty"
    assert math.isclose(result.age, 42.3)


def test_serializing_none():
    class MySchema(Schema):
        id = fields.Str(dump_default="no-id")
        num = fields.Int()
        name = fields.Str()

    data = UserSchema().dump(None)
    assert data == {"id": "no-id"}


def test_default_many_symmetry():
    """The dump/load(s) methods should all default to the many value of the schema."""
    s_many = UserSchema(many=True, only=("name",))
    s_single = UserSchema(only=("name",))
    u1, u2 = User("King Arthur"), User("Sir Lancelot")
    s_single.load(s_single.dump(u1))
    s_single.loads(s_single.dumps(u1))
    s_many.load(s_many.dump([u1, u2]))
    s_many.loads(s_many.dumps([u1, u2]))


def test_on_bind_field_hook():
    class MySchema(Schema):
        foo = fields.Str()

        def on_bind_field(self, field_name, field_obj):
            assert field_obj.parent is self
            field_obj.metadata["fname"] = field_name

    schema = MySchema()
    assert schema.fields["foo"].metadata["fname"] == "foo"


def test_nested_on_bind_field_hook():
    class MySchema(Schema):
        class NestedSchema(Schema):
            bar = fields.Str()

            def on_bind_field(self, field_name, field_obj):
                field_obj.metadata["fname"] = self.context["fname"]

        foo = fields.Nested(NestedSchema)

    with pytest.warns(RemovedInMarshmallow4Warning):
        schema1 = MySchema(context={"fname": "foobar"})
        schema2 = MySchema(context={"fname": "quxquux"})

        assert schema1.fields["foo"].schema.fields["bar"].metadata["fname"] == "foobar"
        assert schema2.fields["foo"].schema.fields["bar"].metadata["fname"] == "quxquux"


class TestValidate:
    def test_validate_raises_with_errors_dict(self):
        s = UserSchema()
        errors = s.validate({"email": "bad-email", "name": "Valid Name"})
        assert type(errors) is dict
        assert "email" in errors
        assert "name" not in errors

        valid_data = {"name": "Valid Name", "email": "valid@email.com"}
        errors = s.validate(valid_data)
        assert errors == {}

    def test_validate_many(self):
        s = UserSchema(many=True)
        in_data = [
            {"name": "Valid Name", "email": "validemail@hotmail.com"},
            {"name": "Valid Name2", "email": "invalid"},
        ]
        errors = s.validate(in_data, many=True)
        assert 1 in errors
        assert "email" in errors[1]

    def test_validate_many_doesnt_store_index_if_index_errors_option_is_false(self):
        class NoIndex(Schema):
            email = fields.Email()

            class Meta:
                index_errors = False

        s = NoIndex()
        in_data = [
            {"name": "Valid Name", "email": "validemail@hotmail.com"},
            {"name": "Valid Name2", "email": "invalid"},
        ]
        errors = s.validate(in_data, many=True)
        assert 1 not in errors
        assert "email" in errors

    def test_validate(self):
        s = UserSchema()
        errors = s.validate({"email": "bad-email"})
        assert errors == {"email": ["Not a valid email address."]}

    def test_validate_required(self):
        class MySchema(Schema):
            foo = fields.Raw(required=True)

        s = MySchema()
        errors = s.validate({"bar": 42})
        assert "foo" in errors
        assert "required" in errors["foo"][0]


@pytest.mark.parametrize("SchemaClass", [UserSchema, UserMetaSchema])
def test_fields_are_not_copies(SchemaClass):
    s = SchemaClass()
    s2 = SchemaClass()
    assert s.fields is not s2.fields


def test_dumps_returns_json(user):
    ser = UserSchema()
    serialized = ser.dump(user)
    json_data = ser.dumps(user)
    assert type(json_data) is str
    expected = json.dumps(serialized)
    assert json_data == expected


def test_naive_datetime_field(user, serialized_user):
    expected = utils.isoformat(user.created)
    assert serialized_user["created"] == expected


def test_datetime_formatted_field(user, serialized_user):
    result = serialized_user["created_formatted"]
    assert result == user.created.strftime("%Y-%m-%d")


def test_datetime_iso_field(user, serialized_user):
    assert serialized_user["created_iso"] == utils.isoformat(user.created)


def test_tz_datetime_field(user, serialized_user):
    # Datetime is corrected back to GMT
    expected = utils.isoformat(user.updated)
    assert serialized_user["updated"] == expected


def test_class_variable(serialized_user):
    assert serialized_user["species"] == "Homo sapiens"


@pytest.mark.parametrize("SchemaClass", [UserSchema, UserMetaSchema])
def test_serialize_many(SchemaClass):
    user1 = User(name="Mick", age=123)
    user2 = User(name="Keith", age=456)
    users = [user1, user2]
    serialized = SchemaClass(many=True).dump(users)
    assert len(serialized) == 2
    assert serialized[0]["name"] == "Mick"
    assert serialized[1]["name"] == "Keith"


def test_inheriting_schema(user):
    sch = ExtendedUserSchema()
    result = sch.dump(user)
    assert result["name"] == user.name
    user.is_old = True
    result = sch.dump(user)
    assert result["is_old"] is True


def test_custom_field(serialized_user, user):
    assert serialized_user["uppername"] == user.name.upper()


def test_url_field(serialized_user, user):
    assert serialized_user["homepage"] == user.homepage


def test_relative_url_field():
    u = {"name": "John", "homepage": "/foo"}
    UserRelativeUrlSchema().load(u)


@pytest.mark.parametrize("SchemaClass", [UserSchema, UserMetaSchema])
def test_stores_invalid_url_error(SchemaClass):
    user = {"name": "Steve", "homepage": "www.foo.com"}
    with pytest.raises(ValidationError) as excinfo:
        SchemaClass().load(user)
    errors = excinfo.value.messages
    assert "homepage" in errors
    expected = ["Not a valid URL."]
    assert errors["homepage"] == expected


@pytest.mark.parametrize("SchemaClass", [UserSchema, UserMetaSchema])
def test_email_field(SchemaClass):
    u = User("John", email="john@example.com")
    s = SchemaClass().dump(u)
    assert s["email"] == "john@example.com"


def test_stored_invalid_email():
    u = {"name": "John", "email": "johnexample.com"}
    with pytest.raises(ValidationError) as excinfo:
        UserSchema().load(u)
    errors = excinfo.value.messages
    assert "email" in errors
    assert errors["email"][0] == "Not a valid email address."


def test_integer_field():
    u = User("John", age=42.3)
    serialized = UserIntSchema().dump(u)
    assert type(serialized["age"]) is int
    assert serialized["age"] == 42


def test_as_string():
    u = User("John", age=42.3)
    serialized = UserFloatStringSchema().dump(u)
    assert type(serialized["age"]) is str
    assert math.isclose(float(serialized["age"]), 42.3)


@pytest.mark.parametrize("SchemaClass", [UserSchema, UserMetaSchema])
def test_method_field(SchemaClass, serialized_user):
    assert serialized_user["is_old"] is False
    u = User("Joe", age=81)
    assert SchemaClass().dump(u)["is_old"] is True


def test_function_field(serialized_user, user):
    assert serialized_user["lowername"] == user.name.lower()


def test_fields_must_be_declared_as_instances(user):
    class BadUserSchema(Schema):
        name = fields.String

    with pytest.raises(
        TypeError, match='Field for "name" must be declared as a Field instance'
    ):
        BadUserSchema().dump(user)


# regression test
def test_bind_field_does_not_swallow_typeerror():
    class MySchema(Schema):
        name = fields.Str()

        def on_bind_field(self, field_name, field_obj):
            raise TypeError("boom")

    with pytest.raises(TypeError, match="boom"):
        MySchema()


@pytest.mark.parametrize("SchemaClass", [UserSchema, UserMetaSchema])
def test_serializing_generator(SchemaClass):
    users = [User("Foo"), User("Bar")]
    user_gen = (u for u in users)
    s = SchemaClass(many=True).dump(user_gen)
    assert len(s) == 2
    assert s[0] == SchemaClass().dump(users[0])


def test_serializing_empty_list_returns_empty_list():
    assert UserSchema(many=True).dump([]) == []
    assert UserMetaSchema(many=True).dump([]) == []


def test_serializing_dict():
    user = {
        "name": "foo",
        "email": "foo@bar.com",
        "age": 42,
        "various_data": {"foo": "bar"},
    }
    data = UserSchema().dump(user)
    assert data["name"] == "foo"
    assert data["age"] == 42
    assert data["various_data"] == {"foo": "bar"}


def test_serializing_dict_with_meta_fields():
    class MySchema(Schema):
        class Meta:
            fields = ("foo", "bar")

    sch = MySchema()
    data = sch.dump({"foo": 42, "bar": 24, "baz": 424})
    assert data["foo"] == 42
    assert data["bar"] == 24
    assert "baz" not in data


@pytest.mark.parametrize("SchemaClass", [UserSchema, UserMetaSchema])
def test_exclude_in_init(SchemaClass, user):
    s = SchemaClass(exclude=("age", "homepage")).dump(user)
    assert "homepage" not in s
    assert "age" not in s
    assert "name" in s


@pytest.mark.parametrize("SchemaClass", [UserSchema, UserMetaSchema])
def test_only_in_init(SchemaClass, user):
    s = SchemaClass(only=("name", "age")).dump(user)
    assert "homepage" not in s
    assert "name" in s
    assert "age" in s


def test_invalid_only_param(user):
    with pytest.raises(ValueError):
        UserSchema(only=("_invalid", "name")).dump(user)


def test_can_serialize_uuid(serialized_user, user):
    assert serialized_user["uid"] == str(user.uid)


def test_can_serialize_time(user, serialized_user):
    expected = user.time_registered.isoformat()[:15]
    assert serialized_user["time_registered"] == expected


def test_json_module_is_deprecated():
    with pytest.deprecated_call():

        class UserJSONSchema(Schema):
            name = fields.String()

            class Meta:
                json_module = mockjson

    user = User("Joe")
    s = UserJSONSchema()
    result = s.dumps(user)
    assert result == mockjson.dumps("val")


def test_render_module():
    class UserJSONSchema(Schema):
        name = fields.String()

        class Meta:
            render_module = mockjson

    user = User("Joe")
    s = UserJSONSchema()
    result = s.dumps(user)
    assert result == mockjson.dumps("val")


def test_custom_error_message():
    class ErrorSchema(Schema):
        email = fields.Email(error_messages={"invalid": "Invalid email"})
        homepage = fields.Url(error_messages={"invalid": "Bad homepage."})
        balance = fields.Decimal(error_messages={"invalid": "Bad balance."})

    u = {"email": "joe.net", "homepage": "joe@example.com", "balance": "blah"}
    s = ErrorSchema()
    with pytest.raises(ValidationError) as excinfo:
        s.load(u)
    errors = excinfo.value.messages
    assert "Bad balance." in errors["balance"]
    assert "Bad homepage." in errors["homepage"]
    assert "Invalid email" in errors["email"]


def test_custom_unknown_error_message():
    custom_message = "custom error message."

    class ErrorSchema(Schema):
        error_messages = {"unknown": custom_message}
        name = fields.String()

    s = ErrorSchema()
    u = {"name": "Joe", "age": 13}
    with pytest.raises(ValidationError) as excinfo:
        s.load(u)
    errors = excinfo.value.messages
    assert custom_message in errors["age"]


def test_custom_type_error_message():
    custom_message = "custom error message."

    class ErrorSchema(Schema):
        error_messages = {"type": custom_message}
        name = fields.String()

    s = ErrorSchema()
    u = ["Joe"]
    with pytest.raises(ValidationError) as excinfo:
        s.load(u)
    errors = excinfo.value.messages
    assert custom_message in errors["_schema"]


def test_custom_type_error_message_with_many():
    custom_message = "custom error message."

    class ErrorSchema(Schema):
        error_messages = {"type": custom_message}
        name = fields.String()

    s = ErrorSchema(many=True)
    u = {"name": "Joe"}
    with pytest.raises(ValidationError) as excinfo:
        s.load(u)
    errors = excinfo.value.messages
    assert custom_message in errors["_schema"]


def test_custom_error_messages_with_inheritance():
    parent_type_message = "parent type error message."
    parent_unknown_message = "parent unknown error message."
    child_type_message = "child type error message."

    class ParentSchema(Schema):
        error_messages = {
            "type": parent_type_message,
            "unknown": parent_unknown_message,
        }
        name = fields.String()

    class ChildSchema(ParentSchema):
        error_messages = {"type": child_type_message}

    unknown_user = {"name": "Eleven", "age": 12}
    type_user = 11

    parent_schema = ParentSchema()
    with pytest.raises(ValidationError) as excinfo:
        parent_schema.load(unknown_user)
    assert parent_unknown_message in excinfo.value.messages["age"]
    with pytest.raises(ValidationError) as excinfo:
        parent_schema.load(type_user)
    assert parent_type_message in excinfo.value.messages["_schema"]

    child_schema = ChildSchema()
    with pytest.raises(ValidationError) as excinfo:
        child_schema.load(unknown_user)
    assert parent_unknown_message in excinfo.value.messages["age"]
    with pytest.raises(ValidationError) as excinfo:
        child_schema.load(type_user)
    assert child_type_message in excinfo.value.messages["_schema"]


def test_load_errors_with_many():
    class ErrorSchema(Schema):
        email = fields.Email()

    data = [
        {"email": "bademail"},
        {"email": "goo@email.com"},
        {"email": "anotherbademail"},
    ]

    with pytest.raises(ValidationError) as excinfo:
        ErrorSchema(many=True).load(data)
    errors = excinfo.value.messages
    assert 0 in errors
    assert 2 in errors
    assert "Not a valid email address." in errors[0]["email"][0]
    assert "Not a valid email address." in errors[2]["email"][0]


def test_error_raised_if_fields_option_is_not_list():
    with pytest.raises(ValueError):

        class BadSchema(Schema):
            name = fields.String()

            class Meta:
                fields = "name"


def test_error_raised_if_additional_option_is_not_list():
    with pytest.raises(ValueError):

        class BadSchema(Schema):
            name = fields.String()

            class Meta:
                additional = "email"


def test_nested_custom_set_in_exclude_reusing_schema():
    class CustomSet:
        # This custom set is to allow the obj check in BaseSchema.__filter_fields
        # to pass, since it'll be a valid instance, and this class overrides
        # getitem method to allow the hasattr check to pass too, which will try
        # to access the first obj index and will simulate a IndexError throwing.
        # e.g. SqlAlchemy.Query is a valid use case for this 'obj'.

        def __getitem__(self, item):
            return [][item]

    class ChildSchema(Schema):
        foo = fields.Raw(required=True)
        bar = fields.Raw()

        class Meta:
            only = ("bar",)

    class ParentSchema(Schema):
        child = fields.Nested(ChildSchema, many=True, exclude=("foo",))

    sch = ParentSchema()
    obj = dict(child=CustomSet())
    sch.dumps(obj)
    data = dict(child=[{"bar": 1}])
    sch.load(data, partial=True)


def test_nested_only():
    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(ChildSchema)

    sch = ParentSchema(only=("bla", "blubb.foo", "blubb.bar"))
    data = dict(bla=1, bli=2, blubb=dict(foo=42, bar=24, baz=242))
    result = sch.dump(data)
    assert "bla" in result
    assert "blubb" in result
    assert "bli" not in result
    child = result["blubb"]
    assert "foo" in child
    assert "bar" in child
    assert "baz" not in child


def test_nested_only_inheritance():
    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(ChildSchema, only=("foo", "bar"))

    sch = ParentSchema(only=("blubb.foo", "blubb.baz"))
    data = dict(bla=1, bli=2, blubb=dict(foo=42, bar=24, baz=242))
    result = sch.dump(data)
    assert "bla" not in result
    assert "blubb" in result
    assert "bli" not in result
    child = result["blubb"]
    assert "foo" in child
    assert "bar" not in child
    assert "baz" not in child


def test_nested_only_empty_inheritance():
    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(ChildSchema, only=("bar",))

    sch = ParentSchema(only=("blubb.foo",))
    data = dict(bla=1, bli=2, blubb=dict(foo=42, bar=24, baz=242))
    result = sch.dump(data)
    assert "bla" not in result
    assert "blubb" in result
    assert "bli" not in result
    child = result["blubb"]
    assert "foo" not in child
    assert "bar" not in child
    assert "baz" not in child


def test_nested_exclude():
    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(ChildSchema)

    sch = ParentSchema(exclude=("bli", "blubb.baz"))
    data = dict(bla=1, bli=2, blubb=dict(foo=42, bar=24, baz=242))
    result = sch.dump(data)
    assert "bla" in result
    assert "blubb" in result
    assert "bli" not in result
    child = result["blubb"]
    assert "foo" in child
    assert "bar" in child
    assert "baz" not in child


def test_nested_exclude_inheritance():
    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(ChildSchema, exclude=("baz",))

    sch = ParentSchema(exclude=("blubb.foo",))
    data = dict(bla=1, bli=2, blubb=dict(foo=42, bar=24, baz=242))
    result = sch.dump(data)
    assert "bla" in result
    assert "blubb" in result
    assert "bli" in result
    child = result["blubb"]
    assert "foo" not in child
    assert "bar" in child
    assert "baz" not in child


def test_nested_only_and_exclude():
    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(ChildSchema)

    sch = ParentSchema(only=("bla", "blubb.foo", "blubb.bar"), exclude=("blubb.foo",))
    data = dict(bla=1, bli=2, blubb=dict(foo=42, bar=24, baz=242))
    result = sch.dump(data)
    assert "bla" in result
    assert "blubb" in result
    assert "bli" not in result
    child = result["blubb"]
    assert "foo" not in child
    assert "bar" in child
    assert "baz" not in child


def test_nested_only_then_exclude_inheritance():
    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(ChildSchema, only=("foo", "bar"))

    sch = ParentSchema(exclude=("blubb.foo",))
    data = dict(bla=1, bli=2, blubb=dict(foo=42, bar=24, baz=242))
    result = sch.dump(data)
    assert "bla" in result
    assert "blubb" in result
    assert "bli" in result
    child = result["blubb"]
    assert "foo" not in child
    assert "bar" in child
    assert "baz" not in child


def test_nested_exclude_then_only_inheritance():
    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(ChildSchema, exclude=("foo",))

    sch = ParentSchema(only=("blubb.bar",))
    data = dict(bla=1, bli=2, blubb=dict(foo=42, bar=24, baz=242))
    result = sch.dump(data)
    assert "bla" not in result
    assert "blubb" in result
    assert "bli" not in result
    child = result["blubb"]
    assert "foo" not in child
    assert "bar" in child
    assert "baz" not in child


def test_nested_exclude_and_only_inheritance():
    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()
        ban = fields.Raw()
        fuu = fields.Raw()

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(
            ChildSchema, only=("foo", "bar", "baz", "ban"), exclude=("foo",)
        )

    sch = ParentSchema(
        only=("blubb.foo", "blubb.bar", "blubb.baz"), exclude=("blubb.baz",)
    )
    data = dict(bla=1, bli=2, blubb=dict(foo=42, bar=24, baz=242))
    result = sch.dump(data)
    assert "bla" not in result
    assert "blubb" in result
    assert "bli" not in result
    child = result["blubb"]
    assert "foo" not in child
    assert "bar" in child
    assert "baz" not in child
    assert "ban" not in child
    assert "fuu" not in child


# https://github.com/marshmallow-code/marshmallow/issues/1160
def test_nested_instance_many():
    class BookSchema(Schema):
        id = fields.Int()
        title = fields.String()

    class UserSchema(Schema):
        id = fields.Int()
        name = fields.String()
        books = fields.Nested(BookSchema(many=True))

    books = [{"id": 1, "title": "First book"}, {"id": 2, "title": "Second book"}]
    user = {"id": 1, "name": "Peter", "books": books}

    user_dump = UserSchema().dump(user)
    assert user_dump["books"] == books

    user_load = UserSchema().load(user_dump)
    assert user_load == user


def test_nested_instance_only():
    class ArtistSchema(Schema):
        first = fields.Str()
        last = fields.Str()

    class AlbumSchema(Schema):
        title = fields.Str()
        artist = fields.Nested(ArtistSchema(), only=("last",))

    schema = AlbumSchema()
    album = {"title": "Hunky Dory", "artist": {"last": "Bowie"}}
    loaded = schema.load(album)
    assert loaded == album
    full_album = {"title": "Hunky Dory", "artist": {"first": "David", "last": "Bowie"}}
    assert schema.dump(full_album) == album


def test_nested_instance_exclude():
    class ArtistSchema(Schema):
        first = fields.Str()
        last = fields.Str()

    class AlbumSchema(Schema):
        title = fields.Str()
        artist = fields.Nested(ArtistSchema(), exclude=("first",))

    schema = AlbumSchema()
    album = {"title": "Hunky Dory", "artist": {"last": "Bowie"}}
    loaded = schema.load(album)
    assert loaded == album
    full_album = {"title": "Hunky Dory", "artist": {"first": "David", "last": "Bowie"}}
    assert schema.dump(full_album) == album


def test_meta_nested_exclude():
    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(ChildSchema)

        class Meta:
            exclude = ("blubb.foo",)

    data = dict(bla=1, bli=2, blubb=dict(foo=42, bar=24, baz=242))

    sch = ParentSchema()
    result = sch.dump(data)
    assert "bla" in result
    assert "blubb" in result
    assert "bli" in result
    child = result["blubb"]
    assert "foo" not in child
    assert "bar" in child
    assert "baz" in child

    # Test fields with dot notations in Meta.exclude on multiple instantiations
    # Regression test for https://github.com/marshmallow-code/marshmallow/issues/1212
    sch = ParentSchema()
    result = sch.dump(data)
    assert "bla" in result
    assert "blubb" in result
    assert "bli" in result
    child = result["blubb"]
    assert "foo" not in child
    assert "bar" in child
    assert "baz" in child


def test_nested_custom_set_not_implementing_getitem():
    # This test checks that marshmallow can serialize implementations of
    # :mod:`collections.abc.MutableSequence`, with ``__getitem__`` arguments
    # that are not integers.

    class ListLikeParent:
        """
        Implements a list-like object that can get children using a
        non-integer key
        """

        def __init__(self, required_key, child):
            """
            :param required_key: The key to use in ``__getitem__`` in order
                to successfully get the ``child``
            :param child: The return value of the ``child`` if
            ``__getitem__`` succeeds
            """
            self.children = {required_key: child}

    class Child:
        """
        Implements an object with some attribute
        """

        def __init__(self, attribute: str):
            """
            :param attribute: The attribute to initialize
            """
            self.attribute = attribute

    class ChildSchema(Schema):
        """
        The marshmallow schema for the child
        """

        attribute = fields.Str()

    class ParentSchema(Schema):
        """
        The schema for the parent
        """

        children = fields.Nested(ChildSchema, many=True)

    attribute = "Foo"
    required_key = "key"
    child = Child(attribute)

    parent = ListLikeParent(required_key, child)

    ParentSchema().dump(parent)


def test_deeply_nested_only_and_exclude():
    class GrandChildSchema(Schema):
        goo = fields.Raw()
        gah = fields.Raw()
        bah = fields.Raw()

    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        flubb = fields.Nested(GrandChildSchema)

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(ChildSchema)

    sch = ParentSchema(
        only=("bla", "blubb.foo", "blubb.flubb.goo", "blubb.flubb.gah"),
        exclude=("blubb.flubb.goo",),
    )
    data = dict(bla=1, bli=2, blubb=dict(foo=3, bar=4, flubb=dict(goo=5, gah=6, bah=7)))
    result = sch.dump(data)
    assert "bla" in result
    assert "blubb" in result
    assert "bli" not in result
    child = result["blubb"]
    assert "foo" in child
    assert "flubb" in child
    assert "bar" not in child
    grand_child = child["flubb"]
    assert "gah" in grand_child
    assert "goo" not in grand_child
    assert "bah" not in grand_child


def test_nested_lambda():
    class ChildSchema(Schema):
        id = fields.Str()
        name = fields.Str()
        parent = fields.Nested(lambda: ParentSchema(only=("id",)), dump_only=True)
        siblings = fields.List(fields.Nested(lambda: ChildSchema(only=("id", "name"))))

    class ParentSchema(Schema):
        id = fields.Str()
        spouse = fields.Nested(lambda: ParentSchema(only=("id",)))
        children = fields.List(
            fields.Nested(lambda: ChildSchema(only=("id", "parent", "siblings")))
        )

    sch = ParentSchema()
    data_to_load = {
        "id": "p1",
        "spouse": {"id": "p2"},
        "children": [{"id": "c1", "siblings": [{"id": "c2", "name": "sis"}]}],
    }
    loaded = sch.load(data_to_load)
    assert loaded == data_to_load

    data_to_dump = dict(
        id="p2",
        spouse=dict(id="p2"),
        children=[
            dict(
                id="c1",
                name="bar",
                parent=dict(id="p2"),
                siblings=[dict(id="c2", name="sis")],
            )
        ],
    )
    dumped = sch.dump(data_to_dump)
    assert dumped == {
        "id": "p2",
        "spouse": {"id": "p2"},
        "children": [
            {
                "id": "c1",
                "parent": {"id": "p2"},
                "siblings": [{"id": "c2", "name": "sis"}],
            }
        ],
    }


@pytest.mark.parametrize("data_key", ("f1", "f5", None))
def test_data_key_collision(data_key):
    class MySchema(Schema):
        f1 = fields.Raw()
        f2 = fields.Raw(data_key=data_key)
        f3 = fields.Raw(data_key="f5")
        f4 = fields.Raw(data_key="f1", load_only=True)

    if data_key is None:
        MySchema()
    else:
        with pytest.raises(ValueError, match=data_key):
            MySchema()


@pytest.mark.parametrize("attribute", ("f1", "f5", None))
def test_attribute_collision(attribute):
    class MySchema(Schema):
        f1 = fields.Raw()
        f2 = fields.Raw(attribute=attribute)
        f3 = fields.Raw(attribute="f5")
        f4 = fields.Raw(attribute="f1", dump_only=True)

    if attribute is None:
        MySchema()
    else:
        with pytest.raises(ValueError, match=attribute):
            MySchema()


class TestDeeplyNestedLoadOnly:
    @pytest.fixture
    def schema(self):
        class GrandChildSchema(Schema):
            str_dump_only = fields.String()
            str_load_only = fields.String()
            str_regular = fields.String()

        class ChildSchema(Schema):
            str_dump_only = fields.String()
            str_load_only = fields.String()
            str_regular = fields.String()
            grand_child = fields.Nested(GrandChildSchema, unknown=EXCLUDE)

        class ParentSchema(Schema):
            str_dump_only = fields.String()
            str_load_only = fields.String()
            str_regular = fields.String()
            child = fields.Nested(ChildSchema, unknown=EXCLUDE)

        return ParentSchema(
            dump_only=(
                "str_dump_only",
                "child.str_dump_only",
                "child.grand_child.str_dump_only",
            ),
            load_only=(
                "str_load_only",
                "child.str_load_only",
                "child.grand_child.str_load_only",
            ),
        )

    @pytest.fixture
    def data(self):
        return dict(
            str_dump_only="Dump Only",
            str_load_only="Load Only",
            str_regular="Regular String",
            child=dict(
                str_dump_only="Dump Only",
                str_load_only="Load Only",
                str_regular="Regular String",
                grand_child=dict(
                    str_dump_only="Dump Only",
                    str_load_only="Load Only",
                    str_regular="Regular String",
                ),
            ),
        )

    def test_load_only(self, schema, data):
        result = schema.dump(data)
        assert "str_load_only" not in result
        assert "str_dump_only" in result
        assert "str_regular" in result
        child = result["child"]
        assert "str_load_only" not in child
        assert "str_dump_only" in child
        assert "str_regular" in child
        grand_child = child["grand_child"]
        assert "str_load_only" not in grand_child
        assert "str_dump_only" in grand_child
        assert "str_regular" in grand_child

    def test_dump_only(self, schema, data):
        result = schema.load(data, unknown=EXCLUDE)
        assert "str_dump_only" not in result
        assert "str_load_only" in result
        assert "str_regular" in result
        child = result["child"]
        assert "str_dump_only" not in child
        assert "str_load_only" in child
        assert "str_regular" in child
        grand_child = child["grand_child"]
        assert "str_dump_only" not in grand_child
        assert "str_load_only" in grand_child
        assert "str_regular" in grand_child


class TestDeeplyNestedListLoadOnly:
    @pytest.fixture
    def schema(self):
        class ChildSchema(Schema):
            str_dump_only = fields.String()
            str_load_only = fields.String()
            str_regular = fields.String()

        class ParentSchema(Schema):
            str_dump_only = fields.String()
            str_load_only = fields.String()
            str_regular = fields.String()
            child = fields.List(fields.Nested(ChildSchema, unknown=EXCLUDE))

        return ParentSchema(
            dump_only=("str_dump_only", "child.str_dump_only"),
            load_only=("str_load_only", "child.str_load_only"),
        )

    @pytest.fixture
    def data(self):
        return dict(
            str_dump_only="Dump Only",
            str_load_only="Load Only",
            str_regular="Regular String",
            child=[
                dict(
                    str_dump_only="Dump Only",
                    str_load_only="Load Only",
                    str_regular="Regular String",
                )
            ],
        )

    def test_load_only(self, schema, data):
        result = schema.dump(data)
        assert "str_load_only" not in result
        assert "str_dump_only" in result
        assert "str_regular" in result
        child = result["child"][0]
        assert "str_load_only" not in child
        assert "str_dump_only" in child
        assert "str_regular" in child

    def test_dump_only(self, schema, data):
        result = schema.load(data, unknown=EXCLUDE)
        assert "str_dump_only" not in result
        assert "str_load_only" in result
        assert "str_regular" in result
        child = result["child"][0]
        assert "str_dump_only" not in child
        assert "str_load_only" in child
        assert "str_regular" in child


def test_nested_constructor_only_and_exclude():
    class GrandChildSchema(Schema):
        goo = fields.Raw()
        gah = fields.Raw()
        bah = fields.Raw()

    class ChildSchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        flubb = fields.Nested(GrandChildSchema)

    class ParentSchema(Schema):
        bla = fields.Raw()
        bli = fields.Raw()
        blubb = fields.Nested(
            ChildSchema, only=("foo", "flubb.goo", "flubb.gah"), exclude=("flubb.goo",)
        )

    sch = ParentSchema(only=("bla", "blubb"))
    data = dict(bla=1, bli=2, blubb=dict(foo=3, bar=4, flubb=dict(goo=5, gah=6, bah=7)))
    result = sch.dump(data)
    assert "bla" in result
    assert "blubb" in result
    assert "bli" not in result
    child = result["blubb"]
    assert "foo" in child
    assert "flubb" in child
    assert "bar" not in child
    grand_child = child["flubb"]
    assert "gah" in grand_child
    assert "goo" not in grand_child
    assert "bah" not in grand_child


def test_only_and_exclude():
    class MySchema(Schema):
        foo = fields.Raw()
        bar = fields.Raw()
        baz = fields.Raw()

    sch = MySchema(only=("foo", "bar"), exclude=("bar",))
    data = dict(foo=42, bar=24, baz=242)
    result = sch.dump(data)
    assert "foo" in result
    assert "bar" not in result


def test_only_and_exclude_with_fields():
    class MySchema(Schema):
        foo = fields.Raw()

        class Meta:
            fields = ("bar", "baz")

    sch = MySchema(only=("bar", "baz"), exclude=("bar",))
    data = dict(foo=42, bar=24, baz=242)
    result = sch.dump(data)
    assert "baz" in result
    assert "bar" not in result


def test_invalid_only_and_exclude_with_fields():
    class MySchema(Schema):
        foo = fields.Raw()

        class Meta:
            fields = ("bar", "baz")

    with pytest.raises(ValueError) as excinfo:
        MySchema(only=("foo", "par"), exclude=("ban",))

    assert "foo" in str(excinfo.value)
    assert "par" in str(excinfo.value)
    assert "ban" in str(excinfo.value)


def test_only_and_exclude_with_additional():
    class MySchema(Schema):
        foo = fields.Raw()

        class Meta:
            additional = ("bar", "baz")

    sch = MySchema(only=("foo", "bar"), exclude=("bar",))
    data = dict(foo=42, bar=24, baz=242)
    result = sch.dump(data)
    assert "foo" in result
    assert "bar" not in result


def test_invalid_only_and_exclude_with_additional():
    class MySchema(Schema):
        foo = fields.Raw()

        class Meta:
            additional = ("bar", "baz")

    with pytest.raises(ValueError) as excinfo:
        MySchema(only=("foop", "par"), exclude=("ban",))

    assert "foop" in str(excinfo.value)
    assert "par" in str(excinfo.value)
    assert "ban" in str(excinfo.value)


def test_exclude_invalid_attribute():
    class MySchema(Schema):
        foo = fields.Raw()

    with pytest.raises(ValueError, match="'bar'"):
        MySchema(exclude=("bar",))


def test_only_bounded_by_fields():
    class MySchema(Schema):
        class Meta:
            fields = ("foo",)

    with pytest.raises(ValueError, match="'baz'"):
        MySchema(only=("baz",))


def test_only_bounded_by_additional():
    class MySchema(Schema):
        class Meta:
            additional = ("b",)

    with pytest.raises(ValueError):
        MySchema(only=("c",)).dump({"c": 3})


def test_only_empty():
    class MySchema(Schema):
        foo = fields.Raw()

    sch = MySchema(only=())
    assert "foo" not in sch.dump({"foo": "bar"})


@pytest.mark.parametrize("param", ("only", "exclude"))
def test_only_and_exclude_as_string(param):
    class MySchema(Schema):
        foo = fields.Raw()

    with pytest.raises(StringNotCollectionError):
        MySchema(**{param: "foo"})


def test_nested_with_sets():
    class Inner(Schema):
        foo = fields.Raw()

    class Outer(Schema):
        inners = fields.Nested(Inner, many=True)

    sch = Outer()

    class Thing(NamedTuple):
        foo: int

    data = dict(inners={Thing(42), Thing(2)})
    result = sch.dump(data)
    assert len(result["inners"]) == 2


def test_meta_serializer_fields():
    u = User("John", age=42.3, email="john@example.com", homepage="http://john.com")
    result = UserMetaSchema().dump(u)
    assert result["name"] == u.name
    assert result["balance"] == decimal.Decimal("100.00")
    assert result["uppername"] == "JOHN"
    assert result["is_old"] is False
    assert result["created"] == utils.isoformat(u.created)
    assert result["finger_count"] == 10
    assert result["various_data"] == dict(u.various_data)


def test_meta_fields_mapping(user):
    s = UserMetaSchema()
    s.dump(user)  # need to call dump to update fields
    assert type(s.fields["balance"]) is fields.Decimal
    # Inferred fields
    assert type(s.fields["name"]._field_cache[fields.String]) is fields.String
    assert type(s.fields["created"]._field_cache[fields.DateTime]) is fields.DateTime
    assert type(s.fields["updated"]._field_cache[fields.DateTime]) is fields.DateTime
    assert type(s.fields["age"]._field_cache[fields.Float]) is fields.Float
    assert type(s.fields["registered"]._field_cache[fields.Boolean]) is fields.Boolean
    assert type(s.fields["sex_choices"]._field_cache[fields.Raw]) is fields.Raw
    assert type(s.fields["hair_colors"]._field_cache[fields.Raw]) is fields.Raw
    assert type(s.fields["finger_count"]._field_cache[fields.Integer]) is fields.Integer
    assert type(s.fields["uid"]._field_cache[fields.UUID]) is fields.UUID
    assert type(s.fields["time_registered"]._field_cache[fields.Time]) is fields.Time
    assert type(s.fields["birthdate"]._field_cache[fields.Date]) is fields.Date
    assert (
        type(s.fields["since_created"]._field_cache[fields.TimeDelta])
        is fields.TimeDelta
    )


def test_meta_field_not_on_obj_raises_attribute_error(user):
    class BadUserSchema(Schema):
        class Meta:
            fields = ("name",)
            exclude = ("notfound",)

    with pytest.raises(ValueError, match="'notfound'"):
        BadUserSchema().dump(user)


def test_exclude_fields(user):
    s = UserExcludeSchema().dump(user)
    assert "created" not in s
    assert "updated" not in s
    assert "name" in s


def test_fields_option_must_be_list_or_tuple():
    with pytest.raises(ValueError):

        class BadFields(Schema):
            class Meta:
                fields = "name"


def test_exclude_option_must_be_list_or_tuple():
    with pytest.raises(ValueError):

        class BadExclude(Schema):
            class Meta:
                exclude = "name"


def test_datetimeformat_option(user):
    meta_fmt = "%Y-%m"
    field_fmt = "%m-%d"

    class DateTimeFormatSchema(Schema):
        updated = fields.DateTime(field_fmt)

        class Meta:
            fields = ("created", "updated")
            datetimeformat = meta_fmt

    serialized = DateTimeFormatSchema().dump(user)
    assert serialized["created"] == user.created.strftime(meta_fmt)
    assert serialized["updated"] == user.updated.strftime(field_fmt)


def test_dateformat_option(user):
    fmt = "%Y-%m"
    field_fmt = "%m-%d"

    class DateFormatSchema(Schema):
        birthdate = fields.Date(field_fmt)

        class Meta:
            fields = ("birthdate", "activation_date")
            dateformat = fmt

    serialized = DateFormatSchema().dump(user)
    assert serialized["birthdate"] == user.birthdate.strftime(field_fmt)
    assert serialized["activation_date"] == user.activation_date.strftime(fmt)


def test_timeformat_option(user):
    fmt = "%H:%M:%S"
    field_fmt = "%H:%M"

    class TimeFormatSchema(Schema):
        birthtime = fields.Time(field_fmt)

        class Meta:
            fields = ("birthtime", "time_registered")
            timeformat = fmt

    serialized = TimeFormatSchema().dump(user)
    assert serialized["birthtime"] == user.birthtime.strftime(field_fmt)
    assert serialized["time_registered"] == user.time_registered.strftime(fmt)


def test_default_dateformat(user):
    class DateFormatSchema(Schema):
        updated = fields.DateTime(format="%m-%d")

        class Meta:
            fields = ("created", "updated")

    serialized = DateFormatSchema().dump(user)
    assert serialized["created"] == utils.isoformat(user.created)
    assert serialized["updated"] == user.updated.strftime("%m-%d")


def test_inherit_meta(user):
    class InheritedMetaSchema(UserMetaSchema):
        pass

    result = InheritedMetaSchema().dump(user)
    expected = UserMetaSchema().dump(user)
    assert result == expected


def test_inherit_meta_override():
    class Parent(Schema):
        class Meta:
            fields = ("name", "email")
            dump_only = ("name",)

    class Child(Schema):
        class Meta(Parent.Meta):
            dump_only = ("name", "email")

    child = Child()
    assert child.opts.fields == ("name", "email")
    assert child.opts.dump_only == ("name", "email")


def test_additional(user):
    s = UserAdditionalSchema().dump(user)
    assert s["lowername"] == user.name.lower()
    assert s["name"] == user.name


def test_cant_set_both_additional_and_fields(user):
    with pytest.raises(ValueError):

        class BadSchema(Schema):
            name = fields.String()

            class Meta:
                fields = ("name", "email")
                additional = ("email", "homepage")


def test_serializing_none_meta():
    s = UserMetaSchema().dump(None)
    assert s == {}


class CustomError(Exception):
    pass


class MySchema(Schema):
    name = fields.String()
    email = fields.Email()
    age = fields.Integer()

    def handle_error(self, errors, obj, **kwargs):
        raise CustomError("Something bad happened")

    def test_load_with_custom_error_handler(self):
        in_data = {"email": "invalid"}

        class MySchema3(Schema):
            email = fields.Email()

            def handle_error(self, error, data, **kwargs):
                assert type(error) is ValidationError
                assert "email" in error.messages
                assert list(error.messages.keys()) == ["email"]
                assert data == in_data
                raise CustomError("Something bad happened")

        with pytest.raises(CustomError):
            MySchema3().load(in_data)

    def test_load_with_custom_error_handler_and_partially_valid_data(self):
        in_data = {"email": "invalid", "url": "http://valid.com"}

        class MySchema(Schema):
            email = fields.Email()
            url = fields.URL()

            def handle_error(self, error, data, **kwargs):
                assert type(error) is ValidationError
                assert "email" in error.messages
                assert list(error.messages.keys()) == ["email"]
                assert data == in_data
                raise CustomError("Something bad happened")

        with pytest.raises(CustomError):
            MySchema().load(in_data)

    def test_custom_error_handler_with_validates_decorator(self):
        in_data = {"num": -1}

        class MySchema(Schema):
            num = fields.Int()

            @validates("num")
            def validate_num(self, value):
                if value < 0:
                    raise ValidationError("Must be greater than 0.")

            def handle_error(self, error, data, **kwargs):
                assert type(error) is ValidationError
                assert "num" in error.messages
                assert list(error.messages.keys()) == ["num"]
                assert data == in_data
                raise CustomError("Something bad happened")

        with pytest.raises(CustomError):
            MySchema().load(in_data)

    def test_custom_error_handler_with_validates_schema_decorator(self):
        in_data = {"num": -1}

        class MySchema(Schema):
            num = fields.Int()

            @validates_schema
            def validates_schema(self, data, **kwargs):
                raise ValidationError("Invalid schema!")

            def handle_error(self, error, data, **kwargs):
                assert type(error) is ValidationError
                assert list(error.messages.keys()) == ["_schema"]
                assert data == in_data
                raise CustomError("Something bad happened")

        with pytest.raises(CustomError):
            MySchema().load(in_data)

    def test_validate_with_custom_error_handler(self):
        with pytest.raises(CustomError):
            MySchema().validate({"age": "notvalid", "email": "invalid"})


class TestFieldValidation:
    def test_errors_are_cleared_after_loading_collection(self):
        def always_fail(val):
            raise ValidationError("lol")

        class MySchema(Schema):
            foo = fields.Str(validate=always_fail)

        schema = MySchema()
        with pytest.raises(ValidationError) as excinfo:
            schema.load([{"foo": "bar"}, {"foo": "baz"}], many=True)
        errors = excinfo.value.messages
        assert len(errors[0]["foo"]) == 1
        assert len(errors[1]["foo"]) == 1
        with pytest.raises(ValidationError) as excinfo:
            schema.load({"foo": "bar"})
        errors = excinfo.value.messages
        assert len(errors["foo"]) == 1

    def test_raises_error_with_list(self):
        def validator(val):
            raise ValidationError(["err1", "err2"])

        class MySchema(Schema):
            foo = fields.Raw(validate=validator)

        s = MySchema()
        errors = s.validate({"foo": 42})
        assert errors["foo"] == ["err1", "err2"]

    # https://github.com/marshmallow-code/marshmallow/issues/110
    def test_raises_error_with_dict(self):
        def validator(val):
            raise ValidationError({"code": "invalid_foo"})

        class MySchema(Schema):
            foo = fields.Raw(validate=validator)

        s = MySchema()
        errors = s.validate({"foo": 42})
        assert errors["foo"] == [{"code": "invalid_foo"}]

    def test_ignored_if_not_in_only(self):
        class MySchema(Schema):
            a = fields.Raw()
            b = fields.Raw()

            @validates("a")
            def validate_a(self, val):
                raise ValidationError({"code": "invalid_a"})

            @validates("b")
            def validate_b(self, val):
                raise ValidationError({"code": "invalid_b"})

        s = MySchema(only=("b",))
        errors = s.validate({"b": "data"})
        assert errors == {"b": {"code": "invalid_b"}}


def test_schema_repr():
    class MySchema(Schema):
        name = fields.String()

    ser = MySchema(many=True)
    rep = repr(ser)
    assert "MySchema" in rep
    assert "many=True" in rep


class TestNestedSchema:
    @pytest.fixture
    def user(self):
        return User(name="Monty", age=81)

    @pytest.fixture
    def blog(self, user):
        col1 = User(name="Mick", age=123)
        col2 = User(name="Keith", age=456)
        return Blog(
            "Monty's blog",
            user=user,
            categories=["humor", "violence"],
            collaborators=[col1, col2],
        )

    # regression test for https://github.com/marshmallow-code/marshmallow/issues/64
    def test_nested_many_with_missing_attribute(self, user):
        class SimpleBlogSchema(Schema):
            title = fields.Str()
            wat = fields.Nested(UserSchema, many=True)

        blog = Blog("Simple blog", user=user, collaborators=None)
        schema = SimpleBlogSchema()
        result = schema.dump(blog)
        assert "wat" not in result

    def test_nested_with_attribute_none(self):
        class InnerSchema(Schema):
            bar = fields.Raw()

        class MySchema(Schema):
            foo = fields.Nested(InnerSchema)

        class MySchema2(Schema):
            foo = fields.Nested(InnerSchema)

        s = MySchema()
        result = s.dump({"foo": None})
        assert result["foo"] is None

        s2 = MySchema2()
        result2 = s2.dump({"foo": None})
        assert result2["foo"] is None

    def test_nested_field_does_not_validate_required(self):
        class BlogRequiredSchema(Schema):
            user = fields.Nested(UserSchema, required=True)

        b = Blog("Authorless blog", user=None)
        BlogRequiredSchema().dump(b)

    def test_nested_none(self):
        class BlogDefaultSchema(Schema):
            user = fields.Nested(UserSchema, dump_default=0)

        b = Blog("Just the default blog", user=None)
        data = BlogDefaultSchema().dump(b)
        assert data["user"] is None

    def test_nested(self, user, blog):
        blog_serializer = BlogSchema()
        serialized_blog = blog_serializer.dump(blog)
        user_serializer = UserSchema()
        serialized_user = user_serializer.dump(user)
        assert serialized_blog["user"] == serialized_user

        with pytest.raises(ValidationError, match="email"):
            BlogSchema().load(
                {"title": "Monty's blog", "user": {"name": "Monty", "email": "foo"}}
            )

    def test_nested_many_fields(self, blog):
        serialized_blog = BlogSchema().dump(blog)
        expected = [UserSchema().dump(col) for col in blog.collaborators]
        assert serialized_blog["collaborators"] == expected

    def test_nested_meta_many(self, blog):
        serialized_blog = BlogUserMetaSchema().dump(blog)
        assert len(serialized_blog["collaborators"]) == 2
        expected = [UserMetaSchema().dump(col) for col in blog.collaborators]
        assert serialized_blog["collaborators"] == expected

    def test_nested_only(self, blog):
        col1 = User(name="Mick", age=123, id_="abc")
        col2 = User(name="Keith", age=456, id_="def")
        blog.collaborators = [col1, col2]
        serialized_blog = BlogOnlySchema().dump(blog)
        assert serialized_blog["collaborators"] == [{"id": col1.id}, {"id": col2.id}]

    def test_exclude(self, blog):
        serialized = BlogSchemaExclude().dump(blog)
        assert "uppername" not in serialized["user"]

    def test_list_field(self, blog):
        serialized = BlogSchema().dump(blog)
        assert serialized["categories"] == ["humor", "violence"]

    def test_nested_load_many(self):
        in_data = {
            "title": "Shine A Light",
            "collaborators": [
                {"name": "Mick", "email": "mick@stones.com"},
                {"name": "Keith", "email": "keith@stones.com"},
            ],
        }
        data = BlogSchema().load(in_data)
        collabs = data["collaborators"]
        assert len(collabs) == 2
        assert all(type(each) is User for each in collabs)
        assert collabs[0].name == in_data["collaborators"][0]["name"]

    def test_nested_errors(self):
        with pytest.raises(ValidationError) as excinfo:
            BlogSchema().load(
                {"title": "Monty's blog", "user": {"name": "Monty", "email": "foo"}}
            )
        errors = excinfo.value.messages
        assert "email" in errors["user"]
        assert len(errors["user"]["email"]) == 1
        assert "Not a valid email address." in errors["user"]["email"][0]
        # No problems with collaborators
        assert "collaborators" not in errors

    def test_nested_method_field(self, blog):
        data = BlogSchema().dump(blog)
        assert data["user"]["is_old"]
        assert data["collaborators"][0]["is_old"]

    def test_nested_function_field(self, blog, user):
        data = BlogSchema().dump(blog)
        assert data["user"]["lowername"] == user.name.lower()
        expected = blog.collaborators[0].name.lower()
        assert data["collaborators"][0]["lowername"] == expected

    def test_serializer_meta_with_nested_fields(self, blog, user):
        data = BlogSchemaMeta().dump(blog)
        assert data["title"] == blog.title
        assert data["user"] == UserSchema().dump(user)
        assert data["collaborators"] == [
            UserSchema().dump(c) for c in blog.collaborators
        ]
        assert data["categories"] == blog.categories

    def test_serializer_with_nested_meta_fields(self, blog):
        # Schema has user = fields.Nested(UserMetaSerializer)
        s = BlogUserMetaSchema().dump(blog)
        assert s["user"] == UserMetaSchema().dump(blog.user)

    def test_nested_fields_must_be_passed_a_serializer(self, blog):
        class BadNestedFieldSchema(BlogSchema):
            user = fields.Nested(fields.String)

        with pytest.raises(ValueError):
            BadNestedFieldSchema().dump(blog)

    # regression test for https://github.com/marshmallow-code/marshmallow/issues/188
    def test_invalid_type_passed_to_nested_field(self):
        class InnerSchema(Schema):
            foo = fields.Raw()

        class MySchema(Schema):
            inner = fields.Nested(InnerSchema, many=True)

        sch = MySchema()

        sch.load({"inner": [{"foo": 42}]})

        with pytest.raises(ValidationError) as excinfo:
            sch.load({"inner": "invalid"})
        errors = excinfo.value.messages
        assert "inner" in errors
        assert errors["inner"] == ["Invalid type."]

        class OuterSchema(Schema):
            inner = fields.Nested(InnerSchema)

        schema = OuterSchema()
        with pytest.raises(ValidationError) as excinfo:
            schema.load({"inner": 1})
        errors = excinfo.value.messages
        assert errors["inner"]["_schema"] == ["Invalid input type."]

    # regression test for https://github.com/marshmallow-code/marshmallow/issues/298
    def test_all_errors_on_many_nested_field_with_validates_decorator(self):
        class Inner(Schema):
            req = fields.Raw(required=True)

        class Outer(Schema):
            inner = fields.Nested(Inner, many=True)

            @validates("inner")
            def validates_inner(self, data):
                raise ValidationError("not a chance")

        outer = Outer()
        with pytest.raises(ValidationError) as excinfo:
            outer.load({"inner": [{}]})
        errors = excinfo.value.messages
        assert "inner" in errors
        assert "_schema" in errors["inner"]

    @pytest.mark.parametrize("unknown", (None, RAISE, INCLUDE, EXCLUDE))
    def test_nested_unknown_validation(self, unknown):
        class ChildSchema(Schema):
            num = fields.Int()

        class ParentSchema(Schema):
            child = fields.Nested(ChildSchema, unknown=unknown)

        data = {"child": {"num": 1, "extra": 1}}
        if unknown is None or unknown == RAISE:
            with pytest.raises(ValidationError) as exc:
                ParentSchema().load(data)
            assert exc.value.messages == {"child": {"extra": ["Unknown field."]}}
        else:
            output = {
                INCLUDE: {"child": {"num": 1, "extra": 1}},
                EXCLUDE: {"child": {"num": 1}},
            }[unknown]
            assert ParentSchema().load(data) == output


class TestPluckSchema:
    @pytest.mark.parametrize("user_schema", [UserSchema, UserSchema()])
    def test_pluck(self, user_schema, blog):
        class FlatBlogSchema(Schema):
            user = fields.Pluck(user_schema, "name")
            collaborators = fields.Pluck(user_schema, "name", many=True)

        s = FlatBlogSchema()
        data = s.dump(blog)
        assert data["user"] == blog.user.name
        for i, name in enumerate(data["collaborators"]):
            assert name == blog.collaborators[i].name

    def test_pluck_none(self, blog):
        class FlatBlogSchema(Schema):
            user = fields.Pluck(UserSchema, "name")
            collaborators = fields.Pluck(UserSchema, "name", many=True)

        col1 = User(name="Mick", age=123)
        col2 = User(name="Keith", age=456)
        blog = Blog(title="Unowned Blog", user=None, collaborators=[col1, col2])
        s = FlatBlogSchema()
        data = s.dump(blog)
        assert data["user"] == blog.user
        for i, name in enumerate(data["collaborators"]):
            assert name == blog.collaborators[i].name

    # Regression test for https://github.com/marshmallow-code/marshmallow/issues/800
    def test_pluck_with_data_key(self, blog):
        class UserSchema(Schema):
            name = fields.String(data_key="username")
            age = fields.Int()

        class FlatBlogSchema(Schema):
            user = fields.Pluck(UserSchema, "name")
            collaborators = fields.Pluck(UserSchema, "name", many=True)

        s = FlatBlogSchema()
        data = s.dump(blog)
        assert data["user"] == blog.user.name
        for i, name in enumerate(data["collaborators"]):
            assert name == blog.collaborators[i].name
        assert s.load(data) == {
            "user": {"name": "Monty"},
            "collaborators": [{"name": "Mick"}, {"name": "Keith"}],
        }


class TestSelfReference:
    @pytest.fixture
    def employer(self):
        return User(name="Joe", age=59)

    @pytest.fixture
    def user(self, employer):
        return User(name="Tom", employer=employer, age=28)

    def test_nesting_schema_by_passing_lambda(self, user, employer):
        class SelfReferencingSchema(Schema):
            name = fields.Str()
            age = fields.Int()
            employer = fields.Nested(
                lambda: SelfReferencingSchema(exclude=("employer",))
            )

        data = SelfReferencingSchema().dump(user)
        assert data["name"] == user.name
        assert data["employer"]["name"] == employer.name
        assert data["employer"]["age"] == employer.age

    def test_nesting_schema_by_passing_class_name(self, user, employer):
        class SelfReferencingSchema(Schema):
            name = fields.Str()
            age = fields.Int()
            employer = fields.Nested("SelfReferencingSchema", exclude=("employer",))

        data = SelfReferencingSchema().dump(user)
        assert data["name"] == user.name
        assert data["employer"]["name"] == employer.name
        assert data["employer"]["age"] == employer.age

    def test_nesting_schema_self_string(self, user, employer):
        with pytest.warns(
            DeprecationWarning, match="Passing 'self' to `Nested` is deprecated"
        ):

            class SelfSchema(Schema):
                name = fields.String()
                age = fields.Integer()
                employer = fields.Nested("self", exclude=("employer",))

        data = SelfSchema().dump(user)
        assert data["name"] == user.name
        assert data["employer"]["name"] == employer.name
        assert data["employer"]["age"] == employer.age

    def test_nesting_within_itself_meta(self, user, employer):
        class SelfSchema(Schema):
            employer = fields.Nested(lambda: SelfSchema(exclude=("employer",)))

            class Meta:
                additional = ("name", "age")

        data = SelfSchema().dump(user)
        assert data["name"] == user.name
        assert data["age"] == user.age
        assert data["employer"]["name"] == employer.name
        assert data["employer"]["age"] == employer.age

    def test_nested_self_with_only_param(self, user, employer):
        class SelfSchema(Schema):
            employer = fields.Nested(lambda: SelfSchema(only=("name",)))

            class Meta:
                fields = ("name", "employer")

        data = SelfSchema().dump(user)
        assert data["name"] == user.name
        assert data["employer"]["name"] == employer.name
        assert "age" not in data["employer"]

    def test_multiple_pluck_self_lambda(self, user):
        class MultipleSelfSchema(Schema):
            emp = fields.Pluck(
                lambda: MultipleSelfSchema(), "name", attribute="employer"
            )
            rels = fields.Pluck(
                lambda: MultipleSelfSchema(), "name", many=True, attribute="relatives"
            )

            class Meta:
                fields = ("name", "emp", "rels")

        schema = MultipleSelfSchema()
        user.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)]
        data = schema.dump(user)
        assert len(data["rels"]) == len(user.relatives)
        relative = data["rels"][0]
        assert relative == user.relatives[0].name

    def test_multiple_pluck_self_string(self, user):
        with pytest.warns(
            DeprecationWarning, match="Passing 'self' to `Nested` is deprecated"
        ):

            class MultipleSelfSchema(Schema):
                emp = fields.Pluck("self", "name", attribute="employer")
                rels = fields.Pluck("self", "name", many=True, attribute="relatives")

                class Meta:
                    fields = ("name", "emp", "rels")

        schema = MultipleSelfSchema()
        user.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)]
        data = schema.dump(user)
        assert len(data["rels"]) == len(user.relatives)
        relative = data["rels"][0]
        assert relative == user.relatives[0].name

    def test_nested_self_many_lambda(self):
        class SelfManySchema(Schema):
            relatives = fields.Nested(lambda: SelfManySchema(), many=True)

            class Meta:
                additional = ("name", "age")

        person = User(name="Foo")
        person.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)]
        data = SelfManySchema().dump(person)
        assert data["name"] == person.name
        assert len(data["relatives"]) == len(person.relatives)
        assert data["relatives"][0]["name"] == person.relatives[0].name
        assert data["relatives"][0]["age"] == person.relatives[0].age

    def test_nested_self_many_string(self):
        with pytest.warns(
            DeprecationWarning, match="Passing 'self' to `Nested` is deprecated"
        ):

            class SelfManySchema(Schema):
                relatives = fields.Nested("self", many=True)

                class Meta:
                    additional = ("name", "age")

        person = User(name="Foo")
        person.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)]
        data = SelfManySchema().dump(person)
        assert data["name"] == person.name
        assert len(data["relatives"]) == len(person.relatives)
        assert data["relatives"][0]["name"] == person.relatives[0].name
        assert data["relatives"][0]["age"] == person.relatives[0].age

    def test_nested_self_list(self):
        class SelfListSchema(Schema):
            relatives = fields.List(fields.Nested(lambda: SelfListSchema()))

            class Meta:
                additional = ("name", "age")

        person = User(name="Foo")
        person.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)]
        data = SelfListSchema().dump(person)
        assert data["name"] == person.name
        assert len(data["relatives"]) == len(person.relatives)
        assert data["relatives"][0]["name"] == person.relatives[0].name
        assert data["relatives"][0]["age"] == person.relatives[0].age

    def test_nested_self_list_string(self):
        with pytest.warns(
            DeprecationWarning, match="Passing 'self' to `Nested` is deprecated"
        ):

            class SelfListSchema(Schema):
                relatives = fields.List(fields.Nested("self"))

                class Meta:
                    additional = ("name", "age")

        person = User(name="Foo")
        person.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)]
        data = SelfListSchema().dump(person)
        assert data["name"] == person.name
        assert len(data["relatives"]) == len(person.relatives)
        assert data["relatives"][0]["name"] == person.relatives[0].name
        assert data["relatives"][0]["age"] == person.relatives[0].age


class RequiredUserSchema(Schema):
    name = fields.Raw(required=True)


def test_serialization_with_required_field():
    user = User(name=None)
    RequiredUserSchema().dump(user)


def test_deserialization_with_required_field():
    in_data = {}
    with pytest.raises(ValidationError) as excinfo:
        RequiredUserSchema().load(in_data)
    data, errors = excinfo.value.valid_data, excinfo.value.messages
    assert "name" in errors
    assert "Missing data for required field." in errors["name"]
    # field value should also not be in output data
    assert "name" not in data


def test_deserialization_with_required_field_and_custom_validator():
    class ValidatingSchema(Schema):
        color = fields.String(
            required=True,
            validate=lambda x: x.lower() == "red" or x.lower() == "blue",
            error_messages={"validator_failed": "Color must be red or blue"},
        )

    with pytest.raises(ValidationError) as excinfo:
        ValidatingSchema().load({"name": "foo"})
    errors = excinfo.value.messages
    assert errors
    assert "color" in errors
    assert "Missing data for required field." in errors["color"]

    with pytest.raises(ValidationError) as excinfo:
        ValidatingSchema().load({"color": "green"})
    errors = excinfo.value.messages
    assert "color" in errors
    assert "Color must be red or blue" in errors["color"]


class UserContextSchema(Schema):
    is_owner = fields.Method("get_is_owner")
    is_collab = fields.Function(lambda user, ctx: user in ctx["blog"])

    def get_is_owner(self, user):
        return self.context["blog"].user.name == user.name


class TestContext:
    def test_context_method(self):
        owner = User("Joe")
        blog = Blog(title="Joe Blog", user=owner)
        context = {"blog": blog}
        serializer = UserContextSchema()
        serializer.context = context
        data = serializer.dump(owner)
        assert data["is_owner"] is True
        nonowner = User("Fred")
        data = serializer.dump(nonowner)
        assert data["is_owner"] is False

    def test_context_method_function(self):
        owner = User("Fred")
        blog = Blog("Killer Queen", user=owner)
        collab = User("Brian")
        blog.collaborators.append(collab)
        context = {"blog": blog}
        serializer = UserContextSchema()
        serializer.context = context
        data = serializer.dump(collab)
        assert data["is_collab"] is True
        noncollab = User("Foo")
        data = serializer.dump(noncollab)
        assert data["is_collab"] is False

    def test_function_field_raises_error_when_context_not_available(self):
        # only has a function field
        class UserFunctionContextSchema(Schema):
            is_collab = fields.Function(lambda user, ctx: user in ctx["blog"])

        owner = User("Joe")
        serializer = UserFunctionContextSchema()
        # no context
        serializer.context = None
        msg = "No context available for Function field {!r}".format("is_collab")
        with pytest.raises(ValidationError, match=msg):
            serializer.dump(owner)

    def test_function_field_handles_bound_serializer(self):
        class SerializeA:
            def __call__(self, value):
                return "value"

        serialize = SerializeA()

        # only has a function field
        class UserFunctionContextSchema(Schema):
            is_collab = fields.Function(serialize)

        owner = User("Joe")
        serializer = UserFunctionContextSchema()
        # no context
        serializer.context = None
        data = serializer.dump(owner)
        assert data["is_collab"] == "value"

    def test_fields_context(self):
        class CSchema(Schema):
            name = fields.String()

        ser = CSchema()
        ser.context["foo"] = 42

        assert ser.fields["name"].context == {"foo": 42}

    def test_nested_fields_inherit_context(self):
        class InnerSchema(Schema):
            likes_bikes = fields.Function(lambda obj, ctx: "bikes" in ctx["info"])

        class CSchema(Schema):
            inner = fields.Nested(InnerSchema)

        ser = CSchema()
        ser.context["info"] = "i like bikes"
        obj = {"inner": {}}
        with pytest.warns(RemovedInMarshmallow4Warning):
            result = ser.dump(obj)
        assert result["inner"]["likes_bikes"] is True

    # Regression test for https://github.com/marshmallow-code/marshmallow/issues/820
    def test_nested_list_fields_inherit_context(self):
        class InnerSchema(Schema):
            foo = fields.Raw()

            @validates("foo")
            def validate_foo(self, value):
                if "foo_context" not in self.context:
                    raise ValidationError("Missing context")

        class OuterSchema(Schema):
            bars = fields.List(fields.Nested(InnerSchema()))

        inner = InnerSchema()
        inner.context["foo_context"] = "foo"
        assert inner.load({"foo": 42})

        outer = OuterSchema()
        outer.context["foo_context"] = "foo"
        assert outer.load({"bars": [{"foo": 42}]})

    # Regression test for https://github.com/marshmallow-code/marshmallow/issues/820
    def test_nested_dict_fields_inherit_context(self):
        class InnerSchema(Schema):
            foo = fields.Raw()

            @validates("foo")
            def validate_foo(self, value):
                if "foo_context" not in self.context:
                    raise ValidationError("Missing context")

        class OuterSchema(Schema):
            bars = fields.Dict(values=fields.Nested(InnerSchema()))

        inner = InnerSchema()
        inner.context["foo_context"] = "foo"
        assert inner.load({"foo": 42})

        outer = OuterSchema()
        outer.context["foo_context"] = "foo"
        assert outer.load({"bars": {"test": {"foo": 42}}})

    # Regression test for https://github.com/marshmallow-code/marshmallow/issues/1404
    def test_nested_field_with_unpicklable_object_in_context(self):
        class Unpicklable:
            def __deepcopy__(self, _):
                raise NotImplementedError

        class InnerSchema(Schema):
            foo = fields.Raw()

        with pytest.warns(RemovedInMarshmallow4Warning):

            class OuterSchema(Schema):
                inner = fields.Nested(InnerSchema(context={"unp": Unpicklable()}))

        outer = OuterSchema()
        obj = {"inner": {"foo": 42}}
        assert outer.dump(obj)


def test_serializer_can_specify_nested_object_as_attribute(blog):
    class BlogUsernameSchema(Schema):
        author_name = fields.String(attribute="user.name")

    ser = BlogUsernameSchema()
    result = ser.dump(blog)
    assert result["author_name"] == blog.user.name


class TestFieldInheritance:
    def test_inherit_fields_from_schema_subclass(self):
        with pytest.warns(ChangedInMarshmallow4Warning):
            expected = {
                "field_a": fields.Number(),
                "field_b": fields.Number(),
            }

        class SerializerA(Schema):
            field_a = expected["field_a"]

        class SerializerB(SerializerA):
            field_b = expected["field_b"]

        assert SerializerB._declared_fields == expected

    def test_inherit_fields_from_non_schema_subclass(self):
        with pytest.warns(ChangedInMarshmallow4Warning):
            expected = {
                "field_a": fields.Number(),
                "field_b": fields.Number(),
            }

        class PlainBaseClass:
            field_a = expected["field_a"]

        class SerializerB1(Schema, PlainBaseClass):
            field_b = expected["field_b"]

        class SerializerB2(PlainBaseClass, Schema):
            field_b = expected["field_b"]

        assert SerializerB1._declared_fields == expected
        assert SerializerB2._declared_fields == expected

    def test_inheritance_follows_mro(self):
        expected = {
            "field_a": fields.String(),
            "field_b": fields.String(),
            "field_c": fields.String(),
            "field_d": fields.String(),
        }
        # Diamond inheritance graph
        # MRO: D -> B -> C -> A

        class SerializerA(Schema):
            field_a = expected["field_a"]

        class SerializerB(SerializerA):
            field_b = expected["field_b"]

        class SerializerC(SerializerA):
            field_c = expected["field_c"]

        class SerializerD(SerializerB, SerializerC):
            field_d = expected["field_d"]

        assert SerializerD._declared_fields == expected


def get_from_dict(schema, obj, key, default=None):
    return obj.get("_" + key, default)


class TestGetAttribute:
    def test_get_attribute_is_used(self):
        class UserDictSchema(Schema):
            name = fields.Str()
            email = fields.Email()

            def get_attribute(self, obj, attr, default):
                return get_from_dict(self, obj, attr, default)

        user_dict = {"_name": "joe", "_email": "joe@shmoe.com"}
        schema = UserDictSchema()
        result = schema.dump(user_dict)
        assert result["name"] == user_dict["_name"]
        assert result["email"] == user_dict["_email"]
        # can't serialize User object
        user = User(name="joe", email="joe@shmoe.com")
        with pytest.raises(AttributeError):
            schema.dump(user)

    def test_get_attribute_with_many(self):
        class UserDictSchema(Schema):
            name = fields.Str()
            email = fields.Email()

            def get_attribute(self, obj, attr, default):
                return get_from_dict(self, obj, attr, default)

        user_dicts = [
            {"_name": "joe", "_email": "joe@shmoe.com"},
            {"_name": "jane", "_email": "jane@shmane.com"},
        ]
        schema = UserDictSchema(many=True)
        results = schema.dump(user_dicts)
        for result, user_dict in zip(results, user_dicts):
            assert result["name"] == user_dict["_name"]
            assert result["email"] == user_dict["_email"]
        # can't serialize User object
        users = [
            User(name="joe", email="joe@shmoe.com"),
            User(name="jane", email="jane@shmane.com"),
        ]
        with pytest.raises(AttributeError):
            schema.dump(users)


class TestRequiredFields:
    class StringSchema(Schema):
        required_field = fields.Str(required=True)
        allow_none_field = fields.Str(allow_none=True)
        allow_none_required_field = fields.Str(required=True, allow_none=True)

    @pytest.fixture
    def string_schema(self):
        return self.StringSchema()

    @pytest.fixture
    def data(self):
        return dict(
            required_field="foo",
            allow_none_field="bar",
            allow_none_required_field="one",
        )

    def test_required_string_field_missing(self, string_schema, data):
        del data["required_field"]
        errors = string_schema.validate(data)
        assert errors["required_field"] == ["Missing data for required field."]

    def test_required_string_field_failure(self, string_schema, data):
        data["required_field"] = None
        errors = string_schema.validate(data)
        assert errors["required_field"] == ["Field may not be null."]

    def test_allow_none_param(self, string_schema, data):
        data["allow_none_field"] = None
        errors = string_schema.validate(data)
        assert errors == {}

        data["allow_none_required_field"] = None
        string_schema.validate(data)

        del data["allow_none_required_field"]
        errors = string_schema.validate(data)
        assert "allow_none_required_field" in errors

    def test_allow_none_custom_message(self, data):
        class MySchema(Schema):
            allow_none_field = fields.Raw(
                allow_none=False, error_messages={"null": "<custom>"}
            )

        schema = MySchema()
        errors = schema.validate({"allow_none_field": None})
        assert errors["allow_none_field"][0] == "<custom>"


class TestDefaults:
    class MySchema(Schema):
        int_no_default = fields.Int(allow_none=True)
        str_no_default = fields.Str(allow_none=True)
        list_no_default = fields.List(fields.Str, allow_none=True)
        nested_no_default = fields.Nested(UserSchema, many=True, allow_none=True)

        int_with_default = fields.Int(allow_none=True, dump_default=42)
        str_with_default = fields.Str(allow_none=True, dump_default="foo")

    @pytest.fixture
    def schema(self):
        return self.MySchema()

    @pytest.fixture
    def data(self):
        return dict(
            int_no_default=None,
            str_no_default=None,
            list_no_default=None,
            nested_no_default=None,
            int_with_default=None,
            str_with_default=None,
        )

    def test_missing_inputs_are_excluded_from_dump_output(self, schema, data):
        for key in [
            "int_no_default",
            "str_no_default",
            "list_no_default",
            "nested_no_default",
        ]:
            d = data.copy()
            del d[key]
            result = schema.dump(d)
            # the missing key is not in the serialized result
            assert key not in result
            # the rest of the keys are in the result
            assert all(k in result for k in d)

    def test_none_is_serialized_to_none(self, schema, data):
        errors = schema.validate(data)
        assert errors == {}
        result = schema.dump(data)
        for key in data:
            msg = f"result[{key!r}] should be None"
            assert result[key] is None, msg

    def test_default_and_value_missing(self, schema, data):
        del data["int_with_default"]
        del data["str_with_default"]
        result = schema.dump(data)
        assert result["int_with_default"] == 42
        assert result["str_with_default"] == "foo"

    def test_loading_none(self, schema, data):
        result = schema.load(data)
        for key in data:
            assert result[key] is None

    def test_missing_inputs_are_excluded_from_load_output(self, schema, data):
        for key in [
            "int_no_default",
            "str_no_default",
            "list_no_default",
            "nested_no_default",
        ]:
            d = data.copy()
            del d[key]
            result = schema.load(d)
            # the missing key is not in the deserialized result
            assert key not in result
            # the rest of the keys are in the result
            assert all(k in result for k in d)


class TestLoadOnly:
    class MySchema(Schema):
        class Meta:
            load_only = ("str_load_only",)
            dump_only = ("str_dump_only",)

        str_dump_only = fields.String()
        str_load_only = fields.String()
        str_regular = fields.String()

    @pytest.fixture
    def schema(self):
        return self.MySchema()

    @pytest.fixture
    def data(self):
        return dict(
            str_dump_only="Dump Only",
            str_load_only="Load Only",
            str_regular="Regular String",
        )

    def test_load_only(self, schema, data):
        result = schema.dump(data)
        assert "str_load_only" not in result
        assert "str_dump_only" in result
        assert "str_regular" in result

    def test_dump_only(self, schema, data):
        result = schema.load(data, unknown=EXCLUDE)
        assert "str_dump_only" not in result
        assert "str_load_only" in result
        assert "str_regular" in result

    # regression test for https://github.com/marshmallow-code/marshmallow/pull/765
    def test_url_field_requre_tld_false(self):
        class NoTldTestSchema(Schema):
            url = fields.Url(require_tld=False, schemes=["marshmallow"])

        schema = NoTldTestSchema()
        data_with_no_top_level_domain = {"url": "marshmallow://app/discounts"}
        result = schema.load(data_with_no_top_level_domain)
        assert result == data_with_no_top_level_domain


class TestFromDict:
    def test_generates_schema(self):
        MySchema = Schema.from_dict({"foo": fields.Str()})
        assert issubclass(MySchema, Schema)

    def test_name(self):
        MySchema = Schema.from_dict({"foo": fields.Str()})
        assert "GeneratedSchema" in repr(MySchema)
        SchemaWithName = Schema.from_dict(
            {"foo": fields.Int()}, name="MyGeneratedSchema"
        )
        assert "MyGeneratedSchema" in repr(SchemaWithName)

    def test_generated_schemas_are_not_registered(self):
        n_registry_entries = len(class_registry._registry)
        Schema.from_dict({"foo": fields.Str()})
        Schema.from_dict({"bar": fields.Str()}, name="MyGeneratedSchema")
        assert len(class_registry._registry) == n_registry_entries
        with pytest.raises(RegistryError):
            class_registry.get_class("GeneratedSchema")
        with pytest.raises(RegistryError):
            class_registry.get_class("MyGeneratedSchema")

    def test_meta_options_are_applied(self):
        class OrderedSchema(Schema):
            class Meta:
                ordered = True
                load_only = ("bar",)

        OSchema = OrderedSchema.from_dict({"foo": fields.Int(), "bar": fields.Int()})
        dumped = OSchema().dump({"foo": 42, "bar": 24})
        assert isinstance(dumped, OrderedDict)
        assert "bar" not in dumped


def test_class_registry_returns_schema_type():
    class DefinitelyUniqueSchema(Schema):
        """
        Just a schema
        """

    SchemaClass = class_registry.get_class(DefinitelyUniqueSchema.__name__)
    assert SchemaClass is DefinitelyUniqueSchema


@pytest.mark.parametrize("usage_location", ["meta", "init", "load"])
def test_unknown_parameter_value_is_validated(usage_location):
    class MySchema(Schema):
        foo = fields.String()

    with pytest.raises(  # noqa: PT012
        ValueError,
        match="Object 'badval' is not a valid value for the 'unknown' parameter",
    ):
        # Meta.unknown setting gets caught at class creation time, since that's when
        # metaclass __new__ runs
        if usage_location == "meta":

            class SubSchema(MySchema):
                class Meta:
                    unknown = "badval"

        # usages in init and load are caught at call time, as expected
        elif usage_location == "init":
            MySchema(unknown="badval")
        else:
            MySchema().load({"foo": "bar"}, unknown="badval")


@pytest.mark.parametrize("dict_cls", (dict, OrderedDict))
def test_set_dict_class(dict_cls):
    """Demonstrate how to specify dict_class as class attribute"""

    class MySchema(Schema):
        dict_class = dict_cls
        foo = fields.String()

    result = MySchema().dump({"foo": "bar"})
    assert result == {"foo": "bar"}
    assert isinstance(result, dict_cls)
