from __future__ import unicode_literals
from decimal import Decimal
from functools import wraps
import sys
import warnings

if sys.version_info[:2] < (2, 7):  # pragma: no cover
    import unittest2 as unittest
else:
    import unittest


from jsonschema import (
    PY3, SchemaError, ValidationError,ErrorTree, Validator,
    iteritems, validate
)


if PY3:
    basestring = unicode = str


class ParametrizedTestCase(type):
    """
    A (deliberately naive & specialized) parametrized test.

    """

    def __new__(cls, name, bases, attrs):
        attr = {}

        for k, v in iteritems(attrs):
            parameters = getattr(v, "_parameters", None)

            if parameters is not None:
                for parameter in parameters:
                    parametrized_name, args = parameter[0], parameter[1:]
                    fn = partial(v, *args)

                    names = ["test", k]
                    if parametrized_name:
                        names.append(parametrized_name)

                    fn_name = "_".join(names)
                    if not PY3:
                        fn_name = fn_name.encode('utf8')
                    fn.__name__ = fn_name
                    attr[fn.__name__] = fn
            else:
                attr[k] = v

        if not PY3:
            name = name.encode('utf8')

        return super(ParametrizedTestCase, cls).__new__(cls, name, bases, attr)


# Inscrutable way to create metaclasses in a Python 2/3 compatible way
# See: http://mikewatkins.ca/2008/11/29/python-2-and-3-metaclasses/
ParameterizedTestCase = ParametrizedTestCase(
    'ParameterizedTestCase', (object,), {}
)


def parametrized(*runs):
    def parametrized_test(fn):
        fn._parameters = runs
        return fn
    return parametrized_test


def partial(fn, *args, **kwargs):
    """
    ``functools.partial`` for methods (suitable for binding).

    """

    @wraps(fn)
    def _partial(self):
        return fn(self, *args, **kwargs)
    return _partial


def validation_test(schema=(), initkwargs=(), **kwschema):
    schema = dict(schema, **kwschema)
    initkwargs = dict(initkwargs)

    def _validation_test(self, expected, instance):
        if expected == "valid":
            validate(instance, schema, **initkwargs)
        elif expected == "invalid":
            with self.assertRaises(ValidationError):
                validate(instance, schema, **initkwargs)
        else:  # pragma: no cover
            raise ValueError("You spelled something wrong.")

    return _validation_test


