from unittest import TestCase

from spidermon.contrib.validation import JSONSchemaValidator
from spidermon.contrib.validation import messages

from slugify import slugify


class SchemaTestCaseMetaclass(type):
    def __new__(mcs, name, bases, attrs):
        def _test_function(data_test):
            def _function(self):
                validator = JSONSchemaValidator(data_test.schema or self.schema)
                assert validator.validate(data_test.data) == (
                    data_test.valid,
                    data_test.expected_errors,
                )

            return _function

        cls = super().__new__(mcs, name, bases, attrs)
        for dt in getattr(cls, "data_tests", []):
            function_name = "test_%s" % slugify(dt.name, separator="_").lower()
            setattr(cls, function_name, _test_function(dt))
        return cls


class SchemaTest(TestCase, metaclass=SchemaTestCaseMetaclass):
    schema = {}
    data_tests = []


class DataTest:
    def __init__(self, name, data, valid, expected_errors=None, schema=None):
        self.name = name
        self.data = data
        self.valid = valid
        self.expected_errors = expected_errors or {}
        self.schema = schema


class AdditionalItems(SchemaTest):
    data_tests = [
        DataTest(
            name="additionalItems as schema, additional items match schema",
            schema={"items": [{}], "additionalItems": {"type": "integer"}},
            data=[None, 2, 3, 4],
            valid=True,
        ),
        DataTest(
            name="additionalItems as schema, additional items do not match schema",
            schema={"items": [{}], "additionalItems": {"type": "integer"}},
            data=[None, 2, 3, "foo"],
            valid=False,
            expected_errors={"3": [messages.INVALID_INT]},
        ),
        DataTest(
            name="no additionalItems, all items match schema",
            schema={"items": {}, "additionalItems": False},
            data=[1, 2, 3, 4, 5],
            valid=True,
        ),
        DataTest(
            name="array of items with no additionalItems, no additional items present",
            schema={"items": [{}, {}, {}], "additionalItems": False},
            data=[1, 2, 3],
            valid=True,
        ),
        DataTest(
            name="array of items with no additionalItems, additional items are not permitted",
            schema={"items": [{}, {}, {}], "additionalItems": False},
            data=[1, 2, 3, 4],
            valid=False,
            expected_errors={"": [messages.TOO_MANY_ITEMS]},
        ),
        DataTest(
            name="additionalItems as false without items, valid",
            schema={"additionalItems": False},
            data=[1, 2, 3, 4, 5],
            valid=True,
        ),
        DataTest(
            name="additionalItems as false without items, ignores non-arrays",
            schema={"additionalItems": False},
            data={"foo": "bar"},
            valid=True,
        ),
        DataTest(
            name="additionalItems are allowed by default, only the first item is validated",
            schema={"items": [{"type": "integer"}]},
            data=[1, "foo", False],
            valid=True,
        ),
        DataTest(
            name="array, sequence without extra item",
            schema={
                "type": "array",
                "items": [{"type": "boolean"}, {"type": "integer"}, {"type": "string"}],
                "additionalItems": {"type": "boolean"},
            },
            data=[True, 1, "foo"],
            valid=True,
        ),
        DataTest(
            name="array, sequence with valid extra item",
            schema={
                "type": "array",
                "items": [{"type": "boolean"}, {"type": "integer"}, {"type": "string"}],
                "additionalItems": {"type": "boolean"},
            },
            data=[True, 1, "foo", True],
            valid=True,
        ),
        DataTest(
            name="array, sequence with invalid extra item",
            schema={
                "type": "array",
                "items": [{"type": "boolean"}, {"type": "integer"}, {"type": "string"}],
                "additionalItems": {"type": "boolean"},
            },
            data=[True, 1, "foo", 1],
            valid=False,
            expected_errors={"3": [messages.INVALID_BOOLEAN]},
        ),
    ]


class AdditionalProperties(SchemaTest):
    schema_false = {
        "properties": {"foo": {}, "bar": {}},
        "patternProperties": {"^v": {}},
        "additionalProperties": False,
    }
    schema_allows_schema = {
        "properties": {"foo": {}, "bar": {}},
        "additionalProperties": {"type": "boolean"},
    }
    schema_alone = {"additionalProperties": {"type": "boolean"}}
    schema_no = {"properties": {"foo": {}, "bar": {}}}
    data_tests = [
        DataTest(
            name="schema_false, no additional properties is valid",
            schema=schema_false,
            data={"foo": 1},
            valid=True,
        ),
        DataTest(
            name="schema_false, an additional property is invalid",
            schema=schema_false,
            data={"foo": 1, "bar": 2, "quux": "boom"},
            valid=False,
            expected_errors={
                # Used with jsonschema 2.4.0:
                # '': [messages.UNEXPECTED_FIELD],
                # This changed in jsonschema 2.6.0:
                # https://github.com/Julian/jsonschema/pull/317
                "": [messages.REGEX_NOT_MATCHED]
            },
        ),
        DataTest(
            name="schema_false, ignores non-objects",
            schema=schema_false,
            data=[1, 2, 3],
            valid=True,
        ),
        DataTest(
            name="schema_false, patternProperties are not additional properties",
            schema=schema_false,
            data={"foo": 1, "vroom": 2},
            valid=True,
        ),
        DataTest(
            name="schema_allows_schema, no additional properties",
            schema=schema_allows_schema,
            data={"foo": 1},
            valid=True,
        ),
        DataTest(
            name="schema_allows_schema, additional valid property",
            schema=schema_allows_schema,
            data={"foo": 1, "bar": 2, "quux": True},
            valid=True,
        ),
        DataTest(
            name="schema_allows_schema, additional invalid property",
            schema=schema_allows_schema,
            data={"foo": 1, "bar": 2, "quux": 12},
            valid=False,
            expected_errors={"quux": [messages.INVALID_BOOLEAN]},
        ),
        DataTest(
            name="schema_alone, additional valid property",
            schema=schema_alone,
            data={"foo": True},
            valid=True,
        ),
        DataTest(
            name="schema_alone, additional invalid property",
            schema=schema_alone,
            data={"foo": 1},
            valid=False,
            expected_errors={"foo": [messages.INVALID_BOOLEAN]},
        ),
        DataTest(
            name="schema_no, additional properties are allowed",
            schema=schema_no,
            data={"foo": 1, "bar": 2, "quux": True},
            valid=True,
        ),
    ]


