from __future__ import annotations

from collections import deque, namedtuple
from contextlib import contextmanager
from decimal import Decimal
from io import BytesIO
from unittest import TestCase, mock
from urllib.request import pathname2url
import json
import os
import sys
import tempfile
import unittest
import warnings

import attr

from asdf._jsonschema import (
    FormatChecker,
    TypeChecker,
    exceptions,
    protocols,
    validators,
)
from asdf._jsonschema.tests._helpers import bug


def fail(validator, errors, instance, schema):
    for each in errors:
        each.setdefault("message", "You told me to fail!")
        yield exceptions.ValidationError(**each)


class TestCreateAndExtend(TestCase):
    def setUp(self):
        self.addCleanup(
            self.assertEqual,
            validators._META_SCHEMAS,
            dict(validators._META_SCHEMAS),
        )

        self.meta_schema = {"$id": "some://meta/schema"}
        self.validators = {"fail": fail}
        self.type_checker = TypeChecker()
        self.Validator = validators.create(
            meta_schema=self.meta_schema,
            validators=self.validators,
            type_checker=self.type_checker,
        )

    def test_attrs(self):
        self.assertEqual(
            (
                self.Validator.VALIDATORS,
                self.Validator.META_SCHEMA,
                self.Validator.TYPE_CHECKER,
            ), (
                self.validators,
                self.meta_schema,
                self.type_checker,
            ),
        )

    def test_init(self):
        schema = {"fail": []}
        self.assertEqual(self.Validator(schema).schema, schema)

    def test_iter_errors_successful(self):
        schema = {"fail": []}
        validator = self.Validator(schema)

        errors = list(validator.iter_errors("hello"))
        self.assertEqual(errors, [])

    def test_iter_errors_one_error(self):
        schema = {"fail": [{"message": "Whoops!"}]}
        validator = self.Validator(schema)

        expected_error = exceptions.ValidationError(
            "Whoops!",
            instance="goodbye",
            schema=schema,
            validator="fail",
            validator_value=[{"message": "Whoops!"}],
            schema_path=deque(["fail"]),
        )

        errors = list(validator.iter_errors("goodbye"))
        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0]._contents(), expected_error._contents())

    def test_iter_errors_multiple_errors(self):
        schema = {
            "fail": [
                {"message": "First"},
                {"message": "Second!", "validator": "asdf"},
                {"message": "Third"},
            ],
        }
        validator = self.Validator(schema)

        errors = list(validator.iter_errors("goodbye"))
        self.assertEqual(len(errors), 3)

    def test_if_a_version_is_provided_it_is_registered(self):
        Validator = validators.create(
            meta_schema={"$id": "something"},
            version="my version",
        )
        self.addCleanup(validators._META_SCHEMAS.pop, "something")
        self.assertEqual(Validator.__name__, "MyVersionValidator")
        self.assertEqual(Validator.__qualname__, "MyVersionValidator")

    def test_repr(self):
        Validator = validators.create(
            meta_schema={"$id": "something"},
            version="my version",
        )
        self.addCleanup(validators._META_SCHEMAS.pop, "something")
        self.assertEqual(
            repr(Validator({})),
            "MyVersionValidator(schema={}, format_checker=None)",
        )

    def test_long_repr(self):
        Validator = validators.create(
            meta_schema={"$id": "something"},
            version="my version",
        )
        self.addCleanup(validators._META_SCHEMAS.pop, "something")
        self.assertEqual(
            repr(Validator({"a": list(range(1000))})), (
                "MyVersionValidator(schema={'a': [0, 1, 2, 3, 4, 5, ...]}, "
                "format_checker=None)"
            ),
        )

    def test_repr_no_version(self):
        Validator = validators.create(meta_schema={})
        self.assertEqual(
            repr(Validator({})),
            "Validator(schema={}, format_checker=None)",
        )

    def test_dashes_are_stripped_from_validator_names(self):
        Validator = validators.create(
            meta_schema={"$id": "something"},
            version="foo-bar",
        )
        self.addCleanup(validators._META_SCHEMAS.pop, "something")
        self.assertEqual(Validator.__qualname__, "FooBarValidator")

    def test_if_a_version_is_not_provided_it_is_not_registered(self):
        original = dict(validators._META_SCHEMAS)
        validators.create(meta_schema={"id": "id"})
        self.assertEqual(validators._META_SCHEMAS, original)

    def test_validates_registers_meta_schema_id(self):
        meta_schema_key = "meta schema id"
        my_meta_schema = {"id": meta_schema_key}

        validators.create(
            meta_schema=my_meta_schema,
            version="my version",
            id_of=lambda s: s.get("id", ""),
        )
        self.addCleanup(validators._META_SCHEMAS.pop, meta_schema_key)

        self.assertIn(meta_schema_key, validators._META_SCHEMAS)

    def test_validates_registers_meta_schema_draft6_id(self):
        meta_schema_key = "meta schema $id"
        my_meta_schema = {"$id": meta_schema_key}

        validators.create(
            meta_schema=my_meta_schema,
            version="my version",
        )
        self.addCleanup(validators._META_SCHEMAS.pop, meta_schema_key)

        self.assertIn(meta_schema_key, validators._META_SCHEMAS)

    def test_create_default_types(self):
        Validator = validators.create(meta_schema={}, validators=())
        self.assertTrue(
            all(
                Validator({}).is_type(instance=instance, type=type)
                for type, instance in [
                    ("array", []),
                    ("boolean", True),
                    ("integer", 12),
                    ("null", None),
                    ("number", 12.0),
                    ("object", {}),
                    ("string", "foo"),
                ]
            ),
        )

    def test_check_schema_with_different_metaschema(self):
        """
        One can create a validator class whose metaschema uses a different
        dialect than itself.
        """

        NoEmptySchemasValidator = validators.create(
            meta_schema={
                "$schema": validators.Draft202012Validator.META_SCHEMA["$id"],
                "not": {"const": {}},
            },
        )
        NoEmptySchemasValidator.check_schema({"foo": "bar"})

        with self.assertRaises(exceptions.SchemaError):
            NoEmptySchemasValidator.check_schema({})

        NoEmptySchemasValidator({"foo": "bar"}).validate("foo")

    def test_check_schema_with_different_metaschema_defaults_to_self(self):
        """
        A validator whose metaschema doesn't declare $schema defaults to its
        own validation behavior, not the latest "normal" specification.
        """

        NoEmptySchemasValidator = validators.create(
            meta_schema={"fail": [{"message": "Meta schema whoops!"}]},
            validators={"fail": fail},
        )
        with self.assertRaises(exceptions.SchemaError):
            NoEmptySchemasValidator.check_schema({})

    def test_extend(self):
        original = dict(self.Validator.VALIDATORS)
        new = object()

        Extended = validators.extend(
            self.Validator,
            validators={"new": new},
        )
        self.assertEqual(
            (
                Extended.VALIDATORS,
                Extended.META_SCHEMA,
                Extended.TYPE_CHECKER,
                self.Validator.VALIDATORS,
            ), (
                dict(original, new=new),
                self.Validator.META_SCHEMA,
                self.Validator.TYPE_CHECKER,
                original,
            ),
        )

    def test_extend_idof(self):
        """
        Extending a validator preserves its notion of schema IDs.
        """
        def id_of(schema):
            return schema.get("__test__", self.Validator.ID_OF(schema))
        correct_id = "the://correct/id/"
        meta_schema = {
            "$id": "the://wrong/id/",
            "__test__": correct_id,
        }
        Original = validators.create(
            meta_schema=meta_schema,
            validators=self.validators,
            type_checker=self.type_checker,
            id_of=id_of,
        )
        self.assertEqual(Original.ID_OF(Original.META_SCHEMA), correct_id)

        Derived = validators.extend(Original)
        self.assertEqual(Derived.ID_OF(Derived.META_SCHEMA), correct_id)


