# -*- coding: utf-8 -*-
from __future__ import absolute_import
from unittest import TestCase

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

from slugify import slugify
import six


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(SchemaTestCaseMetaclass, mcs).__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(six.with_metaclass(SchemaTestCaseMetaclass, TestCase)):
    schema = {}
    data_tests = []


class DataTest(object):
    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):
    schema = {
        "maximum": 3.0,
    }
    schema_exclusive = {
        "maximum": 3.0,
        "exclusiveMaximum": True,
    }
    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='exclusive. below',
            schema=schema_exclusive,
            data=2.2,
            valid=True,
        ),
        DataTest(
            name='exclusive. boundary point',
            schema=schema_exclusive,
            data=3.0,
            valid=False,
            expected_errors={
                '': [messages.NUMBER_TOO_HIGH],
            }
        ),
        DataTest(
            name='exclusive. above',
            schema=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):
    schema = {
        "minimum": 1.1,
    }
    schema_exclusive = {
        "minimum": 1.1,
        "exclusiveMinimum": True,
    }
    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=schema_exclusive,
            data=1.2,
            valid=True,
        ),
        DataTest(
            name='exclusive. boundary point',
            schema=schema_exclusive,
            data=1.1,
            valid=False,
            expected_errors={
                '': [messages.NUMBER_TOO_LOW],
            }
        ),
        DataTest(
            name='exclusive. below',
            schema=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_FIELD],
            }
        ),
        DataTest(
            name="root. recursive mismatch",
            schema=schema_root,
            data={"foo": {"bar": False}},
            valid=False,
            expected_errors={
                'foo': [messages.UNEXPECTED_FIELD],
            }
        ),
        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',      u"",        None),
        ('string',      u"abc",     None),
        ('string',      u"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],
            }
        ),
    ]