class AllOf(SchemaTest):
    schema = {
        "type": "object",
        "properties": {"A": {"type": "boolean"}, "B": {"type": "boolean"}},
        "allOf": [{"required": ["A"]}, {"required": ["B"]}],
    }
    data_tests = [
        DataTest(
            name="Empty object",
            data={},
            valid=False,
            expected_errors={
                "A": [messages.MISSING_REQUIRED_FIELD],
                "B": [messages.MISSING_REQUIRED_FIELD],
            },
        ),
        DataTest(
            name="Mismatch B",
            data={"A": True},
            valid=False,
            expected_errors={"B": [messages.MISSING_REQUIRED_FIELD]},
        ),
        DataTest(
            name="Mismatch A",
            data={"B": True},
            valid=False,
            expected_errors={"A": [messages.MISSING_REQUIRED_FIELD]},
        ),
        DataTest(name="Both values", data={"A": False, "B": False}, valid=True),
        DataTest(
            name="Wrong type for A",
            data={"A": 1, "B": False},
            valid=False,
            expected_errors={"A": [messages.INVALID_BOOLEAN]},
        ),
        DataTest(
            name="Wrong type for both",
            data={"A": 1, "B": 1},
            valid=False,
            expected_errors={
                "A": [messages.INVALID_BOOLEAN],
                "B": [messages.INVALID_BOOLEAN],
            },
        ),
    ]


class AnyOf(SchemaTest):
    schema = {
        "type": "object",
        "properties": {"A": {"type": "boolean"}, "B": {"type": "boolean"}},
        "anyOf": [{"required": ["A"]}, {"required": ["B"]}],
    }
    data_tests = [
        DataTest(
            name="Empty object",
            data={},
            valid=False,
            expected_errors={"": [messages.NOT_VALID_UNDER_ANY_SCHEMA]},
        ),
        DataTest(name="Mismatch B", data={"A": True}, valid=True),
        DataTest(name="Mismatch A", data={"B": True}, valid=True),
        DataTest(name="Both values", data={"A": False, "B": False}, valid=True),
        DataTest(
            name="Wrong type for A",
            data={"A": 1, "B": False},
            valid=False,
            expected_errors={"A": [messages.INVALID_BOOLEAN]},
        ),
        DataTest(
            name="Wrong type for both",
            data={"A": 1, "B": 1},
            valid=False,
            expected_errors={
                "A": [messages.INVALID_BOOLEAN],
                "B": [messages.INVALID_BOOLEAN],
            },
        ),
    ]


class Dependencies(SchemaTest):
    schema_single = {"dependencies": {"bar": ["foo"]}}
    schema_multiple = {"dependencies": {"quux": ["foo", "bar"]}}
    schema_multiple_subschema = {
        "dependencies": {
            "bar": {
                "properties": {"foo": {"type": "integer"}, "bar": {"type": "integer"}}
            }
        }
    }
    data_tests = [
        DataTest(name="single, neither", schema=schema_single, data={}, valid=True),
        DataTest(
            name="single, nondependant",
            schema=schema_single,
            data={"foo": 1},
            valid=True,
        ),
        DataTest(
            name="single, with dependency",
            schema=schema_single,
            data={"foo": 1, "bar": 2},
            valid=True,
        ),
        DataTest(
            name="single, missing dependency",
            schema=schema_single,
            data={"bar": 2},
            valid=False,
            expected_errors={"": [messages.MISSING_DEPENDENT_FIELD]},
        ),
        DataTest(
            name="single, ignores non-objects",
            schema=schema_single,
            data="foo",
            valid=True,
        ),
        DataTest(name="multiple, neither", schema=schema_multiple, data={}, valid=True),
        DataTest(
            name="multiple, nondependants",
            schema=schema_multiple,
            data={"foo": 1, "bar": 2},
            valid=True,
        ),
        DataTest(
            name="multiple, with dependencies",
            schema=schema_multiple,
            data={"foo": 1, "bar": 2, "quux": 3},
            valid=True,
        ),
        DataTest(
            name="multiple, missing dependency",
            schema=schema_multiple,
            data={"foo": 1, "quux": 2},
            valid=False,
            expected_errors={"": [messages.MISSING_DEPENDENT_FIELD]},
        ),
        DataTest(
            name="multiple, missing other dependency",
            schema=schema_multiple,
            data={"bar": 1, "quux": 2},
            valid=False,
            expected_errors={"": [messages.MISSING_DEPENDENT_FIELD]},
        ),
        DataTest(
            name="multiple, missing both dependencies",
            schema=schema_multiple,
            data={"quux": 1},
            valid=False,
            expected_errors={
                "": [messages.MISSING_DEPENDENT_FIELD, messages.MISSING_DEPENDENT_FIELD]
            },
        ),
        DataTest(
            name="multiple subschema, valid",
            schema=schema_multiple_subschema,
            data={"foo": 1, "bar": 2},
            valid=True,
        ),
        DataTest(
            name="multiple subschema, no dependency",
            schema=schema_multiple_subschema,
            data={"foo": "quux"},
            valid=True,
        ),
        DataTest(
            name="multiple subschema, wrong type",
            schema=schema_multiple_subschema,
            data={"foo": "quux", "bar": 2},
            valid=False,
            expected_errors={"foo": [messages.INVALID_INT]},
        ),
        DataTest(
            name="multiple subschema, wrong type other",
            schema=schema_multiple_subschema,
            data={"foo": 2, "bar": "quux"},
            valid=False,
            expected_errors={"bar": [messages.INVALID_INT]},
        ),
        DataTest(
            name="multiple subschema, wrong type both",
            schema=schema_multiple_subschema,
            data={"foo": "quux", "bar": "quux"},
            valid=False,
            expected_errors={
                "foo": [messages.INVALID_INT],
                "bar": [messages.INVALID_INT],
            },
        ),
    ]