class TestValidationErrorMessages(TestCase):
    def message_for(self, instance, schema, *args, **kwargs):
        cls = kwargs.pop("cls", validators._LATEST_VERSION)
        cls.check_schema(schema)
        validator = cls(schema, *args, **kwargs)
        errors = list(validator.iter_errors(instance))
        self.assertTrue(errors, msg=f"No errors were raised for {instance!r}")
        self.assertEqual(
            len(errors),
            1,
            msg=f"Expected exactly one error, found {errors!r}",
        )
        return errors[0].message

    def test_single_type_failure(self):
        message = self.message_for(instance=1, schema={"type": "string"})
        self.assertEqual(message, "1 is not of type 'string'")

    def test_single_type_list_failure(self):
        message = self.message_for(instance=1, schema={"type": ["string"]})
        self.assertEqual(message, "1 is not of type 'string'")

    def test_multiple_type_failure(self):
        types = "string", "object"
        message = self.message_for(instance=1, schema={"type": list(types)})
        self.assertEqual(message, "1 is not of type 'string', 'object'")

    def test_object_with_named_type_failure(self):
        schema = {"type": [{"name": "Foo", "minimum": 3}]}
        message = self.message_for(
            instance=1,
            schema=schema,
            cls=validators.Draft3Validator,
        )
        self.assertEqual(message, "1 is not of type 'Foo'")

    def test_minimum(self):
        message = self.message_for(instance=1, schema={"minimum": 2})
        self.assertEqual(message, "1 is less than the minimum of 2")

    def test_maximum(self):
        message = self.message_for(instance=1, schema={"maximum": 0})
        self.assertEqual(message, "1 is greater than the maximum of 0")

    def test_dependencies_single_element(self):
        depend, on = "bar", "foo"
        schema = {"dependencies": {depend: on}}
        message = self.message_for(
            instance={"bar": 2},
            schema=schema,
            cls=validators.Draft3Validator,
        )
        self.assertEqual(message, "'foo' is a dependency of 'bar'")

    def test_object_without_title_type_failure_draft3(self):
        type = {"type": [{"minimum": 3}]}
        message = self.message_for(
            instance=1,
            schema={"type": [type]},
            cls=validators.Draft3Validator,
        )
        self.assertEqual(
            message,
            "1 is not of type {'type': [{'minimum': 3}]}",
        )

    def test_dependencies_list_draft3(self):
        depend, on = "bar", "foo"
        schema = {"dependencies": {depend: [on]}}
        message = self.message_for(
            instance={"bar": 2},
            schema=schema,
            cls=validators.Draft3Validator,
        )
        self.assertEqual(message, "'foo' is a dependency of 'bar'")

    def test_dependencies_list_draft7(self):
        depend, on = "bar", "foo"
        schema = {"dependencies": {depend: [on]}}
        message = self.message_for(
            instance={"bar": 2},
            schema=schema,
            cls=validators.Draft7Validator,
        )
        self.assertEqual(message, "'foo' is a dependency of 'bar'")

    def test_additionalItems_single_failure(self):
        message = self.message_for(
            instance=[2],
            schema={"items": [], "additionalItems": False},
            cls=validators.Draft3Validator,
        )
        self.assertIn("(2 was unexpected)", message)

    def test_additionalItems_multiple_failures(self):
        message = self.message_for(
            instance=[1, 2, 3],
            schema={"items": [], "additionalItems": False},
            cls=validators.Draft3Validator,
        )
        self.assertIn("(1, 2, 3 were unexpected)", message)

    def test_additionalProperties_single_failure(self):
        additional = "foo"
        schema = {"additionalProperties": False}
        message = self.message_for(instance={additional: 2}, schema=schema)
        self.assertIn("('foo' was unexpected)", message)

    def test_additionalProperties_multiple_failures(self):
        schema = {"additionalProperties": False}
        message = self.message_for(
            instance=dict.fromkeys(["foo", "bar"]),
            schema=schema,
        )

        self.assertIn(repr("foo"), message)
        self.assertIn(repr("bar"), message)
        self.assertIn("were unexpected)", message)

    def test_const(self):
        schema = {"const": 12}
        message = self.message_for(
            instance={"foo": "bar"},
            schema=schema,
        )
        self.assertIn("12 was expected", message)

    def test_contains_draft_6(self):
        schema = {"contains": {"const": 12}}
        message = self.message_for(
            instance=[2, {}, []],
            schema=schema,
            cls=validators.Draft6Validator,
        )
        self.assertEqual(
            message,
            "None of [2, {}, []] are valid under the given schema",
        )

    def test_invalid_format_default_message(self):
        checker = FormatChecker(formats=())
        checker.checks("thing")(lambda value: False)

        schema = {"format": "thing"}
        message = self.message_for(
            instance="bla",
            schema=schema,
            format_checker=checker,
        )

        self.assertIn(repr("bla"), message)
        self.assertIn(repr("thing"), message)
        self.assertIn("is not a", message)

    def test_additionalProperties_false_patternProperties(self):
        schema = {"type": "object",
                  "additionalProperties": False,
                  "patternProperties": {
                      "^abc$": {"type": "string"},
                      "^def$": {"type": "string"},
                  }}
        message = self.message_for(
            instance={"zebra": 123},
            schema=schema,
            cls=validators.Draft4Validator,
        )
        self.assertEqual(
            message,
            "{} does not match any of the regexes: {}, {}".format(
                repr("zebra"), repr("^abc$"), repr("^def$"),
            ),
        )
        message = self.message_for(
            instance={"zebra": 123, "fish": 456},
            schema=schema,
            cls=validators.Draft4Validator,
        )
        self.assertEqual(
            message,
            "{}, {} do not match any of the regexes: {}, {}".format(
                repr("fish"), repr("zebra"), repr("^abc$"), repr("^def$"),
            ),
        )

    def test_False_schema(self):
        message = self.message_for(
            instance="something",
            schema=False,
        )
        self.assertEqual(message, "False schema does not allow 'something'")

    def test_multipleOf(self):
        message = self.message_for(
            instance=3,
            schema={"multipleOf": 2},
        )
        self.assertEqual(message, "3 is not a multiple of 2")

    def test_minItems(self):
        message = self.message_for(instance=[], schema={"minItems": 2})
        self.assertEqual(message, "[] is too short")

    def test_maxItems(self):
        message = self.message_for(instance=[1, 2, 3], schema={"maxItems": 2})
        self.assertEqual(message, "[1, 2, 3] is too long")

    def test_prefixItems_with_items(self):
        message = self.message_for(
            instance=[1, 2, "foo", 5],
            schema={"items": False, "prefixItems": [{}, {}]},
        )
        self.assertEqual(message, "Expected at most 2 items, but found 4")

    def test_minLength(self):
        message = self.message_for(
            instance="",
            schema={"minLength": 2},
        )
        self.assertEqual(message, "'' is too short")

    def test_maxLength(self):
        message = self.message_for(
            instance="abc",
            schema={"maxLength": 2},
        )
        self.assertEqual(message, "'abc' is too long")

    def test_pattern(self):
        message = self.message_for(
            instance="bbb",
            schema={"pattern": "^a*$"},
        )
        self.assertEqual(message, "'bbb' does not match '^a*$'")

    def test_does_not_contain(self):
        message = self.message_for(
            instance=[],
            schema={"contains": {"type": "string"}},
        )
        self.assertEqual(
            message,
            "[] does not contain items matching the given schema",
        )

    def test_contains_too_few(self):
        message = self.message_for(
            instance=["foo", 1],
            schema={"contains": {"type": "string"}, "minContains": 2},
        )
        self.assertEqual(
            message,
            "Too few items match the given schema "
            "(expected at least 2 but only 1 matched)",
        )

    def test_contains_too_few_both_constrained(self):
        message = self.message_for(
            instance=["foo", 1],
            schema={
                "contains": {"type": "string"},
                "minContains": 2,
                "maxContains": 4,
            },
        )
        self.assertEqual(
            message,
            "Too few items match the given schema (expected at least 2 but "
            "only 1 matched)",
        )

    def test_contains_too_many(self):
        message = self.message_for(
            instance=["foo", "bar", "baz"],
            schema={"contains": {"type": "string"}, "maxContains": 2},
        )
        self.assertEqual(
            message,
            "Too many items match the given schema (expected at most 2)",
        )

    def test_contains_too_many_both_constrained(self):
        message = self.message_for(
            instance=["foo"] * 5,
            schema={
                "contains": {"type": "string"},
                "minContains": 2,
                "maxContains": 4,
            },
        )
        self.assertEqual(
            message,
            "Too many items match the given schema (expected at most 4)",
        )

    def test_exclusiveMinimum(self):
        message = self.message_for(
            instance=3,
            schema={"exclusiveMinimum": 5},
        )
        self.assertEqual(
            message,
            "3 is less than or equal to the minimum of 5",
        )

    def test_exclusiveMaximum(self):
        message = self.message_for(instance=3, schema={"exclusiveMaximum": 2})
        self.assertEqual(
            message,
            "3 is greater than or equal to the maximum of 2",
        )

    def test_required(self):
        message = self.message_for(instance={}, schema={"required": ["foo"]})
        self.assertEqual(message, "'foo' is a required property")

    def test_dependentRequired(self):
        message = self.message_for(
            instance={"foo": {}},
            schema={"dependentRequired": {"foo": ["bar"]}},
        )
        self.assertEqual(message, "'bar' is a dependency of 'foo'")

    def test_minProperties(self):
        message = self.message_for(instance={}, schema={"minProperties": 2})
        self.assertEqual(message, "{} does not have enough properties")

    def test_maxProperties(self):
        message = self.message_for(
            instance={"a": {}, "b": {}, "c": {}},
            schema={"maxProperties": 2},
        )
        self.assertEqual(
            message,
            "{'a': {}, 'b': {}, 'c': {}} has too many properties",
        )

    def test_oneOf_matches_none(self):
        message = self.message_for(instance={}, schema={"oneOf": [False]})
        self.assertEqual(
            message,
            "{} is not valid under any of the given schemas",
        )

    def test_oneOf_matches_too_many(self):
        message = self.message_for(instance={}, schema={"oneOf": [True, True]})
        self.assertEqual(message, "{} is valid under each of True, True")

    def test_unevaluated_items(self):
        schema = {"type": "array", "unevaluatedItems": False}
        message = self.message_for(instance=["foo", "bar"], schema=schema)
        self.assertIn(
            message,
            "Unevaluated items are not allowed ('bar', 'foo' were unexpected)",
        )

    def test_unevaluated_items_on_invalid_type(self):
        schema = {"type": "array", "unevaluatedItems": False}
        message = self.message_for(instance="foo", schema=schema)
        self.assertEqual(message, "'foo' is not of type 'array'")

    def test_unevaluated_properties_invalid_against_subschema(self):
        schema = {
            "properties": {"foo": {"type": "string"}},
            "unevaluatedProperties": {"const": 12},
        }
        message = self.message_for(
            instance={
                "foo": "foo",
                "bar": "bar",
                "baz": 12,
            },
            schema=schema,
        )
        self.assertEqual(
            message,
            "Unevaluated properties are not valid under the given schema "
            "('bar' was unevaluated and invalid)",
        )

    def test_unevaluated_properties_disallowed(self):
        schema = {"type": "object", "unevaluatedProperties": False}
        message = self.message_for(
            instance={
                "foo": "foo",
                "bar": "bar",
            },
            schema=schema,
        )
        self.assertEqual(
            message,
            "Unevaluated properties are not allowed "
            "('bar', 'foo' were unexpected)",
        )

    def test_unevaluated_properties_on_invalid_type(self):
        schema = {"type": "object", "unevaluatedProperties": False}
        message = self.message_for(instance="foo", schema=schema)
        self.assertEqual(message, "'foo' is not of type 'object'")


