# Copyright (C) 2010, 2011 Linaro Limited
#
# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
#
# This file is part of json-schema-validator.
#
# json-schema-validator is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation
#
# json-schema-validator is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with json-schema-validator.  If not, see <http://www.gnu.org/licenses/>.

"""
Unit tests for JSON schema
"""

from testscenarios import TestWithScenarios
from testtools import TestCase

from json_schema_validator.errors import ValidationError
from json_schema_validator.shortcuts import validate
from json_schema_validator.validator import Validator


class ValidatorFailureTests(TestWithScenarios, TestCase):

    scenarios = [
        ("type_string_got_null", {
            'schema': '{"type": "string"}',
            'data': 'null',
            'raises': ValidationError(
                "None does not match type 'string'",
                "Object has incorrect type (expected string)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_string_got_integer", {
            'schema': '{"type": "string"}',
            'data': '5',
            'raises': ValidationError(
                "5 does not match type 'string'",
                "Object has incorrect type (expected string)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_number_got_null", {
            'schema': '{"type": "number"}',
            'data': 'null',
            'raises': ValidationError(
                "None does not match type 'number'",
                "Object has incorrect type (expected number)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_number_got_string", {
            'schema': '{"type": "number"}',
            'data': '"foobar"',
            'raises': ValidationError(
                "'foobar' does not match type 'number'",
                "Object has incorrect type (expected number)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_number_got_string_that_looks_like_number", {
            'schema': '{"type": "number"}',
            'data': '"3"',
            'raises': ValidationError(
                "'3' does not match type 'number'",
                "Object has incorrect type (expected number)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_integer_got_float", {
            'schema': '{"type": "integer"}',
            'data': '1.5',
            'raises': ValidationError(
                "1.5 does not match type 'integer'",
                "Object has incorrect type (expected integer)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_integer_got_null", {
            'schema': '{"type": "integer"}',
            'data': 'null',
            'raises': ValidationError(
                "None does not match type 'integer'",
                "Object has incorrect type (expected integer)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_boolean_got_null", {
            'schema': '{"type": "boolean"}',
            'data': 'null',
            'raises': ValidationError(
                "None does not match type 'boolean'",
                "Object has incorrect type (expected boolean)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_boolean_got_empty_string", {
            'schema': '{"type": "boolean"}',
            'data': '""',
            'raises': ValidationError(
                "u'' does not match type 'boolean'",
                "Object has incorrect type (expected boolean)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_boolean_got_empty_list", {
            'schema': '{"type": "boolean"}',
            'data': '[]',
            'raises': ValidationError(
                "[] does not match type 'boolean'",
                "Object has incorrect type (expected boolean)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_boolean_got_empty_object", {
            'schema': '{"type": "boolean"}',
            'data': '{}',
            'raises': ValidationError(
                "{} does not match type 'boolean'",
                "Object has incorrect type (expected boolean)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_object_got_integer", {
            'schema': '{"type": "object"}',
            'data': '1',
            'raises': ValidationError(
                "1 does not match type 'object'",
                "Object has incorrect type (expected object)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_object_got_null", {
            'schema': '{"type": "object"}',
            'data': 'null',
            'raises': ValidationError(
                "None does not match type 'object'",
                "Object has incorrect type (expected object)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_array_got_null", {
            'schema': '{"type": "array"}',
            'data': 'null',
            'raises': ValidationError(
                "None does not match type 'array'",
                "Object has incorrect type (expected array)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_array_got_integer", {
            'schema': '{"type": "array"}',
            'data': '1',
            'raises': ValidationError(
                "1 does not match type 'array'",
                "Object has incorrect type (expected array)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_null_got_empty_string", {
            'schema': '{"type": "null"}',
            'data': '""',
            'raises': ValidationError(
                "u'' does not match type 'null'",
                "Object has incorrect type (expected null)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_null_got_zero", {
            'schema': '{"type": "null"}',
            'data': '0',
            'raises': ValidationError(
                "0 does not match type 'null'",
                "Object has incorrect type (expected null)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_null_got_empty_list", {
            'schema': '{"type": "null"}',
            'data': '[]',
            'raises': ValidationError(
                "[] does not match type 'null'",
                "Object has incorrect type (expected null)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_null_got_empty_object", {
            'schema': '{"type": "null"}',
            'data': '{}',
            'raises': ValidationError(
                "{} does not match type 'null'",
                "Object has incorrect type (expected null)"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("type_list_got_mismatching_item", {
            'schema': '{"type": ["string", "number"]}',
            'data': '{}',
            'raises': ValidationError(
                "{} does not match any of the types in ['string', 'number']",
                "Object has incorrect type (multiple types possible)",
                ".type"),
            'object_expr': 'object',
            'schema_expr': 'schema.type',
        }),
        ("property_check_is_not_primitive", {
            'schema': """
            {
                "type": "object",
                "properties": {
                    "foo": {
                        "type": "number"
                    }
                }
            }""",
            'data': '{"foo": "foobar"}',
            'raises': ValidationError(
                "'foobar' does not match type 'number'",
                "Object has incorrect type (expected number)"),
            'object_expr': 'object.foo',
            'schema_expr': 'schema.properties.foo.type',
        }),
        ("property_check_validates_optional_properties", {
            'schema': """
            {
                "type": "object",
                "properties": {
                    "foo": {
                        "type": "number",
                        "optional": true
                    }
                }
            }""",
            'data': '{"foo": null}',
            'raises': ValidationError(
                "None does not match type 'number'",
                "Object has incorrect type (expected number)"),
            'object_expr': 'object.foo',
            'schema_expr': 'schema.properties.foo.type',
        }),
        ("property_check_reports_missing_non_optional_properties", {
            'schema': """
            {
                "type": "object",
                "properties": {
                    "foo": {
                        "type": "number",
                        "optional": false
                    }
                }
            }""",
            'data': '{}',
            'raises': ValidationError(
                "{} does not have property 'foo'",
                "Object lacks property 'foo'"),
            'object_expr': 'object',
            'schema_expr': 'schema.properties.foo.optional',
        }),
        ("property_check_reports_unknown_properties_when_additionalProperties_is_false", {
            'schema': """
            {
                "type": "object",
                "additionalProperties": false
            }""",
            'data': '{"foo": 5}',
            'raises': ValidationError(
                "{'foo': 5} has unknown property 'foo' and"
                " additionalProperties is false",
                "Object has unknown property 'foo' but additional "
                "properties are disallowed"),
            'object_expr': 'object',
            'schema_expr': 'schema.additionalProperties',
        }),
        ("property_check_validates_additional_properties_using_specified_type_when_additionalProperties_is_an_object_violation", {
            'schema': """
            {
                "type": "object",
                "additionalProperties": {
                    "type": "string"
                }
            }""",
            'data': '{"foo": "aaa", "bar": 5}',
            'raises': ValidationError(
                "5 does not match type 'string'",
                "Object has incorrect type (expected string)"),
            'object_expr': 'object.bar',
            'schema_expr': 'schema.additionalProperties.type',
        }),
        ("enum_check_reports_unlisted_values", {
            'schema': '{"enum": [1, 2, 3]}',
            'data': '5',
            'raises': ValidationError(
                '5 does not match any value in enumeration [1, 2, 3]',
                "Object does not match any value in enumeration"),
            'object_expr': 'object',
            'schema_expr': 'schema.enum',
        }),
        ("items_with_single_schema_finds_problems", {
            'schema': '{"items": {"type": "string"}}',
            'data': '["foo", null, "froz"]',
            'raises': ValidationError(
                "None does not match type 'string'",
                "Object has incorrect type (expected string)"),
            'object_expr': 'object[1]',
            'schema_expr': 'schema.items.type',
        }),
        ("items_with_array_schema_checks_for_too_short_data", {
            'schema': """
            {
                "items": [
                    {"type": "string"},
                    {"type": "boolean"}
                ]
            }""",
            'data': '["foo"]',
            'raises': ValidationError(
                "['foo'] is shorter than array schema [{'type':"
                " 'string'}, {'type': 'boolean'}]",
                "Object array is shorter than schema array"),
            'object_expr': 'object',
            'schema_expr': 'schema.items',
        }),
        ("items_with_array_schema_and_additionalProperties_of_false_checks_for_too_much_data", {
            'schema': """
            {
                "items": [
                    {"type": "string"},
                    {"type": "boolean"}
                ],
                "additionalProperties": false
            }""",
            'data': '["foo", false, 5]',
            'raises': ValidationError(
                "['foo', False, 5] is not of the same length as array"
                " schema [{'type': 'string'}, {'type': 'boolean'}] and"
                " additionalProperties is false",
                "Object array is not of the same length as schema array"),
            'object_expr': 'object',
            'schema_expr': 'schema.items',
        }),
        ("items_with_array_schema_and_additionalProperties_can_find_problems", {
            'schema': """
            {
                "items": [
                    {"type": "string"},
                    {"type": "boolean"}
                ],
                "additionalProperties": {
                    "type": "number"
                }
            }""",
            'data': '["foo", false, 5, 7.9, null]',
            'raises': ValidationError(
                "None does not match type 'number'",
                "Object has incorrect type (expected number)"),
            'object_expr': 'object[4]',
            'schema_expr': 'schema.additionalProperties.type',
        }),
        ("array_with_array_schema_and_uniqueItems_is_True", {
            'schema': """
            {
                "type": "array",
                "items": {"type": "string"},
                "uniqueItems": true
            }""",
            'data': '["foo", "bar", "foo"]',
            'raises': ValidationError(
                "Repeated items found in ['foo', 'bar', 'foo']",
                "Repeated items found in array"),
            'object_expr': 'object',
            'schema_expr': 'schema.items',
        }),
        ("requires_with_simple_property_name_can_report_problems", {
            'schema': """
            {
                "properties": {
                    "foo": {
                        "optional": true
                    },
                    "bar": {
                        "requires": "foo",
                        "optional": true
                    }
                }
            }
            """,
            'data': '{"bar": null}',
            'raises': ValidationError(
                "None requires presence of property 'foo' in the same"
                " object",
                "Enclosing object does not have property 'foo'"),
            'object_expr': 'object.bar',
            'schema_expr': 'schema.properties.bar.requires',
        }),
        ("requires_with_simple_property_name_can_report_problems_while_nested", {
            'schema': """
            {
                "type": "object",
                "properties": {
                    "nested": {
                        "properties": {
                            "foo": {
                                "optional": true
                            },
                            "bar": {
                                "requires": "foo",
                                "optional": true
                            }
                        }
                    }
                }
            }
            """,
            'data': '{"nested": {"bar": null}}',
            'raises': ValidationError(
                "None requires presence of property 'foo' in the same"
                " object",
                "Enclosing object does not have property 'foo'"),
            'object_expr': 'object.nested.bar',
            'schema_expr': 'schema.properties.nested.properties.bar.requires',
        }),
        ("requires_with_schema_can_report_problems", {
            'schema': """
            {
                "properties": {
                    "foo": {
                        "optional": true
                    },
                    "bar": {
                        "requires": {
                            "properties": {
                                "foo": {
                                    "type": "number"
                                }
                            }
                        },
                        "optional": true
                    }
                }
            }
            """,
            'data': '{"bar": null}',
            'raises': ValidationError(
                "{'bar': None} does not have property 'foo'",
                "Object lacks property 'foo'"),
            'object_expr': 'object',
            'schema_expr': 'schema.properties.bar.requires.properties.foo.optional',
        }),
        ("requires_with_schema_can_report_subtle_problems", {
            # In this test presence of "bar" requires that "foo" is
            # present and has a type "number"
            'schema': """
            {
                "properties": {
                    "foo": {
                        "optional": true
                    },
                    "bar": {
                        "requires": {
                            "properties": {
                                "foo": {
                                    "type": "number"
                                }
                            }
                        },
                        "optional": true
                    }
                }
            }
            """,
            'data': '{"bar": null, "foo": "not a number"}',
            'raises': ValidationError(
                "'not a number' does not match type 'number'",
                "Object has incorrect type (expected number)"),
            'object_expr': 'object.foo',
            'schema_expr': 'schema.properties.bar.requires.properties.foo.type'
        }),
        ("format_date_time_finds_problems", {
            'schema': '{"format": "date-time"}',
            'data': '"broken"',
            'raises': ValidationError(
                "'broken' is not a string representing JSON date-time",
                "Object is not a string representing JSON date-time"),
            'object_expr': 'object',
            'schema_expr': 'schema.format'
        }),
    ]

    def test_validation_error_has_proper_message(self):
        ex = self.assertRaises(ValidationError,
                               validate, self.schema, self.data)
        self.assertEqual(ex.message, self.raises.message)

    def test_validation_error_has_proper_new_message(self):
        ex = self.assertRaises(ValidationError,
                               validate, self.schema, self.data)
        self.assertEqual(ex.new_message, self.raises.new_message)

    def test_validation_error_has_proper_object_expr(self):
        ex = self.assertRaises(ValidationError,
                               validate, self.schema, self.data)
        self.assertEqual(ex.object_expr, self.object_expr)

    def test_validation_error_has_proper_schema_expr(self):
        ex = self.assertRaises(ValidationError,
                               validate, self.schema, self.data)
        self.assertEqual(ex.schema_expr, self.schema_expr)


class ValidatorSuccessTests(TestWithScenarios, TestCase):

    scenarios = [
        ("type_string_got_string", {
            'schema': '{"type": "string"}',
            'data': '"foobar"'
        }),
        ("type_number_got_integer", {
            'schema': '{"type": "number"}',
            'data': '1',
        }),
        ("type_number_number_float", {
            'schema': '{"type": "number"}',
            'data': '1.1',
        }),
        ("type_integer_got_integer_one", {
            'schema': '{"type": "integer"}',
            'data': '1'
        }),
        ("type_integer_got_integer", {
            'schema': '{"type": "integer"}',
            'data': '5'
        }),
        ("type_boolean_got_true", {
            'schema': '{"type": "boolean"}',
            'data': 'true',
        }),
        ("type_boolean_got_false", {
            'schema': '{"type": "boolean"}',
            'data': 'true',
        }),
        ("type_object_got_object", {
            'schema': '{"type": "object"}',
            'data': '{}'
        }),
        ("type_array_got_array", {
            'schema': '{"type": "array"}',
            'data': '[]'
        }),
        ("type_null_got_null", {
            'schema': '{"type": "null"}',
            'data': 'null',
        }),
        ("type_any_got_null", {
            'schema': '{"type": "any"}',
            'data': 'null',
        }),
        ("type_any_got_integer", {
            'schema': '{"type": "any"}',
            'data': '5',
        }),
        ("type_any_got_boolean", {
            'schema': '{"type": "any"}',
            'data': 'false',
        }),
        ("type_any_got_string", {
            'schema': '{"type": "any"}',
            'data': '"foobar"',
        }),
        ("type_any_got_array", {
            'schema': '{"type": "any"}',
            'data': '[]',
        }),
        ("type_any_got_object", {
            'schema': '{"type": "any"}',
            'data': '{}',
        }),
        ("type_nested_schema_check", {
            'schema': '{"type": {"type": "number"}}',
            'data': '5',
        }),
        ("type_list_with_two_values_got_first_value", {
            'schema': '{"type": ["number", "string"]}',
            'data': '1',
        }),
        ("type_list_with_two_values_got_second_value", {
            'schema': '{"type": ["number", "string"]}',
            'data': '"string"',
        }),
        ("property_ignored_on_non_objects", {
            'schema': '{"properties": {"foo": {"type": "number"}}}',
            'data': '"foobar"',
        }),
        ("property_checks_known_props", {
            'schema': """
            {
                "type": "object",
                "properties": {
                    "foo": {
                        "type": "number"
                    },
                    "bar": {
                        "type": "boolean"
                    }
                }
            }""",
            'data': """
            {
                "foo": 5,
                "bar": false
            }"""
        }),
        ("property_check_ignores_missing_optional_properties", {
            'schema': """
            {
                "type": "object",
                "properties": {
                    "foo": {
                        "type": "number",
                        "optional": true
                    }
                }
            }""",
            'data': '{}',
        }),
        ("property_check_ignores_normal_properties_when_additionalProperties_is_false", {
            'schema': """
            {
                "type": "object",
                "properties": {
                    "foo": {}
                },
                "additionalProperties": false
            }""",
            'data': '{"foo": 5}',
        }),
        ("property_check_validates_additional_properties_using_specified_type_when_additionalProperties_is_an_object", {
            'schema': """
            {
                "type": "object",
                "additionalProperties": {
                    "type": "string"
                }
            }""",
            'data': '{"foo": "aaa", "bar": "bbb"}',
        }),
        ("enum_check_does_nothing_by_default", {
            'schema': '{}',
            'data': '5',
        }),
        ("enum_check_verifies_possible_values", {
            'schema': '{"enum": [1, 2, 3]}',
            'data': '2',
        }),
        ("items_check_does_nothing_for_non_arrays", {
            'schema': '{"items": {"type": "string"}}',
            'data': '5',
        }),
        ("items_with_single_schema_applies_to_each_item", {
            'schema': '{"items": {"type": "string"}}',
            'data': '["foo", "bar", "froz"]',
        }),
        ("items_with_array_schema_applies_to_corresponding_items", {
            'schema': """
            {
                "items": [
                    {"type": "string"},
                    {"type": "boolean"}
                ]
            }""",
            'data': '["foo", true]',
        }),
        ("items_with_array_schema_and_additionalProperties", {
            'schema': """
            {
                "items": [
                    {"type": "string"},
                    {"type": "boolean"}
                ],
                "additionalProperties": {
                    "type": "number"
                }
            }""",
            'data': '["foo", false, 5, 7.9]',
        }),
        ("requires_with_simple_property_name_does_nothing_when_parent_property_is_not_used", {
            'schema': """
            {
                "properties": {
                    "foo": {
                        "optional": true
                    },
                    "bar": {
                        "requires": "foo",
                        "optional": true
                    }
                }
            }
            """,
            'data': '{}',
        }),
        ("requires_with_simple_property_name_works_when_condition_satisfied", {
            'schema': """
            {
                "properties": {
                    "foo": {
                        "optional": true
                    },
                    "bar": {
                        "requires": "foo",
                        "optional": true
                    }
                }
            }
            """,
            'data': '{"foo": null, "bar": null}',
        }),
        ("requires_with_schema_name_does_nothing_when_parent_property_is_not_used", {
            'schema': """
            {
                "properties": {
                    "foo": {
                        "optional": true
                    },
                    "bar": {
                        "requires": {
                            "properties": {
                                "foo": {
                                    "type": "number"
                                }
                            }
                        },
                        "optional": true
                    }
                }
            }
            """,
            'data': '{}',
        }),
        ("format_date_time_works", {
            'schema': '{"format": "date-time"}',
            'data': '"2010-11-12T14:38:55Z"',
        }),
        ("array_with_array_schema_and_uniqueItems_is_True", {
            'schema': """
            {
                "type": "array",
                "items": {"type": "string"},
                "uniqueItems": true
            }""",
            'data': '["foo", "bar", "baz"]',
        }),
    ]

    def test_validator_does_not_raise_an_exception(self):
        self.assertEqual(
            True, validate(self.schema, self.data))