class Enum(SchemaTest):
    schema_simple = {"enum": [1, 2, 3]}
    schema_heterogeneous = {"enum": [6, "foo", [], True, {"foo": 12}]}
    schema_properties = {
        "type": "object",
        "properties": {"foo": {"enum": ["foo"]}, "bar": {"enum": ["bar"]}},
        "required": ["bar"],
    }
    data_tests = [
        DataTest(name="simple, valid", schema=schema_simple, data=1, valid=True),
        DataTest(
            name="simple, invalid",
            schema=schema_simple,
            data=4,
            valid=False,
            expected_errors={"": [messages.VALUE_NOT_IN_CHOICES]},
        ),
        DataTest(
            name="heterogeneous, valid",
            schema=schema_heterogeneous,
            data=[],
            valid=True,
        ),
        DataTest(
            name="heterogeneous, invalid",
            schema=schema_heterogeneous,
            data=None,
            valid=False,
            expected_errors={"": [messages.VALUE_NOT_IN_CHOICES]},
        ),
        DataTest(
            name="heterogeneous, deep valid",
            schema=schema_heterogeneous,
            data={"foo": False},
            valid=False,
            expected_errors={"": [messages.VALUE_NOT_IN_CHOICES]},
        ),
        DataTest(
            name="properties, both valid",
            schema=schema_properties,
            data={"foo": "foo", "bar": "bar"},
            valid=True,
        ),
        DataTest(
            name="properties, missing optional valid",
            schema=schema_properties,
            data={"bar": "bar"},
            valid=True,
        ),
        DataTest(
            name="properties, missing required",
            schema=schema_properties,
            data={"foo": "foo"},
            valid=False,
            expected_errors={"bar": [messages.MISSING_REQUIRED_FIELD]},
        ),
        DataTest(
            name="properties, missing all",
            schema=schema_properties,
            data={},
            valid=False,
            expected_errors={"bar": [messages.MISSING_REQUIRED_FIELD]},
        ),
    ]


class Format(SchemaTest):
    schema = {
        "type": "object",
        "properties": {
            "datetimes": {
                "type": "array",
                "items": {"type": "string", "format": "date-time"},
            },
            "emails": {"type": "array", "items": {"type": "string", "format": "email"}},
            "ipv4s": {"type": "array", "items": {"type": "string", "format": "ipv4"}},
            "ipv6s": {"type": "array", "items": {"type": "string", "format": "ipv6"}},
            "hostnames": {
                "type": "array",
                "items": {"type": "string", "format": "hostname"},
            },
            "urls": {"type": "array", "items": {"type": "string", "format": "url"}},
            "regexes": {
                "type": "array",
                "items": {"type": "string", "format": "regex"},
            },
        },
    }
    data_tests = [
        DataTest(
            name="datetime. valid",
            data={
                "datetimes": ["2013-03-25T12:42:31+00:32", "2013-03-25T22:04:10.04399Z"]
            },
            valid=True,
        ),
        DataTest(
            name="datetime. invalid",
            data={
                "datetimes": [
                    "2015-05-13 13:35:15.718978",
                    "2015-05-13 13:35:15",
                    "13-05-2013",
                ]
            },
            valid=False,
            expected_errors={
                "datetimes.0": [messages.INVALID_DATETIME],
                "datetimes.1": [messages.INVALID_DATETIME],
                "datetimes.2": [messages.INVALID_DATETIME],
            },
        ),
        DataTest(
            name="email. valid",
            data={
                "emails": [
                    "johndoe@domain.com",
                    "john.doe@domain.com",
                    "john.doe@sub.domain.com",
                    "j@sub.domain.com",
                    "j@d.com",
                    "j@domain.co.uk",
                ]
            },
            valid=True,
        ),
        DataTest(
            name="email. invalid",
            data={
                "emails": [
                    "",
                    "johndoe",
                    "johndoe@domain",
                    "johndoe@domain.",
                    "@domain",
                    "@domain.com",
                    "domain.com",
                ]
            },
            valid=False,
            expected_errors={
                "emails.0": [messages.INVALID_EMAIL],
                "emails.1": [messages.INVALID_EMAIL],
                "emails.2": [messages.INVALID_EMAIL],
                "emails.3": [messages.INVALID_EMAIL],
                "emails.4": [messages.INVALID_EMAIL],
                "emails.5": [messages.INVALID_EMAIL],
                "emails.6": [messages.INVALID_EMAIL],
            },
        ),
        DataTest(
            name="ipv4. valid",
            data={
                "ipv4s": [
                    "98.139.180.149",
                    "69.89.31.226",
                    "192.168.1.1",
                    "127.0.0.0",
                    "0.0.0.0",
                    "255.255.255.255",
                ]
            },
            valid=True,
        ),
        DataTest(
            name="ipv4. invalid",
            data={
                "ipv4s": [
                    "",
                    "0",
                    "0.",
                    "0.0",
                    "0.0.",
                    "0.0.0",
                    "0.0.0.0.",
                    "0.0.0.0.0",
                    "256.256.256.256",
                    "2002:4559:1FE2::4559:1FE2",
                    "2002:4559:1FE2:0:0:0:4559:1FE2",
                    "2002:4559:1FE2:0000:0000:0000:4559:1FE2",
                ]
            },
            valid=False,
            expected_errors={
                "ipv4s.0": [messages.INVALID_IPV4],
                "ipv4s.1": [messages.INVALID_IPV4],
                "ipv4s.2": [messages.INVALID_IPV4],
                "ipv4s.3": [messages.INVALID_IPV4],
                "ipv4s.4": [messages.INVALID_IPV4],
                "ipv4s.5": [messages.INVALID_IPV4],
                "ipv4s.6": [messages.INVALID_IPV4],
                "ipv4s.7": [messages.INVALID_IPV4],
                "ipv4s.8": [messages.INVALID_IPV4],
                "ipv4s.9": [messages.INVALID_IPV4],
                "ipv4s.10": [messages.INVALID_IPV4],
                "ipv4s.11": [messages.INVALID_IPV4],
            },
        ),
        DataTest(
            name="ipv6. valid",
            data={
                "ipv6s": [
                    "2002:4559:1FE2::4559:1FE2",
                    "2002:4559:1FE2:0:0:0:4559:1FE2",
                    "2002:4559:1FE2:0000:0000:0000:4559:1FE2",
                ]
            },
            valid=True,
        ),
        DataTest(
            name="ipv6. invalid",
            data={
                "ipv6s": [
                    "",
                    "98.139.180.149",
                    "69.89.31.226",
                    "192.168.1.1",
                    "127.0.0.0",
                    "0.0.0.0",
                    "255.255.255.255",
                ]
            },
            valid=False,
            expected_errors={
                "ipv6s.0": [messages.INVALID_IPV6],
                "ipv6s.1": [messages.INVALID_IPV6],
                "ipv6s.2": [messages.INVALID_IPV6],
                "ipv6s.3": [messages.INVALID_IPV6],
                "ipv6s.4": [messages.INVALID_IPV6],
                "ipv6s.5": [messages.INVALID_IPV6],
                "ipv6s.6": [messages.INVALID_IPV6],
            },
        ),
        DataTest(
            name="hostname. valid",
            data={
                "hostnames": [
                    "localhost",
                    "google",
                    "google.com",
                    "xn--hxajbheg2az3al.xn--jxalpdlp",
                    "a" * 63,
                ]
            },
            valid=True,
        ),
        DataTest(
            name="hostname. invalid",
            data={"hostnames": ["...", "a" * 64, "-hi-", "_hi_", "*hi*"]},
            valid=False,
            expected_errors={
                "hostnames.0": [messages.INVALID_HOSTNAME],
                "hostnames.1": [messages.INVALID_HOSTNAME],
                "hostnames.2": [messages.INVALID_HOSTNAME],
                "hostnames.3": [messages.INVALID_HOSTNAME],
                "hostnames.4": [messages.INVALID_HOSTNAME],
            },
        ),
        DataTest(
            name="urls. valid",
            data={
                "urls": [
                    "http://www.domain",
                    "http://www.com",
                    "http://www.domain.com.",
                    "http://www.domain.com/.",
                    "http://www.domain.com/..",
                    "http://www.domain.com//cataglog//index.html",
                    "http://www.domain.net/",
                    "http://www.domain.com/level2/leafnode-L2.xhtml/",
                    "http://www.domain.com/level2/level3/leafnode-L3.xhtml/",
                    "http://www.domain.com?pageid=123&testid=1524",
                    "http://www.domain.com/do.html#A",
                ]
            },
            valid=True,
        ),
        DataTest(
            name="urls. invalid",
            data={
                "urls": [
                    "",
                    "http://",
                    "http://www.",
                    "www.",
                    "http://www. .com",
                    "domain.com",
                    "www.domain.com",
                    "http:/www.domain.com",
                    "http//www.domain.com",
                    "http:www.domain.com",
                    "htp://domain.com/",
                    "http://sub.domain.com\\en-us\\default.aspx\\",
                    "http:\\\\msdn.domain.com\\en-us\\library\\default.aspx\\",
                    "http:\\\\www.domain.com\\leafnode-L1.html",
                    "./",
                    "../",
                    "http:\\\\www.domain.com\\leafnode-L1.xhtml\\",
                ]
            },
            valid=False,
            expected_errors={
                "urls.0": [messages.INVALID_URL],
                "urls.1": [messages.INVALID_URL],
                "urls.2": [messages.INVALID_URL],
                "urls.3": [messages.INVALID_URL],
                "urls.4": [messages.INVALID_URL],
                "urls.5": [messages.INVALID_URL],
                "urls.6": [messages.INVALID_URL],
                "urls.7": [messages.INVALID_URL],
                "urls.8": [messages.INVALID_URL],
                "urls.9": [messages.INVALID_URL],
                "urls.10": [messages.INVALID_URL],
                "urls.11": [messages.INVALID_URL],
                "urls.12": [messages.INVALID_URL],
                "urls.13": [messages.INVALID_URL],
                "urls.14": [messages.INVALID_URL],
                "urls.15": [messages.INVALID_URL],
                "urls.16": [messages.INVALID_URL],
            },
        ),
        DataTest(name="regexes. valid", data={"regexes": ["[0-9]+"]}, valid=True),
        DataTest(
            name="regexes. invalid",
            data={"regexes": ["[0-9]++"]},
            valid=False,
            expected_errors={"regexes.0": [messages.INVALID_REGEX]},
        ),
    ]