class TestValidationErrorDetails(TestCase):
    # TODO: These really need unit tests for each individual keyword, rather
    #       than just these higher level tests.
    def test_anyOf(self):
        instance = 5
        schema = {
            "anyOf": [
                {"minimum": 20},
                {"type": "string"},
            ],
        }

        validator = validators.Draft4Validator(schema)
        errors = list(validator.iter_errors(instance))
        self.assertEqual(len(errors), 1)
        e = errors[0]

        self.assertEqual(e.validator, "anyOf")
        self.assertEqual(e.validator_value, schema["anyOf"])
        self.assertEqual(e.instance, instance)
        self.assertEqual(e.schema, schema)
        self.assertIsNone(e.parent)

        self.assertEqual(e.path, deque([]))
        self.assertEqual(e.relative_path, deque([]))
        self.assertEqual(e.absolute_path, deque([]))
        self.assertEqual(e.json_path, "$")

        self.assertEqual(e.schema_path, deque(["anyOf"]))
        self.assertEqual(e.relative_schema_path, deque(["anyOf"]))
        self.assertEqual(e.absolute_schema_path, deque(["anyOf"]))

        self.assertEqual(len(e.context), 2)

        e1, e2 = sorted_errors(e.context)

        self.assertEqual(e1.validator, "minimum")
        self.assertEqual(e1.validator_value, schema["anyOf"][0]["minimum"])
        self.assertEqual(e1.instance, instance)
        self.assertEqual(e1.schema, schema["anyOf"][0])
        self.assertIs(e1.parent, e)

        self.assertEqual(e1.path, deque([]))
        self.assertEqual(e1.absolute_path, deque([]))
        self.assertEqual(e1.relative_path, deque([]))
        self.assertEqual(e1.json_path, "$")

        self.assertEqual(e1.schema_path, deque([0, "minimum"]))
        self.assertEqual(e1.relative_schema_path, deque([0, "minimum"]))
        self.assertEqual(
            e1.absolute_schema_path, deque(["anyOf", 0, "minimum"]),
        )

        self.assertFalse(e1.context)

        self.assertEqual(e2.validator, "type")
        self.assertEqual(e2.validator_value, schema["anyOf"][1]["type"])
        self.assertEqual(e2.instance, instance)
        self.assertEqual(e2.schema, schema["anyOf"][1])
        self.assertIs(e2.parent, e)

        self.assertEqual(e2.path, deque([]))
        self.assertEqual(e2.relative_path, deque([]))
        self.assertEqual(e2.absolute_path, deque([]))
        self.assertEqual(e2.json_path, "$")

        self.assertEqual(e2.schema_path, deque([1, "type"]))
        self.assertEqual(e2.relative_schema_path, deque([1, "type"]))
        self.assertEqual(e2.absolute_schema_path, deque(["anyOf", 1, "type"]))

        self.assertEqual(len(e2.context), 0)

    def test_type(self):
        instance = {"foo": 1}
        schema = {
            "type": [
                {"type": "integer"},
                {
                    "type": "object",
                    "properties": {"foo": {"enum": [2]}},
                },
            ],
        }

        validator = validators.Draft3Validator(schema)
        errors = list(validator.iter_errors(instance))
        self.assertEqual(len(errors), 1)
        e = errors[0]

        self.assertEqual(e.validator, "type")
        self.assertEqual(e.validator_value, schema["type"])
        self.assertEqual(e.instance, instance)
        self.assertEqual(e.schema, schema)
        self.assertIsNone(e.parent)

        self.assertEqual(e.path, deque([]))
        self.assertEqual(e.relative_path, deque([]))
        self.assertEqual(e.absolute_path, deque([]))
        self.assertEqual(e.json_path, "$")

        self.assertEqual(e.schema_path, deque(["type"]))
        self.assertEqual(e.relative_schema_path, deque(["type"]))
        self.assertEqual(e.absolute_schema_path, deque(["type"]))

        self.assertEqual(len(e.context), 2)

        e1, e2 = sorted_errors(e.context)

        self.assertEqual(e1.validator, "type")
        self.assertEqual(e1.validator_value, schema["type"][0]["type"])
        self.assertEqual(e1.instance, instance)
        self.assertEqual(e1.schema, schema["type"][0])
        self.assertIs(e1.parent, e)

        self.assertEqual(e1.path, deque([]))
        self.assertEqual(e1.relative_path, deque([]))
        self.assertEqual(e1.absolute_path, deque([]))
        self.assertEqual(e1.json_path, "$")

        self.assertEqual(e1.schema_path, deque([0, "type"]))
        self.assertEqual(e1.relative_schema_path, deque([0, "type"]))
        self.assertEqual(e1.absolute_schema_path, deque(["type", 0, "type"]))

        self.assertFalse(e1.context)

        self.assertEqual(e2.validator, "enum")
        self.assertEqual(e2.validator_value, [2])
        self.assertEqual(e2.instance, 1)
        self.assertEqual(e2.schema, {"enum": [2]})
        self.assertIs(e2.parent, e)

        self.assertEqual(e2.path, deque(["foo"]))
        self.assertEqual(e2.relative_path, deque(["foo"]))
        self.assertEqual(e2.absolute_path, deque(["foo"]))
        self.assertEqual(e2.json_path, "$.foo")

        self.assertEqual(
            e2.schema_path, deque([1, "properties", "foo", "enum"]),
        )
        self.assertEqual(
            e2.relative_schema_path, deque([1, "properties", "foo", "enum"]),
        )
        self.assertEqual(
            e2.absolute_schema_path,
            deque(["type", 1, "properties", "foo", "enum"]),
        )

        self.assertFalse(e2.context)

    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]},
            },
        }

        validator = validators.Draft3Validator(schema)
        errors = validator.iter_errors(instance)
        e1, e2, e3, e4 = sorted_errors(errors)

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

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

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

        self.assertEqual(e1.json_path, "$.bar")
        self.assertEqual(e2.json_path, "$.baz")
        self.assertEqual(e3.json_path, "$.baz")
        self.assertEqual(e4.json_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},
                        },
                    },
                },
            },
        }

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

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

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

        self.assertEqual(e1.schema_path, deque(["type"]))
        self.assertEqual(e2.schema_path, deque(["items", "type"]))
        self.assertEqual(
            list(e3.schema_path), ["items", "properties", "bar", "type"],
        )
        self.assertEqual(
            list(e4.schema_path),
            ["items", "properties", "bar", "properties", "bar", "required"],
        )
        self.assertEqual(
            list(e5.schema_path),
            ["items", "properties", "bar", "properties", "baz", "minItems"],
        )
        self.assertEqual(
            list(e6.schema_path), ["items", "properties", "foo", "enum"],
        )

        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")

    def test_recursive(self):
        schema = {
            "definitions": {
                "node": {
                    "anyOf": [{
                        "type": "object",
                        "required": ["name", "children"],
                        "properties": {
                            "name": {
                                "type": "string",
                            },
                            "children": {
                                "type": "object",
                                "patternProperties": {
                                    "^.*$": {
                                        "$ref": "#/definitions/node",
                                    },
                                },
                            },
                        },
                    }],
                },
            },
            "type": "object",
            "required": ["root"],
            "properties": {"root": {"$ref": "#/definitions/node"}},
        }

        instance = {
            "root": {
                "name": "root",
                "children": {
                    "a": {
                        "name": "a",
                        "children": {
                            "ab": {
                                "name": "ab",
                                # missing "children"
                            },
                        },
                    },
                },
            },
        }
        validator = validators.Draft4Validator(schema)

        e, = validator.iter_errors(instance)
        self.assertEqual(e.absolute_path, deque(["root"]))
        self.assertEqual(
            e.absolute_schema_path, deque(["properties", "root", "anyOf"]),
        )
        self.assertEqual(e.json_path, "$.root")

        e1, = e.context
        self.assertEqual(e1.absolute_path, deque(["root", "children", "a"]))
        self.assertEqual(
            e1.absolute_schema_path, deque(
                [
                    "properties",
                    "root",
                    "anyOf",
                    0,
                    "properties",
                    "children",
                    "patternProperties",
                    "^.*$",
                    "anyOf",
                ],
            ),
        )
        self.assertEqual(e1.json_path, "$.root.children.a")

        e2, = e1.context
        self.assertEqual(
            e2.absolute_path, deque(
                ["root", "children", "a", "children", "ab"],
            ),
        )
        self.assertEqual(
            e2.absolute_schema_path, deque(
                [
                    "properties",
                    "root",
                    "anyOf",
                    0,
                    "properties",
                    "children",
                    "patternProperties",
                    "^.*$",
                    "anyOf",
                    0,
                    "properties",
                    "children",
                    "patternProperties",
                    "^.*$",
                    "anyOf",
                ],
            ),
        )
        self.assertEqual(e2.json_path, "$.root.children.a.children.ab")

    def test_additionalProperties(self):
        instance = {"bar": "bar", "foo": 2}
        schema = {"additionalProperties": {"type": "integer", "minimum": 5}}

        validator = validators.Draft3Validator(schema)
        errors = validator.iter_errors(instance)
        e1, e2 = sorted_errors(errors)

        self.assertEqual(e1.path, deque(["bar"]))
        self.assertEqual(e2.path, deque(["foo"]))

        self.assertEqual(e1.json_path, "$.bar")
        self.assertEqual(e2.json_path, "$.foo")

        self.assertEqual(e1.validator, "type")
        self.assertEqual(e2.validator, "minimum")

    def test_patternProperties(self):
        instance = {"bar": 1, "foo": 2}
        schema = {
            "patternProperties": {
                "bar": {"type": "string"},
                "foo": {"minimum": 5},
            },
        }

        validator = validators.Draft3Validator(schema)
        errors = validator.iter_errors(instance)
        e1, e2 = sorted_errors(errors)

        self.assertEqual(e1.path, deque(["bar"]))
        self.assertEqual(e2.path, deque(["foo"]))

        self.assertEqual(e1.json_path, "$.bar")
        self.assertEqual(e2.json_path, "$.foo")

        self.assertEqual(e1.validator, "type")
        self.assertEqual(e2.validator, "minimum")

    def test_additionalItems(self):
        instance = ["foo", 1]
        schema = {
            "items": [],
            "additionalItems": {"type": "integer", "minimum": 5},
        }

        validator = validators.Draft3Validator(schema)
        errors = validator.iter_errors(instance)
        e1, e2 = sorted_errors(errors)

        self.assertEqual(e1.path, deque([0]))
        self.assertEqual(e2.path, deque([1]))

        self.assertEqual(e1.json_path, "$[0]")
        self.assertEqual(e2.json_path, "$[1]")

        self.assertEqual(e1.validator, "type")
        self.assertEqual(e2.validator, "minimum")

    def test_additionalItems_with_items(self):
        instance = ["foo", "bar", 1]
        schema = {
            "items": [{}],
            "additionalItems": {"type": "integer", "minimum": 5},
        }

        validator = validators.Draft3Validator(schema)
        errors = validator.iter_errors(instance)
        e1, e2 = sorted_errors(errors)

        self.assertEqual(e1.path, deque([1]))
        self.assertEqual(e2.path, deque([2]))

        self.assertEqual(e1.json_path, "$[1]")
        self.assertEqual(e2.json_path, "$[2]")

        self.assertEqual(e1.validator, "type")
        self.assertEqual(e2.validator, "minimum")

    def test_propertyNames(self):
        instance = {"foo": 12}
        schema = {"propertyNames": {"not": {"const": "foo"}}}

        validator = validators.Draft7Validator(schema)
        error, = validator.iter_errors(instance)

        self.assertEqual(error.validator, "not")
        self.assertEqual(
            error.message,
            "'foo' should not be valid under {'const': 'foo'}",
        )
        self.assertEqual(error.path, deque([]))
        self.assertEqual(error.json_path, "$")
        self.assertEqual(error.schema_path, deque(["propertyNames", "not"]))

    def test_if_then(self):
        schema = {
            "if": {"const": 12},
            "then": {"const": 13},
        }

        validator = validators.Draft7Validator(schema)
        error, = validator.iter_errors(12)

        self.assertEqual(error.validator, "const")
        self.assertEqual(error.message, "13 was expected")
        self.assertEqual(error.path, deque([]))
        self.assertEqual(error.json_path, "$")
        self.assertEqual(error.schema_path, deque(["then", "const"]))

    def test_if_else(self):
        schema = {
            "if": {"const": 12},
            "else": {"const": 13},
        }

        validator = validators.Draft7Validator(schema)
        error, = validator.iter_errors(15)

        self.assertEqual(error.validator, "const")
        self.assertEqual(error.message, "13 was expected")
        self.assertEqual(error.path, deque([]))
        self.assertEqual(error.json_path, "$")
        self.assertEqual(error.schema_path, deque(["else", "const"]))

    def test_boolean_schema_False(self):
        validator = validators.Draft7Validator(False)
        error, = validator.iter_errors(12)

        self.assertEqual(
            (
                error.message,
                error.validator,
                error.validator_value,
                error.instance,
                error.schema,
                error.schema_path,
                error.json_path,
            ),
            (
                "False schema does not allow 12",
                None,
                None,
                12,
                False,
                deque([]),
                "$",
            ),
        )

    def test_ref(self):
        ref, schema = "someRef", {"additionalProperties": {"type": "integer"}}
        validator = validators.Draft7Validator(
            {"$ref": ref},
            resolver=validators.RefResolver("", {}, store={ref: schema}),
        )
        error, = validator.iter_errors({"foo": "notAnInteger"})

        self.assertEqual(
            (
                error.message,
                error.validator,
                error.validator_value,
                error.instance,
                error.absolute_path,
                error.schema,
                error.schema_path,
                error.json_path,
            ),
            (
                "'notAnInteger' is not of type 'integer'",
                "type",
                "integer",
                "notAnInteger",
                deque(["foo"]),
                {"type": "integer"},
                deque(["additionalProperties", "type"]),
                "$.foo",
            ),
        )

    def test_prefixItems(self):
        schema = {"prefixItems": [{"type": "string"}, {}, {}, {"maximum": 3}]}
        validator = validators.Draft202012Validator(schema)
        type_error, min_error = validator.iter_errors([1, 2, "foo", 5])
        self.assertEqual(
            (
                type_error.message,
                type_error.validator,
                type_error.validator_value,
                type_error.instance,
                type_error.absolute_path,
                type_error.schema,
                type_error.schema_path,
                type_error.json_path,
            ),
            (
                "1 is not of type 'string'",
                "type",
                "string",
                1,
                deque([0]),
                {"type": "string"},
                deque(["prefixItems", 0, "type"]),
                "$[0]",
            ),
        )
        self.assertEqual(
            (
                min_error.message,
                min_error.validator,
                min_error.validator_value,
                min_error.instance,
                min_error.absolute_path,
                min_error.schema,
                min_error.schema_path,
                min_error.json_path,
            ),
            (
                "5 is greater than the maximum of 3",
                "maximum",
                3,
                5,
                deque([3]),
                {"maximum": 3},
                deque(["prefixItems", 3, "maximum"]),
                "$[3]",
            ),
        )

    def test_prefixItems_with_items(self):
        schema = {
            "items": {"type": "string"},
            "prefixItems": [{}],
        }
        validator = validators.Draft202012Validator(schema)
        e1, e2 = validator.iter_errors(["foo", 2, "bar", 4, "baz"])
        self.assertEqual(
            (
                e1.message,
                e1.validator,
                e1.validator_value,
                e1.instance,
                e1.absolute_path,
                e1.schema,
                e1.schema_path,
                e1.json_path,
            ),
            (
                "2 is not of type 'string'",
                "type",
                "string",
                2,
                deque([1]),
                {"type": "string"},
                deque(["items", "type"]),
                "$[1]",
            ),
        )
        self.assertEqual(
            (
                e2.message,
                e2.validator,
                e2.validator_value,
                e2.instance,
                e2.absolute_path,
                e2.schema,
                e2.schema_path,
                e2.json_path,
            ),
            (
                "4 is not of type 'string'",
                "type",
                "string",
                4,
                deque([3]),
                {"type": "string"},
                deque(["items", "type"]),
                "$[3]",
            ),
        )

    def test_contains_too_many(self):
        """
        `contains` + `maxContains` produces only one error, even if there are
        many more incorrectly matching elements.
        """
        schema = {"contains": {"type": "string"}, "maxContains": 2}
        validator = validators.Draft202012Validator(schema)
        error, = validator.iter_errors(["foo", 2, "bar", 4, "baz", "quux"])
        self.assertEqual(
            (
                error.message,
                error.validator,
                error.validator_value,
                error.instance,
                error.absolute_path,
                error.schema,
                error.schema_path,
                error.json_path,
            ),
            (
                "Too many items match the given schema (expected at most 2)",
                "maxContains",
                2,
                ["foo", 2, "bar", 4, "baz", "quux"],
                deque([]),
                {"contains": {"type": "string"}, "maxContains": 2},
                deque(["contains"]),
                "$",
            ),
        )

    def test_contains_too_few(self):
        schema = {"contains": {"type": "string"}, "minContains": 2}
        validator = validators.Draft202012Validator(schema)
        error, = validator.iter_errors(["foo", 2, 4])
        self.assertEqual(
            (
                error.message,
                error.validator,
                error.validator_value,
                error.instance,
                error.absolute_path,
                error.schema,
                error.schema_path,
                error.json_path,
            ),
            (
                (
                    "Too few items match the given schema "
                    "(expected at least 2 but only 1 matched)"
                ),
                "minContains",
                2,
                ["foo", 2, 4],
                deque([]),
                {"contains": {"type": "string"}, "minContains": 2},
                deque(["contains"]),
                "$",
            ),
        )

    def test_contains_none(self):
        schema = {"contains": {"type": "string"}, "minContains": 2}
        validator = validators.Draft202012Validator(schema)
        error, = validator.iter_errors([2, 4])
        self.assertEqual(
            (
                error.message,
                error.validator,
                error.validator_value,
                error.instance,
                error.absolute_path,
                error.schema,
                error.schema_path,
                error.json_path,
            ),
            (
                "[2, 4] does not contain items matching the given schema",
                "contains",
                {"type": "string"},
                [2, 4],
                deque([]),
                {"contains": {"type": "string"}, "minContains": 2},
                deque(["contains"]),
                "$",
            ),
        )

    def test_ref_sibling(self):
        schema = {
            "$defs": {"foo": {"required": ["bar"]}},
            "properties": {
                "aprop": {
                    "$ref": "#/$defs/foo",
                    "required": ["baz"],
                },
            },
        }

        validator = validators.Draft202012Validator(schema)
        e1, e2 = validator.iter_errors({"aprop": {}})
        self.assertEqual(
            (
                e1.message,
                e1.validator,
                e1.validator_value,
                e1.instance,
                e1.absolute_path,
                e1.schema,
                e1.schema_path,
                e1.relative_schema_path,
                e1.json_path,
            ),
            (
                "'bar' is a required property",
                "required",
                ["bar"],
                {},
                deque(["aprop"]),
                {"required": ["bar"]},
                deque(["properties", "aprop", "required"]),
                deque(["properties", "aprop", "required"]),
                "$.aprop",
            ),
        )
        self.assertEqual(
            (
                e2.message,
                e2.validator,
                e2.validator_value,
                e2.instance,
                e2.absolute_path,
                e2.schema,
                e2.schema_path,
                e2.relative_schema_path,
                e2.json_path,
            ),
            (
                "'baz' is a required property",
                "required",
                ["baz"],
                {},
                deque(["aprop"]),
                {"$ref": "#/$defs/foo", "required": ["baz"]},
                deque(["properties", "aprop", "required"]),
                deque(["properties", "aprop", "required"]),
                "$.aprop",
            ),
        )