class TestValidate(ParameterizedTestCase, unittest.TestCase):
    integer = parametrized(
        ("integer", "valid", 1),
        ("number", "invalid", 1.1),
        ("string", "invalid", "foo"),
        ("object", "invalid", {}),
        ("array", "invalid", []),
        ("boolean", "invalid", True),
        ("null", "invalid", None),
    )(validation_test(type="integer"))

    number = parametrized(
        ("integer", "valid", 1),
        ("number", "valid", 1.1),
        ("string", "invalid", "foo"),
        ("object", "invalid", {}),
        ("array", "invalid", []),
        ("boolean", "invalid", True),
        ("null", "invalid", None),
    )(validation_test(type="number"))

    string = parametrized(
        ("integer", "invalid", 1),
        ("number", "invalid", 1.1),
        ("unicode", "valid", "foo"),
        ("str", "valid", "foo"),
        ("object", "invalid", {}),
        ("array", "invalid", []),
        ("boolean", "invalid", True),
        ("null", "invalid", None),
    )(validation_test(type="string"))

    object = parametrized(
        ("integer", "invalid", 1),
        ("number", "invalid", 1.1),
        ("string", "invalid", "foo"),
        ("object", "valid", {}),
        ("array", "invalid", []),
        ("boolean", "invalid", True),
        ("null", "invalid", None),
    )(validation_test(type="object"))

    array = parametrized(
        ("integer", "invalid", 1),
        ("number", "invalid", 1.1),
        ("string", "invalid", "foo"),
        ("object", "invalid", {}),
        ("array", "valid", []),
        ("boolean", "invalid", True),
        ("null", "invalid", None),
    )(validation_test(type="array"))

    boolean = parametrized(
        ("integer", "invalid", 1),
        ("number", "invalid", 1.1),
        ("string", "invalid", "foo"),
        ("object", "invalid", {}),
        ("array", "invalid", []),
        ("true", "valid", True),
        ("false", "valid", False),
        ("null", "invalid", None),
    )(validation_test(type="boolean"))

    null = parametrized(
        ("integer", "invalid", 1),
        ("number", "invalid", 1.1),
        ("string", "invalid", "foo"),
        ("object", "invalid", {}),
        ("array", "invalid", []),
        ("boolean", "invalid", True),
        ("null", "valid", None),
    )(validation_test(type="null"))

    any = parametrized(
        ("integer", "valid", 1),
        ("number", "valid", 1.1),
        ("string", "valid", "foo"),
        ("object", "valid", {}),
        ("array", "valid", []),
        ("boolean", "valid", True),
        ("null", "valid", None),
    )(validation_test(type="any"))

    multiple_types = parametrized(
        ("integer", "valid", 1),
        ("string", "valid", "foo"),
        ("number", "invalid", 1.1),
        ("object", "invalid", {}),
        ("array", "invalid", []),
        ("boolean", "invalid", True),
        ("null", "invalid", None),
    )(validation_test(type=["integer", "string"]))

    multiple_types_schema = parametrized(
        ("match", "valid", [1, 2]),
        ("other_match", "valid", {"foo" : "bar"}),
        ("number", "invalid", 1.1),
        ("boolean", "invalid", True),
        ("null", "invalid", None),
    )(validation_test(type=["array", {"type" : "object"}]))

    multiple_types_subschema = parametrized(
        ("integer", "valid", 1),
        ("object_right_type", "valid", {"foo" : None}),
        ("object_wrong_type", "invalid", {"foo" : 1}),
        ("object_another_wrong_type", "invalid", {"foo" : 1.1}),
    )(validation_test(
        type=["integer", {"properties" : {"foo" : {"type" : "null"}}}]
    ))

    properties = parametrized(
        ("", "valid", {"foo" : 1, "bar" : "baz"}),
        ("extra_property", "valid",
         {"foo" : 1, "bar" : "baz", "quux" : 42}),
        ("invalid_type", "invalid", {"foo" : 1, "bar" : []}),
    )(validation_test(
        {
            "properties" : {
                "foo" : {"type" : "number"},
                "bar" : {"type" : "string"},
            }
        }
    ))

    patternProperties = parametrized(
        ("single_match", "valid", {"foo" : 1}),
        ("multiple_match", "valid", {"foo" : 1, "fah" : 2, "bar" : "baz"}),
        ("single_mismatch", "invalid", {"foo" : "bar"}),
        ("multiple_mismatch", "invalid", {"foo" : 1, "fah" : "bar"}),
    )(validation_test(patternProperties={"f.*" : {"type" : "integer"}}))

    multiple_patternProperties = parametrized(
        ("match", "valid", {"a" : 21}),
        ("other_match", "valid", {"aaaa" : 18}),
        ("multiple_match", "valid", {"a" : 21, "aaaa" : 18}),
        ("mismatch", "invalid", {"aaa" : "bar"}),
        ("other_mismatch", "invalid", {"aaaa" : 31}),
        ("multiple_mismatch", "invalid", {"aaa" : "foo", "aaaa" : 32}),
    )(validation_test(patternProperties={
        "a*" : {"type" : "integer"},
        "aaa*" : {"maximum" : 20},
        }
    ))

    def test_additionalProperties_allowed_by_default(self):
        schema = {
            "properties" : {
                "foo" : {"type" : "number"},
                "bar" : {"type" : "string"},
            }
        }
        validate({"foo" : 1, "bar" : "baz", "quux" : False}, schema)

    @parametrized(
        ("", False),
        ("schema", {"type" : "boolean"}),
    )
    def additionalProperties(self, aP):
        schema = {
            "properties" : {
                "foo" : {"type" : "number"},
                "bar" : {"type" : "string"},
            },

            "additionalProperties" : aP,
        }

        with self.assertRaises(ValidationError):
            validate({"foo" : 1, "bar" : "baz", "quux" : "boom"}, schema)

    def test_additionalProperties_ignores_nonobjects(self):
        validate(None, {"additionalProperties" : False})

    @parametrized(
        ("single_extra", {"foo" : 2}, ["'foo' was unexpected)"]),
        ("multiple_extras",
         dict.fromkeys(["foo", "bar", "quux"]),
         ["'bar'", "'foo'", "'quux'", "were unexpected)"],
        ),
    )
    def additionalProperties_errorMessage(self, instance, errs):
        schema = {"additionalProperties" : False}

        with self.assertRaises(ValidationError) as error:
            validate(instance, schema)

        self.assertTrue(all(err in unicode(error.exception) for err in errs))

    items = parametrized(
        ("", "valid", [1, 2, 3]),
        ("wrong_type", "invalid", [1, "x"]),
    )(validation_test(items={"type" : "integer"}))

    items_tuple_typing = parametrized(
        ("", "valid", [1, "foo"]),
        ("wrong_type", "invalid", ["foo", 1])
    )(validation_test(items=[{"type" : "integer"}, {"type" : "string"}]))

    def test_additionalItems_allowed_by_default(self):
        validate(
            [1, "foo", False],
            {"items" : [{"type" : "integer"}, {"type" : "string"}]}
        )

    additionalItems = parametrized(
        ("no_additional", "valid", [1, "foo"]),
        ("additional", "invalid", [1, "foo", False]),
    )(validation_test({
        "items" : [{"type" : "integer"}, {"type" : "string"}],
        "additionalItems" : False,
    }))

    additionalItems_schema = parametrized(
        ("match", "valid", [1, "foo", 3]),
        ("mismatch", "invalid", [1, "foo", "bar"]),
    )(validation_test({
        "items" : [{"type" : "integer"}, {"type" : "string"}],
        "additionalItems" : {"type" : "integer"},
    }))

    def test_additionalItems_ignores_nonarrays(self):
        validate(None, {"additionalItems" : False})

    @parametrized(
        ("single_extra", [2], "(2 was unexpected)"),
        ("multiple_extras", [1, 2, 3], "(1, 2, 3 were unexpected)"),
    )
    def additionalItems_errorMessage(self, instance, err):
        schema = {"additionalItems" : False}
        self.assertRaisesRegexp(
            ValidationError, err, validate, instance, schema
        )

    @parametrized(
        ("false_by_default", "valid", {}, {}),
        ("false_explicit", "valid", {"required" : False}, {}),
        ("one", "valid", {"required" : True}, {}),
        ("other", "invalid", {}, {"required" : True}),
        ("both", "invalid", {"required" : True}, {"required" : True}),
    )
    def required(self, expect, foo, bar):
        schema = {
            "properties" : {
                "foo" : {"type" : "number"},
                "bar" : {"type" : "string"},
            }
        }

        schema["properties"]["foo"].update(foo)
        schema["properties"]["bar"].update(bar)

        test = validation_test(schema)
        test(self, expect, {"foo" : 1})

    dependencies = parametrized(
        ("neither", "valid", {}),
        ("nondependant", "valid", {"foo" : 1}),
        ("with_dependency", "valid", {"foo" : 1, "bar" : 2}),
        ("missing_dependency", "invalid", {"bar" : 2}),
    )(validation_test(properties={"bar" : {"dependencies" : "foo"}}))

    multiple_dependencies = parametrized(
        ("neither", "valid", {}),
        ("nondependants", "valid", {"foo" : 1, "bar" : 2}),
        ("with_dependencies", "valid", {"foo" : 1, "bar" : 2, "quux" : 3}),
        ("missing_dependency", "invalid", {"foo" : 1, "quux" : 2}),
        ("missing_other_dependency", "invalid", {"bar" : 1, "quux" : 2}),
        ("missing_both_dependencies", "invalid", {"quux" : 1}),
    )(validation_test(
        properties={"quux" : {"dependencies" : ["foo", "bar"]}}
    ))

    multiple_dependencies_subschema = parametrized(
        ("", "valid", {"foo" : 1, "bar" : 2}),
        ("wrong_type", "invalid", {"foo" : "quux", "bar" : 2}),
        ("wrong_type_other", "invalid", {"foo" : 2, "bar" : "quux"}),
        ("wrong_type_both", "invalid", {"foo" : "quux", "bar" : "quux"}),
    )(validation_test(properties={
        "bar" : {
            "dependencies" : {
                "properties" : {
                    "foo" : {"type" : "integer"},
                    "bar" : {"type" : "integer"},
        }}}}))

    @parametrized(
        ("", "valid", {}, 2.6),
        ("fail", "invalid", {}, .6),
        ("exclusiveMinimum", "valid", {"exclusiveMinimum" : True}, 1.2),
        ("exclusiveMinimum_fail", "invalid",
         {"exclusiveMinimum" : True}, 1.1),
    )
    def minimum(self, expect, eM, instance):
        eM["minimum"] = 1.1
        test = validation_test(eM)
        test(self, expect, instance)

    @parametrized(
        ("", "valid", {}, 2.6),
        ("fail", "invalid", {}, 3.5),
        ("exclusiveMaximum", "valid", {"exclusiveMaximum" : True}, 2.2),
        ("exclusiveMaximum_fail", "invalid",
         {"exclusiveMaximum" : True}, 3.0),
    )
    def maximum(self, expect, eM, instance):
        eM["maximum"] = 3.0
        test = validation_test(eM)
        test(self, expect, instance)

    minItems = parametrized(
        ("exact", "valid", [1]),
        ("longer", "valid", [1, 2]),
        ("too_short", "invalid", []),
        ("ignores_strings", "valid", "a"),
    )(validation_test(minItems=1))

    maxItems = parametrized(
        ("exact", "valid", [1, 2]),
        ("shorter", "valid", [1]),
        ("empty", "valid", []),
        ("too_long", "invalid", [1, 2, 3]),
        ("ignores_strings", "valid", "aaaa"),
    )(validation_test(maxItems=2))

    uniqueItems = parametrized(
        ("unique", "valid", [1, 2]),
        ("not_unique", "invalid", [1, 1]),
        ("object_unique", "valid", [{"foo" : "bar"}, {"foo" : "baz"}]),
        ("object_not_unique", "invalid", [{"foo" : "bar"}, {"foo" : "bar"}]),
        ("array_unique", "valid", [["foo"], ["bar"]]),
        ("array_not_unique", "invalid", [["foo"], ["foo"]]),
        ("nested", "valid", [
            {"foo" : {"bar" : {"baz" : "quux"}}},
            {"foo" : {"bar" : {"baz" : "spam"}}},
        ]),
        ("nested_not_unique", "invalid", [
            {"foo" : {"bar" : {"baz" : "quux"}}},
            {"foo" : {"bar" : {"baz" : "quux"}}},
        ])
    )(validation_test(uniqueItems=True))

    pattern = parametrized(
        ("match", "valid", "aaa"),
        ("mismatch", "invalid", "ab"),
        ("ignores_other_stuff", "valid", True),
    )(validation_test(pattern="^a*$"))

    minLength = parametrized(
        ("", "valid", "foo"),
        ("too_short", "invalid", "f"),
        ("ignores_arrays", "valid", [1]),
    )(validation_test(minLength=2))

    maxLength = parametrized(
        ("", "valid", "f"),
        ("too_long", "invalid", "foo"),
        ("ignores_arrays", "valid", [1, 2, 3]),
    )(validation_test(maxLength=2))

    @parametrized(
        ("integer", "valid", 1, [1, 2, 3]),
        ("integer_fail", "invalid", 6, [1, 2, 3]),
        ("string", "valid", "foo", ["foo", "bar"]),
        ("string_fail", "invalid", "quux", ["foo", "bar"]),
        ("bool", "valid", True, [True]),
        ("bool_fail", "invalid", False, [True]),
        ("object", "valid", {"foo" : "bar"}, [{"foo" : "bar"}]),
        ("object_fail", "invalid", {"foo" : "bar"}, [{"foo" : "quux"}]),
    )
    def enum(self, expect, instance, enum):
        test = validation_test(enum=enum)
        test(self, expect, instance)

    @parametrized(
        ("int_by_int", "valid", 10, 2),
        ("int_by_int_fail", "invalid", 7, 2),
        ("number_by_number", "valid", 3.3, 1.1),
        ("number_by_number_fail", "invalid", 3.5, 1.1),
        ("number_by_number_small", "valid", .0075, .0001),
        ("number_by_number_small_fail", "invalid", .00751, .0001),
        ("number_by_number_again", "valid", 1.09, .01),
        ("number_by_number_again_2", "valid", 1.89, .01),
    )
    def divisibleBy(self, expect, instance, dB):
        test = validation_test(divisibleBy=dB)
        test(self, expect, instance)

    disallow = parametrized(
        ("", "valid", "foo"),
        ("disallowed", "invalid", 1),
    )(validation_test(disallow="integer"))

    multiple_disallow = parametrized(
        ("", "valid", "foo"),
        ("mismatch", "invalid", 1),
        ("other_mismatch", "invalid", True),
    )(validation_test(disallow=["integer", "boolean"]))

    multiple_disallow_subschema = parametrized(
        ("match", "valid", 1),
        ("other_match", "valid", {"foo" : 1}),
        ("mismatch", "invalid", "foo"),
        ("other_mismatch", "invalid", {"foo" : "bar"}),
    )(validation_test(
        disallow=["string", {"properties" : {"foo" : {"type" : "string"}}}]
    ))

    @parametrized(
        ("", "valid", {"foo" : "baz", "bar" : 2}),
        ("mismatch_extends", "invalid", {"foo" : "baz"}),
        ("mismatch_extended", "invalid", {"bar" : 2}),
        ("wrong_type", "invalid", {"foo" : "baz", "bar" : "quux"}),
    )
    def extends(self, expect, instance):
        schema = {
            "properties" : {"bar" : {"type" : "integer", "required" : True}},
            "extends" : {
                "properties" : {
                    "foo" : {"type" : "string", "required" : True},
                }
            },
        }

        test = validation_test(**schema)
        test(self, expect, instance)

    @parametrized(
        ("", "valid", {"foo" : "quux", "bar" : 2, "baz" : None}),
        ("mismatch_first_extends", "invalid", {"bar" : 2, "baz" : None}),
        ("mismatch_second_extends", "invalid", {"foo" : "quux", "bar" : 2}),
        ("mismatch_both", "invalid", {"bar" : 2}),
    )
    def multiple_extends(self, expect, instance):
        schema = {
            "properties" : {"bar" : {"type" : "integer", "required" : True}},
            "extends" : [
                {
                    "properties" : {
                    "foo" : {"type" : "string", "required" : True},
                    }
                },
                {
                    "properties" : {
                    "baz" : {"type" : "null", "required" : True},
                    }
                },
            ],
        }

        test = validation_test(**schema)
        test(self, expect, instance)

    extends_simple_types = parametrized(
        ("", "valid", 25),
        ("mismatch_extends", "invalid", 35)
    )(validation_test(minimum=20, extends={"maximum" : 30}))

    def test_iter_errors(self):
        instance = [1, 2]
        schema = {
            "disallow" : "array",
            "enum" : [["a", "b", "c"], ["d", "e", "f"]],
            "minItems" : 3
        }

        if PY3:
            errors = sorted([
                "'array' is disallowed for [1, 2]",
                "[1, 2] is too short",
                "[1, 2] is not one of [['a', 'b', 'c'], ['d', 'e', 'f']]",
            ])
        else:
            errors = sorted([
                "u'array' is disallowed for [1, 2]",
                "[1, 2] is too short",
                "[1, 2] is not one of [[u'a', u'b', u'c'], [u'd', u'e', u'f']]",
            ])

        self.assertEqual(
            sorted(str(e) for e in Validator().iter_errors(instance, schema)),
            errors,
        )

    def test_unknown_type_error(self):
        with self.assertRaises(SchemaError):
            validate(1, {"type" : "foo"}, unknown_type="error")

    def test_unknown_type_warn(self):
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            validate(1, {"type" : "foo"}, unknown_type="warn")
        self.assertEqual(len(w), 1)

    def test_unknown_type_skip(self):
        validate(1, {"type" : "foo"}, unknown_type="skip")

    def test_unknown_property_error(self):
        with self.assertRaises(SchemaError):
            validate(1, {"foo" : "bar"}, unknown_property="error")

    def test_unknown_property_warn(self):
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            validate(1, {"foo" : "bar"}, unknown_property="warn")
        self.assertEqual(len(w), 1)

    def test_unknown_property_skip(self):
        validate(
            1,
            {"foo" : "foo", "type" : "integer"},
            unknown_property="skip"
        )

    decimal = parametrized(
        ("integer", "valid", 1),
        ("number", "valid", 1.1),
        ("decimal", "valid", Decimal(1) / Decimal(8)),
        ("string", "invalid", "foo"),
        ("object", "invalid", {}),
        ("array", "invalid", []),
        ("boolean", "invalid", True),
        ("null", "invalid", None),
    )(validation_test(
        initkwargs={"types" : {"number" : (int, float, Decimal)}},
        type="number")
    )

    # TODO: we're in need of more meta schema tests
    def test_invalid_properties(self):
        with self.assertRaises(SchemaError):
            validate({}, {"properties": {"test": True}})

    def test_minItems_invalid_string(self):
        with self.assertRaises(SchemaError):
            validate([1], {"minItems" : "1"})  # needs to be an integer

    def test_iter_errors_multiple_failures_one_validator(self):
        instance = {"foo" : 2, "bar" : [1], "baz" : 15, "quux" : "spam"}
        schema = {
            "properties" : {
                "foo" : {"type" : "string"},
                "bar" : {"minItems" : 2},
                "baz" : {"maximum" : 10, "enum" : [2, 4, 6, 8]},
            }
        }

        errors = list(Validator().iter_errors(instance, schema))
        self.assertEqual(len(errors), 4)