class Items(SchemaTest):
    schema_items = {"items": {"type": "integer"}}
    schema_array = {"items": [{"type": "integer"}, {"type": "string"}]}
    data_tests = [
        DataTest(
            name="schema_items. valid items",
            schema=schema_items,
            data=[1, 2, 3],
            valid=True,
        ),
        DataTest(
            name="schema_items. wrong type of items",
            schema=schema_items,
            data=[1, "x"],
            valid=False,
            expected_errors={"1": [messages.INVALID_INT]},
        ),
        DataTest(
            name="schema_items. ignores non-arrays",
            schema=schema_items,
            data={"foo": "bar"},
            valid=True,
        ),
        DataTest(
            name="schema_array. correct types",
            schema=schema_array,
            data=[1, "foo"],
            valid=True,
        ),
        DataTest(
            name="schema_array. wrong types",
            schema=schema_array,
            data=["foo", 1],
            valid=False,
            expected_errors={
                "0": [messages.INVALID_INT],
                "1": [messages.INVALID_STRING],
            },
        ),
    ]


class MaxItems(SchemaTest):
    schema = {"maxItems": 2}
    data_tests = [
        DataTest(name="shorter is valid", data=[1], valid=True),
        DataTest(name="exact length is valid", data=[1, 2], valid=True),
        DataTest(
            name="too long is invalid",
            data=[1, 2, 3],
            valid=False,
            expected_errors={"": [messages.FIELD_TOO_LONG]},
        ),
        DataTest(name="ignores non-arrays", data="foobar", valid=True),
    ]


class MaxLength(SchemaTest):
    schema = {"maxLength": 2}
    data_tests = [
        DataTest(name="shorter is valid", data="f", valid=True),
        DataTest(name="exact length is valid", data="fo", valid=True),
        DataTest(
            name="too long is invalid",
            data="foo",
            valid=False,
            expected_errors={"": [messages.FIELD_TOO_LONG]},
        ),
        DataTest(name="ignores non-strings", data=100, valid=True),
    ]


class MaxProperties(SchemaTest):
    schema = {"maxProperties": 2}
    data_tests = [
        DataTest(name="shorter is valid", data={"foo": 1}, valid=True),
        DataTest(name="exact length is valid", data={"foo": 1, "bar": 2}, valid=True),
        DataTest(
            name="too long is invalid",
            data={"foo": 1, "bar": 2, "baz": 3},
            valid=False,
            expected_errors={"": [messages.TOO_MANY_PROPERTIES]},
        ),
        DataTest(name="ignores non-objects", data="foobar", valid=True),
    ]