class MetaSchemaTestsMixin:
    # TODO: These all belong upstream
    def test_invalid_properties(self):
        with self.assertRaises(exceptions.SchemaError):
            self.Validator.check_schema({"properties": 12})

    def test_minItems_invalid_string(self):
        with self.assertRaises(exceptions.SchemaError):
            # needs to be an integer
            self.Validator.check_schema({"minItems": "1"})

    def test_enum_allows_empty_arrays(self):
        """
        Technically, all the spec says is they SHOULD have elements, not MUST.

        (As of Draft 6. Previous drafts do say MUST).

        See #529.
        """
        if self.Validator in {
            validators.Draft3Validator,
            validators.Draft4Validator,
        }:
            with self.assertRaises(exceptions.SchemaError):
                self.Validator.check_schema({"enum": []})
        else:
            self.Validator.check_schema({"enum": []})

    def test_enum_allows_non_unique_items(self):
        """
        Technically, all the spec says is they SHOULD be unique, not MUST.

        (As of Draft 6. Previous drafts do say MUST).

        See #529.
        """
        if self.Validator in {
            validators.Draft3Validator,
            validators.Draft4Validator,
        }:
            with self.assertRaises(exceptions.SchemaError):
                self.Validator.check_schema({"enum": [12, 12]})
        else:
            self.Validator.check_schema({"enum": [12, 12]})

    def test_schema_with_invalid_regex(self):
        with self.assertRaises(exceptions.SchemaError):
            self.Validator.check_schema({"pattern": "*notaregex"})

    def test_schema_with_invalid_regex_with_disabled_format_validation(self):
        self.Validator.check_schema(
            {"pattern": "*notaregex"},
            format_checker=None,
        )