class TestValidationErrorDetails(unittest.TestCase):
    # TODO: These really need unit tests for each individual validator, rather
    #       than just these higher level tests.
    def test_single_nesting(self):
        instance = {"foo" : 2, "bar" : [1], "baz" : 15, "quux" : "spam"}
        schema = {
            "properties" : {
                "foo" : {"type" : "string"},
                "bar" : {"minItems" : 2},
                "baz" : {"maximum" : 10, "enum" : [2, 4, 6, 8]},
            }
        }

        errors = Validator().iter_errors(instance, schema)
        e1, e2, e3, e4 = sorted_errors(errors)

        self.assertEqual(e1.path, ["bar"])
        self.assertEqual(e2.path, ["baz"])
        self.assertEqual(e3.path, ["baz"])
        self.assertEqual(e4.path, ["foo"])

        self.assertEqual(e1.validator, "minItems")
        self.assertEqual(e2.validator, "enum")
        self.assertEqual(e3.validator, "maximum")
        self.assertEqual(e4.validator, "type")

    def test_multiple_nesting(self):
        instance = [1, {"foo" : 2, "bar" : {"baz" : [1]}}, "quux"]
        schema = {
            "type" : "string",
            "items" : {
                "type" : ["string", "object"],
                "properties" : {
                    "foo" : {"enum" : [1, 3]},
                    "bar" : {
                        "type" : "array",
                        "properties" : {
                            "bar" : {"required" : True},
                            "baz" : {"minItems" : 2},
                        }
                    }
                }
            }
        }

        errors = Validator().iter_errors(instance, schema)
        e1, e2, e3, e4, e5, e6 = sorted_errors(errors)

        self.assertEqual(e1.path, [])
        self.assertEqual(e2.path, [0])
        self.assertEqual(e3.path, ["bar", 1])
        self.assertEqual(e4.path, ["bar", "bar", 1])
        self.assertEqual(e5.path, ["baz", "bar", 1])
        self.assertEqual(e6.path, ["foo", 1])

        self.assertEqual(e1.validator, "type")
        self.assertEqual(e2.validator, "type")
        self.assertEqual(e3.validator, "type")
        self.assertEqual(e4.validator, "required")
        self.assertEqual(e5.validator, "minItems")
        self.assertEqual(e6.validator, "enum")