class Maximum(SchemaTest):
    # exclusiveMaximum behaviour changed from draft-04 to draft-06
    # http://json-schema.org/draft-06/json-schema-release-notes.html#backwards-incompatible-changes
    schema = {"maximum": 3.0}
    draft4_schema_exclusive = {
        "$schema": "http://json-schema.org/draft-04/schema",
        "maximum": 3.0,
        "exclusiveMaximum": True,
    }
    draft6_schema_exclusive = {
        "$schema": "http://json-schema.org/draft-06/schema",
        "exclusiveMaximum": 3.0,
    }

    data_tests = [
        DataTest(name="below", data=2.6, valid=True),
        DataTest(
            name="above",
            data=3.5,
            valid=False,
            expected_errors={"": [messages.NUMBER_TOO_HIGH]},
        ),
        DataTest(
            name="ignores non-numbers", data={"foo": 1, "bar": 2, "baz": 3}, valid=True
        ),
        DataTest(
            name="draft4 exclusive. below",
            schema=draft4_schema_exclusive,
            data=2.2,
            valid=True,
        ),
        DataTest(
            name="draft4_exclusive. boundary point",
            schema=draft4_schema_exclusive,
            data=3.0,
            valid=False,
            expected_errors={"": [messages.NUMBER_TOO_HIGH]},
        ),
        DataTest(
            name="draft4_exclusive. above",
            schema=draft4_schema_exclusive,
            data=3.5,
            valid=False,
            expected_errors={"": [messages.NUMBER_TOO_HIGH]},
        ),
        DataTest(
            name="draft6 exclusive. below",
            schema=draft6_schema_exclusive,
            data=2.2,
            valid=True,
        ),
        DataTest(
            name="draft6_exclusive. boundary point",
            schema=draft6_schema_exclusive,
            data=3.0,
            valid=False,
            expected_errors={"": [messages.NUMBER_TOO_HIGH]},
        ),
        DataTest(
            name="draft6_exclusive. above",
            schema=draft6_schema_exclusive,
            data=3.5,
            valid=False,
            expected_errors={"": [messages.NUMBER_TOO_HIGH]},
        ),
    ]


class MinItems(SchemaTest):
    schema = {"minItems": 1}
    data_tests = [
        DataTest(name="longer is valid", data=[1, 2], valid=True),
        DataTest(name="exact length is valid", data=[1], valid=True),
        DataTest(
            name="too short is invalid",
            data=[],
            valid=False,
            expected_errors={"": [messages.FIELD_TOO_SHORT]},
        ),
        DataTest(name="ignores non-arrays", data="", valid=True),
    ]


class MinProperties(SchemaTest):
    schema = {"minProperties": 1}
    data_tests = [
        DataTest(name="longer is valid", data={"foo": 1, "bar": 2}, valid=True),
        DataTest(name="exact length is valid", data={"foo": 1}, valid=True),
        DataTest(
            name="too short is invalid",
            data={},
            valid=False,
            expected_errors={"": [messages.NOT_ENOUGH_PROPERTIES]},
        ),
        DataTest(name="ignores non-objects", data="", valid=True),
    ]


class Minimum(SchemaTest):
    # exclusiveMinimum behaviour changed from draft-04 to draft-06
    # http://json-schema.org/draft-06/json-schema-release-notes.html#backwards-incompatible-changes
    schema = {"minimum": 1.1}
    draft4_schema_exclusive = {
        "$schema": "http://json-schema.org/draft-04/schema",
        "minimum": 1.1,
        "exclusiveMinimum": True,
    }
    draft6_schema_exclusive = {
        "$schema": "http://json-schema.org/draft-06/schema",
        "exclusiveMinimum": 1.1,
    }

    data_tests = [
        DataTest(name="above", data=2.6, valid=True),
        DataTest(
            name="below",
            data=0.6,
            valid=False,
            expected_errors={"": [messages.NUMBER_TOO_LOW]},
        ),
        DataTest(name="ignores non-numbers", data="x", valid=True),
        DataTest(
            name="exclusive. above",
            schema=draft4_schema_exclusive,
            data=1.2,
            valid=True,
        ),
        DataTest(
            name="exclusive. above",
            schema=draft6_schema_exclusive,
            data=1.2,
            valid=True,
        ),
        DataTest(
            name="draft4 exclusive. boundary point",
            schema=draft4_schema_exclusive,
            data=1.1,
            valid=False,
            expected_errors={"": [messages.NUMBER_TOO_LOW]},
        ),
        DataTest(
            name="draft6 exclusive. boundary point",
            schema=draft6_schema_exclusive,
            data=1.1,
            valid=False,
            expected_errors={"": [messages.NUMBER_TOO_LOW]},
        ),
        DataTest(
            name="draft4 exclusive. below",
            schema=draft4_schema_exclusive,
            data=0.6,
            valid=False,
            expected_errors={"": [messages.NUMBER_TOO_LOW]},
        ),
        DataTest(
            name="draft6 exclusive. below",
            schema=draft6_schema_exclusive,
            data=0.6,
            valid=False,
            expected_errors={"": [messages.NUMBER_TOO_LOW]},
        ),
    ]


class MultipleOf(SchemaTest):
    schema_int = {"multipleOf": 2}
    schema_number = {"multipleOf": 1.5}
    schema_small_number = {"multipleOf": 0.0001}
    data_tests = [
        DataTest(name="int. valid", schema=schema_int, data=10, valid=True),
        DataTest(name="int. valid float", schema=schema_int, data=10.0, valid=True),
        DataTest(
            name="int. invalid",
            schema=schema_int,
            data=7,
            valid=False,
            expected_errors={"": [messages.NOT_MULTIPLE_OF]},
        ),
        DataTest(name="int. ignores non-numbers", data="x", valid=True),
        DataTest(
            name="number. zero is multiple of anything",
            schema=schema_number,
            data=0,
            valid=True,
        ),
        DataTest(name="number. valid", schema=schema_number, data=4.5, valid=True),
        DataTest(
            name="number. invalid",
            schema=schema_number,
            data=35,
            valid=False,
            expected_errors={"": [messages.NOT_MULTIPLE_OF]},
        ),
        DataTest(
            name="small number. valid",
            schema=schema_small_number,
            data=0.0075,
            valid=True,
        ),
        DataTest(
            name="small number. invalid",
            schema=schema_small_number,
            data=0.00751,
            valid=False,
            expected_errors={"": [messages.NOT_MULTIPLE_OF]},
        ),
    ]