class ValidatorTestMixin(MetaSchemaTestsMixin, object):
    def test_it_implements_the_validator_protocol(self):
        self.assertIsInstance(self.Validator({}), protocols.Validator)

    def test_valid_instances_are_valid(self):
        schema, instance = self.valid
        self.assertTrue(self.Validator(schema).is_valid(instance))

    def test_invalid_instances_are_not_valid(self):
        schema, instance = self.invalid
        self.assertFalse(self.Validator(schema).is_valid(instance))

    def test_non_existent_properties_are_ignored(self):
        self.Validator({object(): object()}).validate(instance=object())

    def test_it_creates_a_ref_resolver_if_not_provided(self):
        self.assertIsInstance(
            self.Validator({}).resolver,
            validators.RefResolver,
        )

    def test_it_delegates_to_a_ref_resolver(self):
        ref, schema = "someCoolRef", {"type": "integer"}
        resolver = validators.RefResolver("", {}, store={ref: schema})
        validator = self.Validator({"$ref": ref}, resolver=resolver)

        with self.assertRaises(exceptions.ValidationError):
            validator.validate(None)

    def test_evolve(self):
        ref, schema = "someCoolRef", {"type": "integer"}
        resolver = validators.RefResolver("", {}, store={ref: schema})

        validator = self.Validator(schema, resolver=resolver)
        new = validator.evolve(schema={"type": "string"})

        expected = self.Validator({"type": "string"}, resolver=resolver)

        self.assertEqual(new, expected)
        self.assertNotEqual(new, validator)

    def test_evolve_with_subclass(self):
        """
        Subclassing validators isn't supported public API, but some users have
        done it, because we don't actually error entirely when it's done :/

        We need to deprecate doing so first to help as many of these users
        ensure they can move to supported APIs, but this test ensures that in
        the interim, we haven't broken those users.
        """

        with self.assertWarns(DeprecationWarning):
            @attr.s
            class OhNo(self.Validator):
                foo = attr.ib(factory=lambda: [1, 2, 3])
                _bar = attr.ib(default=37)

        validator = OhNo({}, bar=12)
        self.assertEqual(validator.foo, [1, 2, 3])

        new = validator.evolve(schema={"type": "integer"})
        self.assertEqual(new.foo, [1, 2, 3])
        self.assertEqual(new._bar, 12)

    def test_it_delegates_to_a_legacy_ref_resolver(self):
        """
        Legacy RefResolvers support only the context manager form of
        resolution.
        """

        class LegacyRefResolver:
            @contextmanager
            def resolving(this, ref):
                self.assertEqual(ref, "the ref")
                yield {"type": "integer"}

        resolver = LegacyRefResolver()
        schema = {"$ref": "the ref"}

        with self.assertRaises(exceptions.ValidationError):
            self.Validator(schema, resolver=resolver).validate(None)

    def test_is_type_is_true_for_valid_type(self):
        self.assertTrue(self.Validator({}).is_type("foo", "string"))

    def test_is_type_is_false_for_invalid_type(self):
        self.assertFalse(self.Validator({}).is_type("foo", "array"))

    def test_is_type_evades_bool_inheriting_from_int(self):
        self.assertFalse(self.Validator({}).is_type(True, "integer"))
        self.assertFalse(self.Validator({}).is_type(True, "number"))

    def test_it_can_validate_with_decimals(self):
        schema = {"items": {"type": "number"}}
        Validator = validators.extend(
            self.Validator,
            type_checker=self.Validator.TYPE_CHECKER.redefine(
                "number",
                lambda checker, thing: isinstance(
                    thing, (int, float, Decimal),
                ) and not isinstance(thing, bool),
            ),
        )

        validator = Validator(schema)
        validator.validate([1, 1.1, Decimal(1) / Decimal(8)])

        invalid = ["foo", {}, [], True, None]
        self.assertEqual(
            [error.instance for error in validator.iter_errors(invalid)],
            invalid,
        )

    def test_it_returns_true_for_formats_it_does_not_know_about(self):
        validator = self.Validator(
            {"format": "carrot"}, format_checker=FormatChecker(),
        )
        validator.validate("bugs")

    def test_it_does_not_validate_formats_by_default(self):
        validator = self.Validator({})
        self.assertIsNone(validator.format_checker)

    def test_it_validates_formats_if_a_checker_is_provided(self):
        checker = FormatChecker()
        bad = ValueError("Bad!")

        @checker.checks("foo", raises=ValueError)
        def check(value):
            if value == "good":
                return True
            elif value == "bad":
                raise bad
            else:  # pragma: no cover
                self.fail("What is {}? [Baby Don't Hurt Me]".format(value))

        validator = self.Validator(
            {"format": "foo"}, format_checker=checker,
        )

        validator.validate("good")
        with self.assertRaises(exceptions.ValidationError) as cm:
            validator.validate("bad")

        # Make sure original cause is attached
        self.assertIs(cm.exception.cause, bad)

    def test_non_string_custom_type(self):
        non_string_type = object()
        schema = {"type": [non_string_type]}
        Crazy = validators.extend(
            self.Validator,
            type_checker=self.Validator.TYPE_CHECKER.redefine(
                non_string_type,
                lambda checker, thing: isinstance(thing, int),
            ),
        )
        Crazy(schema).validate(15)

    def test_it_properly_formats_tuples_in_errors(self):
        """
        A tuple instance properly formats validation errors for uniqueItems.

        See #224
        """
        TupleValidator = validators.extend(
            self.Validator,
            type_checker=self.Validator.TYPE_CHECKER.redefine(
                "array",
                lambda checker, thing: isinstance(thing, tuple),
            ),
        )
        with self.assertRaises(exceptions.ValidationError) as e:
            TupleValidator({"uniqueItems": True}).validate((1, 1))
        self.assertIn("(1, 1) has non-unique elements", str(e.exception))

    def test_check_redefined_sequence(self):
        """
        Allow array to validate against another defined sequence type
        """
        schema = {"type": "array", "uniqueItems": True}
        MyMapping = namedtuple("MyMapping", "a, b")
        Validator = validators.extend(
            self.Validator,
            type_checker=self.Validator.TYPE_CHECKER.redefine_many(
                {
                    "array": lambda checker, thing: isinstance(
                        thing, (list, deque),
                    ),
                    "object": lambda checker, thing: isinstance(
                        thing, (dict, MyMapping),
                    ),
                },
            ),
        )
        validator = Validator(schema)

        valid_instances = [
            deque(["a", None, "1", "", True]),
            deque([[False], [0]]),
            [deque([False]), deque([0])],
            [[deque([False])], [deque([0])]],
            [[[[[deque([False])]]]], [[[[deque([0])]]]]],
            [deque([deque([False])]), deque([deque([0])])],
            [MyMapping("a", 0), MyMapping("a", False)],
            [
                MyMapping("a", [deque([0])]),
                MyMapping("a", [deque([False])]),
            ],
            [
                MyMapping("a", [MyMapping("a", deque([0]))]),
                MyMapping("a", [MyMapping("a", deque([False]))]),
            ],
            [deque(deque(deque([False]))), deque(deque(deque([0])))],
        ]

        for instance in valid_instances:
            validator.validate(instance)

        invalid_instances = [
            deque(["a", "b", "a"]),
            deque([[False], [False]]),
            [deque([False]), deque([False])],
            [[deque([False])], [deque([False])]],
            [[[[[deque([False])]]]], [[[[deque([False])]]]]],
            [deque([deque([False])]), deque([deque([False])])],
            [MyMapping("a", False), MyMapping("a", False)],
            [
                MyMapping("a", [deque([False])]),
                MyMapping("a", [deque([False])]),
            ],
            [
                MyMapping("a", [MyMapping("a", deque([False]))]),
                MyMapping("a", [MyMapping("a", deque([False]))]),
            ],
            [deque(deque(deque([False]))), deque(deque(deque([False])))],
        ]

        for instance in invalid_instances:
            with self.assertRaises(exceptions.ValidationError):
                validator.validate(instance)