class TestErrorTree(unittest.TestCase):
    def test_tree(self):
        instance = [1, {"foo" : 2, "bar" : {"baz" : [1]}}, "quux"]
        schema = {
            "type" : "string",
            "items" : {
                "type" : ["string", "object"],
                "properties" : {
                    "foo" : {"enum" : [1, 3]},
                    "bar" : {
                        "type" : "array",
                        "properties" : {
                            "bar" : {"required" : True},
                            "baz" : {"minItems" : 2},
                        }
                    }
                }
            }
        }

        errors = sorted_errors(Validator().iter_errors(instance, schema))
        e1, e2, e3, e4, e5, e6 = errors
        tree = ErrorTree(errors)

        self.assertEqual(len(tree), 6)

        self.assertIn(0, tree)
        self.assertIn(1, tree)
        self.assertIn("bar", tree[1])
        self.assertIn("foo", tree[1])
        self.assertIn("baz", tree[1]["bar"])

        self.assertEqual(tree.errors["type"], e1)
        self.assertEqual(tree[0].errors["type"], e2)
        self.assertEqual(tree[1]["bar"].errors["type"], e3)
        self.assertEqual(tree[1]["bar"]["bar"].errors["required"], e4)
        self.assertEqual(tree[1]["bar"]["baz"].errors["minItems"], e5)
        self.assertEqual(tree[1]["foo"].errors["enum"], e6)


class TestIgnorePropertiesForIrrelevantTypes(unittest.TestCase):
    def test_minimum(self):
        validate("x", {"type": ["string", "number"], "minimum": 10})

    def test_maximum(self):
        validate("x", {"type": ["string", "number"], "maximum": 10})

    def test_properties(self):
        validate(1, {"type": ["integer", "object"], "properties": {"x": {}}})

    def test_items(self):
        validate(
            1, {"type": ["integer", "array"], "items": {"type": "string"}}
        )

    def test_divisibleBy(self):
        validate("x", {"type": ["integer", "string"], "divisibleBy": 10})


def sorted_errors(errors):
    return sorted(errors, key=lambda e : [str(err) for err in e.path])