class Not(SchemaTest):
    schema_not = {"not": {"type": "integer"}}
    schema_multiple = {"not": {"type": ["integer", "boolean"]}}
    schema_complex = {
        "not": {"type": "object", "properties": {"foo": {"type": "string"}}}
    }
    schema_forbidden = {"properties": {"foo": {"not": {}}}}
    data_tests = [
        DataTest(name="not. allowed", schema=schema_not, data="foo", valid=True),
        DataTest(
            name="not. disallowed",
            schema=schema_not,
            data=1,
            valid=False,
            expected_errors={"": [messages.NOT_ALLOWED_VALUE]},
        ),
        DataTest(
            name="multiple. allowed", schema=schema_multiple, data="foo", valid=True
        ),
        DataTest(
            name="multiple. mismatch",
            schema=schema_multiple,
            data=1,
            valid=False,
            expected_errors={"": [messages.NOT_ALLOWED_VALUE]},
        ),
        DataTest(
            name="multiple. other mismatch",
            schema=schema_multiple,
            data=True,
            valid=False,
            expected_errors={"": [messages.NOT_ALLOWED_VALUE]},
        ),
        DataTest(name="complex. match", schema=schema_complex, data=1, valid=True),
        DataTest(
            name="complex. other match",
            schema=schema_complex,
            data={"foo": 1},
            valid=True,
        ),
        DataTest(
            name="complex. mismatch",
            schema=schema_complex,
            data={"foo": "bar"},
            valid=False,
            expected_errors={"": [messages.NOT_ALLOWED_VALUE]},
        ),
        DataTest(
            name="forbidden. present",
            schema=schema_forbidden,
            data={"foo": 1, "bar": 2},
            valid=False,
            expected_errors={"foo": [messages.NOT_ALLOWED_VALUE]},
        ),
        DataTest(
            name="forbidden. absent",
            schema=schema_forbidden,
            data={"bar": 1, "baz": 2},
            valid=True,
        ),
    ]


class OneOf(SchemaTest):
    schema = {
        "type": "object",
        "properties": {"A": {"type": "boolean"}, "B": {"type": "boolean"}},
        "oneOf": [{"required": ["A"]}, {"required": ["B"]}],
    }
    data_tests = [
        DataTest(
            name="Empty object",
            data={},
            valid=False,
            expected_errors={"": [messages.NOT_VALID_UNDER_ANY_SCHEMA]},
        ),
        DataTest(name="Mismatch B", data={"A": True}, valid=True),
        DataTest(name="Mismatch A", data={"B": True}, valid=True),
        DataTest(
            name="Both values",
            data={"A": False, "B": False},
            valid=False,
            expected_errors={"": [messages.VALID_FOR_SEVERAL_EXCLUSIVE_SCHEMAS]},
        ),
        DataTest(
            name="Wrong type for A",
            data={"A": 1, "B": False},
            valid=False,
            expected_errors={
                "": [messages.VALID_FOR_SEVERAL_EXCLUSIVE_SCHEMAS],
                "A": [messages.INVALID_BOOLEAN],
            },
        ),
        DataTest(
            name="Wrong type for both",
            data={"A": 1, "B": 1},
            valid=False,
            expected_errors={
                "": [messages.VALID_FOR_SEVERAL_EXCLUSIVE_SCHEMAS],
                "A": [messages.INVALID_BOOLEAN],
                "B": [messages.INVALID_BOOLEAN],
            },
        ),
    ]


class Pattern(SchemaTest):
    schema = {"pattern": "^a*$"}
    data_tests = [
        DataTest(name="valid", data="aaa", valid=True),
        DataTest(
            name="invalid",
            data="abc",
            valid=False,
            expected_errors={"": [messages.REGEX_NOT_MATCHED]},
        ),
        DataTest(name="ignores non-strings", data=True, valid=True),
    ]


class PatternProperties(SchemaTest):
    schema_single = {"patternProperties": {"f.*o": {"type": "integer"}}}
    schema_multiple = {
        "patternProperties": {"a*": {"type": "integer"}, "aaa*": {"maximum": 20}}
    }
    schema_complex = {
        "patternProperties": {
            "[0-9]{2,}": {"type": "boolean"},
            "X_": {"type": "string"},
        }
    }
    data_tests = [
        DataTest(
            name="single. valid", schema=schema_single, data={"foo": 1}, valid=True
        ),
        DataTest(
            name="single. multiple valid",
            schema=schema_single,
            data={"foo": 1, "foooooo": 2},
            valid=True,
        ),
        DataTest(
            name="single. invalid",
            schema=schema_single,
            data={"foo": "bar", "fooooo": 2},
            valid=False,
            expected_errors={"foo": [messages.INVALID_INT]},
        ),
        DataTest(
            name="single. multiple invalid",
            schema=schema_single,
            data={"foo": "bar", "foooooo": "baz"},
            valid=False,
            expected_errors={
                "foo": [messages.INVALID_INT],
                "foooooo": [messages.INVALID_INT],
            },
        ),
        DataTest(
            name="single. ignores non-objects",
            schema=schema_single,
            data=12,
            valid=True,
        ),
        DataTest(
            name="multiple. valid", schema=schema_multiple, data={"a": 21}, valid=True
        ),
        DataTest(
            name="multiple. simultaneous valid",
            schema=schema_multiple,
            data={"aaaa": 18},
            valid=True,
        ),
        DataTest(
            name="multiple. multiple valid",
            schema=schema_multiple,
            data={"a": 21, "aaaa": 18},
            valid=True,
        ),
        DataTest(
            name="multiple. invalid one",
            schema=schema_multiple,
            data={"a": "bar"},
            valid=False,
            expected_errors={"a": [messages.INVALID_INT]},
        ),
        DataTest(
            name="multiple. invalid other",
            schema=schema_multiple,
            data={"aaaa": 31},
            valid=False,
            expected_errors={"aaaa": [messages.NUMBER_TOO_HIGH]},
        ),
        DataTest(
            name="multiple. invalid both",
            schema=schema_multiple,
            data={"aaa": "foo", "aaaa": 31},
            valid=False,
            expected_errors={
                "aaa": [messages.INVALID_INT],
                "aaaa": [messages.NUMBER_TOO_HIGH],
            },
        ),
        DataTest(
            name="complex. non recognized members are ignored",
            schema=schema_complex,
            data={"answer 1": "42"},
            valid=True,
        ),
        DataTest(
            name="complex. recognized members are accounted for",
            schema=schema_complex,
            data={"a31b": None},
            valid=False,
            expected_errors={"a31b": [messages.INVALID_BOOLEAN]},
        ),
        DataTest(
            name="complex. regexes are case sensitive",
            schema=schema_complex,
            data={"a_x_3": 3},
            valid=True,
        ),
        DataTest(
            name="complex. regexes are case sensitive 2",
            schema=schema_complex,
            data={"a_X_3": 3},
            valid=False,
            expected_errors={"a_X_3": [messages.INVALID_STRING]},
        ),
    ]