class AntiDraft6LeakMixin:
    """
    Make sure functionality from draft 6 doesn't leak backwards in time.
    """

    def test_True_is_not_a_schema(self):
        with self.assertRaises(exceptions.SchemaError) as e:
            self.Validator.check_schema(True)
        self.assertIn("True is not of type", str(e.exception))

    def test_False_is_not_a_schema(self):
        with self.assertRaises(exceptions.SchemaError) as e:
            self.Validator.check_schema(False)
        self.assertIn("False is not of type", str(e.exception))

    @unittest.skip(bug(523))
    def test_True_is_not_a_schema_even_if_you_forget_to_check(self):
        resolver = validators.RefResolver("", {})
        with self.assertRaises(Exception) as e:
            self.Validator(True, resolver=resolver).validate(12)
        self.assertNotIsInstance(e.exception, exceptions.ValidationError)

    @unittest.skip(bug(523))
    def test_False_is_not_a_schema_even_if_you_forget_to_check(self):
        resolver = validators.RefResolver("", {})
        with self.assertRaises(Exception) as e:
            self.Validator(False, resolver=resolver).validate(12)
        self.assertNotIsInstance(e.exception, exceptions.ValidationError)


class TestDraft3Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase):
    Validator = validators.Draft3Validator
    valid: tuple[dict, dict] = ({}, {})
    invalid = {"type": "integer"}, "foo"

    def test_any_type_is_valid_for_type_any(self):
        validator = self.Validator({"type": "any"})
        validator.validate(object())

    def test_any_type_is_redefinable(self):
        """
        Sigh, because why not.
        """
        Crazy = validators.extend(
            self.Validator,
            type_checker=self.Validator.TYPE_CHECKER.redefine(
                "any", lambda checker, thing: isinstance(thing, int),
            ),
        )
        validator = Crazy({"type": "any"})
        validator.validate(12)
        with self.assertRaises(exceptions.ValidationError):
            validator.validate("foo")

    def test_is_type_is_true_for_any_type(self):
        self.assertTrue(self.Validator({"type": "any"}).is_valid(object()))

    def test_is_type_does_not_evade_bool_if_it_is_being_tested(self):
        self.assertTrue(self.Validator({}).is_type(True, "boolean"))
        self.assertTrue(self.Validator({"type": "any"}).is_valid(True))


class TestDraft4Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase):
    Validator = validators.Draft4Validator
    valid: tuple[dict, dict] = ({}, {})
    invalid = {"type": "integer"}, "foo"


class TestDraft6Validator(ValidatorTestMixin, TestCase):
    Validator = validators.Draft6Validator
    valid: tuple[dict, dict] = ({}, {})
    invalid = {"type": "integer"}, "foo"


class TestDraft7Validator(ValidatorTestMixin, TestCase):
    Validator = validators.Draft7Validator
    valid: tuple[dict, dict] = ({}, {})
    invalid = {"type": "integer"}, "foo"


class TestDraft201909Validator(ValidatorTestMixin, TestCase):
    Validator = validators.Draft201909Validator
    valid: tuple[dict, dict] = ({}, {})
    invalid = {"type": "integer"}, "foo"


class TestDraft202012Validator(ValidatorTestMixin, TestCase):
    Validator = validators.Draft202012Validator
    valid: tuple[dict, dict] = ({}, {})
    invalid = {"type": "integer"}, "foo"


class TestLatestValidator(TestCase):
    """
    These really apply to multiple versions but are easiest to test on one.
    """

    def test_ref_resolvers_may_have_boolean_schemas_stored(self):
        ref = "someCoolRef"
        schema = {"$ref": ref}
        resolver = validators.RefResolver("", {}, store={ref: False})
        validator = validators._LATEST_VERSION(schema, resolver=resolver)

        with self.assertRaises(exceptions.ValidationError):
            validator.validate(None)


class TestValidatorFor(TestCase):
    def test_draft_3(self):
        schema = {"$schema": "http://json-schema.org/draft-03/schema"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft3Validator,
        )

        schema = {"$schema": "http://json-schema.org/draft-03/schema#"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft3Validator,
        )

    def test_draft_4(self):
        schema = {"$schema": "http://json-schema.org/draft-04/schema"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft4Validator,
        )

        schema = {"$schema": "http://json-schema.org/draft-04/schema#"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft4Validator,
        )

    def test_draft_6(self):
        schema = {"$schema": "http://json-schema.org/draft-06/schema"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft6Validator,
        )

        schema = {"$schema": "http://json-schema.org/draft-06/schema#"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft6Validator,
        )

    def test_draft_7(self):
        schema = {"$schema": "http://json-schema.org/draft-07/schema"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft7Validator,
        )

        schema = {"$schema": "http://json-schema.org/draft-07/schema#"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft7Validator,
        )

    def test_draft_201909(self):
        schema = {"$schema": "https://json-schema.org/draft/2019-09/schema"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft201909Validator,
        )

        schema = {"$schema": "https://json-schema.org/draft/2019-09/schema#"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft201909Validator,
        )

    def test_draft_202012(self):
        schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft202012Validator,
        )

        schema = {"$schema": "https://json-schema.org/draft/2020-12/schema#"}
        self.assertIs(
            validators.validator_for(schema),
            validators.Draft202012Validator,
        )

    def test_True(self):
        self.assertIs(
            validators.validator_for(True),
            validators._LATEST_VERSION,
        )

    def test_False(self):
        self.assertIs(
            validators.validator_for(False),
            validators._LATEST_VERSION,
        )

    def test_custom_validator(self):
        Validator = validators.create(
            meta_schema={"id": "meta schema id"},
            version="12",
            id_of=lambda s: s.get("id", ""),
        )
        schema = {"$schema": "meta schema id"}
        self.assertIs(
            validators.validator_for(schema),
            Validator,
        )

    def test_custom_validator_draft6(self):
        Validator = validators.create(
            meta_schema={"$id": "meta schema $id"},
            version="13",
        )
        schema = {"$schema": "meta schema $id"}
        self.assertIs(
            validators.validator_for(schema),
            Validator,
        )

    def test_validator_for_jsonschema_default(self):
        self.assertIs(validators.validator_for({}), validators._LATEST_VERSION)

    def test_validator_for_custom_default(self):
        self.assertIs(validators.validator_for({}, default=None), None)

    def test_warns_if_meta_schema_specified_was_not_found(self):
        with self.assertWarns(DeprecationWarning) as cm:
            validators.validator_for(schema={"$schema": "unknownSchema"})

        self.assertEqual(cm.filename, __file__)
        self.assertEqual(
            str(cm.warning),
            "The metaschema specified by $schema was not found. "
            "Using the latest draft to validate, but this will raise "
            "an error in the future.",
        )

    def test_does_not_warn_if_meta_schema_is_unspecified(self):
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            validators.validator_for(schema={}, default={})
        self.assertFalse(w)

    def test_validator_for_custom_default_with_schema(self):
        schema, default = {"$schema": "mailto:foo@example.com"}, object()
        self.assertIs(validators.validator_for(schema, default), default)