class Properties(SchemaTest):
    schema = {"properties": {"foo": {"type": "integer"}, "bar": {"type": "string"}}}
    schema_mixed = {
        "properties": {
            "foo": {"type": "array", "maxItems": 3},
            "bar": {"type": "array"},
        },
        "patternProperties": {"f.o": {"minItems": 2}},
        "additionalProperties": {"type": "integer"},
    }
    data_tests = [
        DataTest(name="both present", data={"foo": 1, "bar": "baz"}, valid=True),
        DataTest(
            name="one invalid",
            data={"foo": 1, "bar": {}},
            valid=False,
            expected_errors={"bar": [messages.INVALID_STRING]},
        ),
        DataTest(
            name="both invalid",
            data={"foo": [], "bar": {}},
            valid=False,
            expected_errors={
                "foo": [messages.INVALID_INT],
                "bar": [messages.INVALID_STRING],
            },
        ),
        DataTest(name="other properties", data={"quux": []}, valid=True),
        DataTest(name="ignores non-objects", data=[], valid=True),
        DataTest(
            name="mixed. property validates property",
            schema=schema_mixed,
            data={"foo": [1, 2]},
            valid=True,
        ),
        DataTest(
            name="mixed. property invalidates property",
            schema=schema_mixed,
            data={"foo": [1, 2, 3, 4]},
            valid=False,
            expected_errors={"foo": [messages.FIELD_TOO_LONG]},
        ),
        DataTest(
            name="mixed. patternProperty invalidates property",
            schema=schema_mixed,
            data={"foo": []},
            valid=False,
            expected_errors={"foo": [messages.FIELD_TOO_SHORT]},
        ),
        DataTest(
            name="mixed. patternProperty validates nonproperty",
            schema=schema_mixed,
            data={"fxo": [1, 2]},
            valid=True,
        ),
        DataTest(
            name="mixed. patternProperty invalidates nonproperty",
            schema=schema_mixed,
            data={"fxo": []},
            valid=False,
            expected_errors={"fxo": [messages.FIELD_TOO_SHORT]},
        ),
        DataTest(
            name="mixed. additionalProperty ignores property",
            schema=schema_mixed,
            data={"bar": []},
            valid=True,
        ),
        DataTest(
            name="mixed. additionalProperty validates others",
            schema=schema_mixed,
            data={"quux": 3},
            valid=True,
        ),
        DataTest(
            name="mixed. additionalProperty invalidates others",
            schema=schema_mixed,
            data={"quux": "foo"},
            valid=False,
            expected_errors={"quux": [messages.INVALID_INT]},
        ),
    ]


class Ref(SchemaTest):
    schema_root = {"properties": {"foo": {"$ref": "#"}}, "additionalProperties": False}
    schema_relative = {
        "properties": {"foo": {"type": "integer"}, "bar": {"$ref": "#/properties/foo"}}
    }
    schema_relative_array = {"items": [{"type": "integer"}, {"$ref": "#/items/0"}]}
    schema_escaped = {
        "tilda~field": {"type": "integer"},
        "slash/field": {"type": "integer"},
        "percent%field": {"type": "integer"},
        "properties": {
            "tilda": {"$ref": "#/tilda~0field"},
            "slash": {"$ref": "#/slash~1field"},
            "percent": {"$ref": "#/percent%25field"},
        },
    }
    schema_nested = {
        "definitions": {
            "a": {"type": "integer"},
            "b": {"$ref": "#/definitions/a"},
            "c": {"$ref": "#/definitions/b"},
        },
        "$ref": "#/definitions/c",
    }
    data_tests = [
        DataTest(
            name="root. match", schema=schema_root, data={"foo": False}, valid=True
        ),
        DataTest(
            name="root. recursive match",
            schema=schema_root,
            data={"foo": {"foo": False}},
            valid=True,
        ),
        DataTest(
            name="root. mismatch",
            schema=schema_root,
            data={"bar": False},
            valid=False,
            expected_errors={
                "": [
                    messages.UNEXPECTED_FIELDS.format(
                        unexpected_fields="('bar' was unexpected)"
                    )
                ]
            },
        ),
        DataTest(
            name="root. recursive mismatch",
            schema=schema_root,
            data={"foo": {"bar": False}},
            valid=False,
            expected_errors={
                "foo": [
                    messages.UNEXPECTED_FIELDS.format(
                        unexpected_fields="('bar' was unexpected)"
                    )
                ]
            },
        ),
        DataTest(
            name="relative. match", schema=schema_relative, data={"bar": 3}, valid=True
        ),
        DataTest(
            name="relative. mismatch",
            schema=schema_relative,
            data={"bar": True},
            valid=False,
            expected_errors={"bar": [messages.INVALID_INT]},
        ),
        DataTest(
            name="relative array. match",
            schema=schema_relative_array,
            data=[1, 2],
            valid=True,
        ),
        DataTest(
            name="relative array. mismatch",
            schema=schema_relative_array,
            data=[1, "foo"],
            valid=False,
            expected_errors={"1": [messages.INVALID_INT]},
        ),
        DataTest(
            name="escaped. slash",
            schema=schema_escaped,
            data={"slash": "aoeu"},
            valid=False,
            expected_errors={"slash": [messages.INVALID_INT]},
        ),
        DataTest(
            name="escaped. tilda",
            schema=schema_escaped,
            data={"tilda": "aoeu"},
            valid=False,
            expected_errors={"tilda": [messages.INVALID_INT]},
        ),
        DataTest(
            name="escaped. percent",
            schema=schema_escaped,
            data={"percent": "aoeu"},
            valid=False,
            expected_errors={"percent": [messages.INVALID_INT]},
        ),
        DataTest(
            name="escaped. all",
            schema=schema_escaped,
            data={"slash": "aoeu", "tilda": "aoeu", "percent": "aoeu"},
            valid=False,
            expected_errors={
                "slash": [messages.INVALID_INT],
                "tilda": [messages.INVALID_INT],
                "percent": [messages.INVALID_INT],
            },
        ),
        DataTest(name="nested. valid", schema=schema_nested, data=5, valid=True),
        DataTest(
            name="nested. invalid",
            schema=schema_nested,
            data="a",
            valid=False,
            expected_errors={"": [messages.INVALID_INT]},
        ),
    ]