class TestValidate(TestCase):
    def assertUses(self, schema, Validator):
        result = []
        with mock.patch.object(Validator, "check_schema", result.append):
            validators.validate({}, schema)
        self.assertEqual(result, [schema])

    def test_draft3_validator_is_chosen(self):
        self.assertUses(
            schema={"$schema": "http://json-schema.org/draft-03/schema#"},
            Validator=validators.Draft3Validator,
        )
        # Make sure it works without the empty fragment
        self.assertUses(
            schema={"$schema": "http://json-schema.org/draft-03/schema"},
            Validator=validators.Draft3Validator,
        )

    def test_draft4_validator_is_chosen(self):
        self.assertUses(
            schema={"$schema": "http://json-schema.org/draft-04/schema#"},
            Validator=validators.Draft4Validator,
        )
        # Make sure it works without the empty fragment
        self.assertUses(
            schema={"$schema": "http://json-schema.org/draft-04/schema"},
            Validator=validators.Draft4Validator,
        )

    def test_draft6_validator_is_chosen(self):
        self.assertUses(
            schema={"$schema": "http://json-schema.org/draft-06/schema#"},
            Validator=validators.Draft6Validator,
        )
        # Make sure it works without the empty fragment
        self.assertUses(
            schema={"$schema": "http://json-schema.org/draft-06/schema"},
            Validator=validators.Draft6Validator,
        )

    def test_draft7_validator_is_chosen(self):
        self.assertUses(
            schema={"$schema": "http://json-schema.org/draft-07/schema#"},
            Validator=validators.Draft7Validator,
        )
        # Make sure it works without the empty fragment
        self.assertUses(
            schema={"$schema": "http://json-schema.org/draft-07/schema"},
            Validator=validators.Draft7Validator,
        )

    def test_draft202012_validator_is_chosen(self):
        self.assertUses(
            schema={
                "$schema": "https://json-schema.org/draft/2020-12/schema#",
            },
            Validator=validators.Draft202012Validator,
        )
        # Make sure it works without the empty fragment
        self.assertUses(
            schema={
                "$schema": "https://json-schema.org/draft/2020-12/schema",
            },
            Validator=validators.Draft202012Validator,
        )

    def test_draft202012_validator_is_the_default(self):
        self.assertUses(schema={}, Validator=validators.Draft202012Validator)

    def test_validation_error_message(self):
        with self.assertRaises(exceptions.ValidationError) as e:
            validators.validate(12, {"type": "string"})
        self.assertRegex(
            str(e.exception),
            "(?s)Failed validating '.*' in schema.*On instance",
        )

    def test_schema_error_message(self):
        with self.assertRaises(exceptions.SchemaError) as e:
            validators.validate(12, {"type": 12})
        self.assertRegex(
            str(e.exception),
            "(?s)Failed validating '.*' in metaschema.*On schema",
        )

    def test_it_uses_best_match(self):
        schema = {
            "oneOf": [
                {"type": "number", "minimum": 20},
                {"type": "array"},
            ],
        }
        with self.assertRaises(exceptions.ValidationError) as e:
            validators.validate(12, schema)
        self.assertIn("12 is less than the minimum of 20", str(e.exception))


class TestRefResolver(TestCase):

    base_uri = ""
    stored_uri = "foo://stored"
    stored_schema = {"stored": "schema"}

    def setUp(self):
        self.referrer = {}
        self.store = {self.stored_uri: self.stored_schema}
        self.resolver = validators.RefResolver(
            self.base_uri, self.referrer, self.store,
        )

    def test_it_does_not_retrieve_schema_urls_from_the_network(self):
        ref = validators.Draft3Validator.META_SCHEMA["id"]
        with mock.patch.object(self.resolver, "resolve_remote") as patched:
            with self.resolver.resolving(ref) as resolved:
                pass
        self.assertEqual(resolved, validators.Draft3Validator.META_SCHEMA)
        self.assertFalse(patched.called)

    def test_it_resolves_local_refs(self):
        ref = "#/properties/foo"
        self.referrer["properties"] = {"foo": object()}
        with self.resolver.resolving(ref) as resolved:
            self.assertEqual(resolved, self.referrer["properties"]["foo"])

    def test_it_resolves_local_refs_with_id(self):
        schema = {"id": "http://bar/schema#", "a": {"foo": "bar"}}
        resolver = validators.RefResolver.from_schema(
            schema,
            id_of=lambda schema: schema.get("id", ""),
        )
        with resolver.resolving("#/a") as resolved:
            self.assertEqual(resolved, schema["a"])
        with resolver.resolving("http://bar/schema#/a") as resolved:
            self.assertEqual(resolved, schema["a"])

    def test_it_retrieves_stored_refs(self):
        with self.resolver.resolving(self.stored_uri) as resolved:
            self.assertIs(resolved, self.stored_schema)

        self.resolver.store["cached_ref"] = {"foo": 12}
        with self.resolver.resolving("cached_ref#/foo") as resolved:
            self.assertEqual(resolved, 12)

    def test_it_can_construct_a_base_uri_from_a_schema(self):
        schema = {"id": "foo"}
        resolver = validators.RefResolver.from_schema(
            schema,
            id_of=lambda schema: schema.get("id", ""),
        )
        self.assertEqual(resolver.base_uri, "foo")
        self.assertEqual(resolver.resolution_scope, "foo")
        with resolver.resolving("") as resolved:
            self.assertEqual(resolved, schema)
        with resolver.resolving("#") as resolved:
            self.assertEqual(resolved, schema)
        with resolver.resolving("foo") as resolved:
            self.assertEqual(resolved, schema)
        with resolver.resolving("foo#") as resolved:
            self.assertEqual(resolved, schema)

    def test_it_can_construct_a_base_uri_from_a_schema_without_id(self):
        schema = {}
        resolver = validators.RefResolver.from_schema(schema)
        self.assertEqual(resolver.base_uri, "")
        self.assertEqual(resolver.resolution_scope, "")
        with resolver.resolving("") as resolved:
            self.assertEqual(resolved, schema)
        with resolver.resolving("#") as resolved:
            self.assertEqual(resolved, schema)

    def test_custom_uri_scheme_handlers(self):
        def handler(url):
            self.assertEqual(url, ref)
            return schema

        schema = {"foo": "bar"}
        ref = "foo://bar"
        resolver = validators.RefResolver("", {}, handlers={"foo": handler})
        with resolver.resolving(ref) as resolved:
            self.assertEqual(resolved, schema)

    def test_cache_remote_on(self):
        response = [object()]

        def handler(url):
            try:
                return response.pop()
            except IndexError:  # pragma: no cover
                self.fail("Response must not have been cached!")

        ref = "foo://bar"
        resolver = validators.RefResolver(
            "", {}, cache_remote=True, handlers={"foo": handler},
        )
        with resolver.resolving(ref):
            pass
        with resolver.resolving(ref):
            pass

    def test_cache_remote_off(self):
        response = [object()]

        def handler(url):
            try:
                return response.pop()
            except IndexError:  # pragma: no cover
                self.fail("Handler called twice!")

        ref = "foo://bar"
        resolver = validators.RefResolver(
            "", {}, cache_remote=False, handlers={"foo": handler},
        )
        with resolver.resolving(ref):
            pass

    def test_if_you_give_it_junk_you_get_a_resolution_error(self):
        error = ValueError("Oh no! What's this?")

        def handler(url):
            raise error

        ref = "foo://bar"
        resolver = validators.RefResolver("", {}, handlers={"foo": handler})
        with self.assertRaises(exceptions.RefResolutionError) as err:
            with resolver.resolving(ref):
                self.fail("Shouldn't get this far!")  # pragma: no cover
        self.assertEqual(err.exception, exceptions.RefResolutionError(error))

    def test_helpful_error_message_on_failed_pop_scope(self):
        resolver = validators.RefResolver("", {})
        resolver.pop_scope()
        with self.assertRaises(exceptions.RefResolutionError) as exc:
            resolver.pop_scope()
        self.assertIn("Failed to pop the scope", str(exc.exception))


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


@attr.s
class ReallyFakeRequests:

    _responses = attr.ib()

    def get(self, url):
        response = self._responses.get(url)
        if url is None:  # pragma: no cover
            raise ValueError("Unknown URL: " + repr(url))
        return _ReallyFakeJSONResponse(json.dumps(response))


@attr.s
class _ReallyFakeJSONResponse:

    _response = attr.ib()

    def json(self):
        return json.loads(self._response)