class Required(SchemaTest):
    schema = {"properties": {"foo": {}, "bar": {}}, "required": ["foo"]}
    schema_default = {"properties": {"foo": {}}}
    data_tests = [
        DataTest(name="valid", data={"foo": 1}, valid=True),
        DataTest(
            name="invalid",
            data={"bar": 1},
            valid=False,
            expected_errors={"foo": [messages.MISSING_REQUIRED_FIELD]},
        ),
        DataTest(
            name="not required by default", schema=schema_default, data={}, valid=True
        ),
    ]


class Type(SchemaTest):
    type_tests = [
        # -------------------------------------------------------
        # type          data        expected error
        # -------------------------------------------------------
        # array
        ("array", [], None),
        ("array", [1, 2], None),
        ("array", None, messages.INVALID_ARRAY),
        ("array", {}, messages.INVALID_ARRAY),
        ("array", "[]", messages.INVALID_ARRAY),
        ("array", "abc", messages.INVALID_ARRAY),
        ("array", 1, messages.INVALID_ARRAY),
        # boolean
        ("boolean", True, None),
        ("boolean", False, None),
        ("boolean", None, messages.INVALID_BOOLEAN),
        ("boolean", "True", messages.INVALID_BOOLEAN),
        ("boolean", "False", messages.INVALID_BOOLEAN),
        ("boolean", 0, messages.INVALID_BOOLEAN),
        ("boolean", 1, messages.INVALID_BOOLEAN),
        # integer
        ("integer", 0, None),
        ("integer", 1, None),
        ("integer", -500, None),
        ("integer", 1000000, None),
        ("integer", None, messages.INVALID_INT),
        ("integer", 1.2, messages.INVALID_INT),
        ("integer", "1", messages.INVALID_INT),
        # number
        ("number", 0, None),
        ("number", 1, None),
        ("number", -500, None),
        ("number", 1000000, None),
        ("number", 1.2, None),
        ("number", -34.7, None),
        ("number", None, messages.INVALID_NUMBER),
        ("number", "1", messages.INVALID_NUMBER),
        ("number", True, messages.INVALID_NUMBER),
        # null
        ("null", None, None),
        ("null", [], messages.NOT_NULL),
        ("null", {}, messages.NOT_NULL),
        ("null", 0, messages.NOT_NULL),
        ("null", "", messages.NOT_NULL),
        # object
        ("object", {}, None),
        ("object", {"a": 1}, None),
        ("object", None, messages.INVALID_OBJECT),
        ("object", [], messages.INVALID_OBJECT),
        ("object", 0, messages.INVALID_OBJECT),
        ("object", "", messages.INVALID_OBJECT),
        ("object", "abc", messages.INVALID_OBJECT),
        # string
        ("string", "", None),
        ("string", "abc", None),
        ("string", "-", None),
        ("string", "...", None),
        ("string", "", None),
        ("string", "abc", None),
        ("string", "España", None),
        ("string", "1", None),
        ("string", None, messages.INVALID_STRING),
        ("string", 1, messages.INVALID_STRING),
        ("string", [], messages.INVALID_STRING),
        ("string", {}, messages.INVALID_STRING),
    ]
    data_tests = [
        DataTest(
            name="%02d_%s" % (i + 1, data_type),
            data=data,
            valid=expected_error is None,
            expected_errors={"": [expected_error]} if expected_error else None,
            schema={"type": data_type},
        )
        for i, (data_type, data, expected_error) in enumerate(type_tests)
    ]


class Unique(SchemaTest):
    schema = {"uniqueItems": True}
    data_tests = [
        DataTest(name="unique array", data=[1, 2], valid=True),
        DataTest(
            name="non-unique array",
            data=[1, 1],
            valid=False,
            expected_errors={"": [messages.NOT_UNIQUE]},
        ),
        DataTest(
            name="non-unique numbers",
            data=[1.0, 1.00, 1],
            valid=False,
            expected_errors={"": [messages.NOT_UNIQUE]},
        ),
        DataTest(
            name="unique objects array",
            data=[{"foo": "bar"}, {"foo": "baz"}],
            valid=True,
        ),
        DataTest(
            name="non-unique objects array",
            data=[{"foo": "bar"}, {"foo": "bar"}],
            valid=False,
            expected_errors={"": [messages.NOT_UNIQUE]},
        ),
        DataTest(
            name="unique nested objects array",
            data=[{"foo": {"bar": {"baz": True}}}, {"foo": {"bar": {"baz": False}}}],
            valid=True,
        ),
        DataTest(
            name="non-unique nested objects array",
            data=[{"foo": {"bar": {"baz": True}}}, {"foo": {"bar": {"baz": True}}}],
            valid=False,
            expected_errors={"": [messages.NOT_UNIQUE]},
        ),
        DataTest(name="unique array of arrays", data=[["foo"], ["bar"]], valid=True),
        DataTest(
            name="non-unique array of arrays",
            data=[["foo"], ["foo"]],
            valid=False,
            expected_errors={"": [messages.NOT_UNIQUE]},
        ),
        DataTest(name="1 and True", data=[1, True], valid=True),
        DataTest(name="0 and False", data=[0, False], valid=True),
        DataTest(
            name="unique heterogeneous types", data=[{}, [1], True, None, 1], valid=True
        ),
        DataTest(
            name="non-unique heterogeneous types",
            data=[{}, [1], True, None, {}, 1],
            valid=False,
            expected_errors={"": [messages.NOT_UNIQUE]},
        ),
    ]
