/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.johnzon.jsonschema;

import static java.util.Collections.emptyMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;

import jakarta.json.Json;
import jakarta.json.JsonBuilderFactory;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class JsonSchemaValidatorTest {
    private static JsonSchemaValidatorFactory factory;

    private final JsonBuilderFactory jsonFactory = Json.createBuilderFactory(emptyMap());

    @BeforeClass
    public static void init() {
        factory = new JsonSchemaValidatorFactory();
    }

    @AfterClass
    public static void destroy() {
        factory.close();
    }

    @Test
    public void patternPropertiesNested() {
        try (final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("nested", jsonFactory.createObjectBuilder()
                                .add("type", "object")
                                .add("patternProperties", jsonFactory.createObjectBuilder()
                                        .add("[0-9]+", jsonFactory.createObjectBuilder().add("type", "number"))
                                        .build())))
                .build())) {

            assertTrue(validator.apply(jsonFactory.createObjectBuilder().build()).isSuccess());
            assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                            .add("nested", jsonFactory.createObjectBuilder()
                                    .add("1", 1)
                                    .build())
                            .build())
                    .isSuccess());

            final ValidationResult result = validator.apply(jsonFactory.createObjectBuilder()
                    .add("nested", jsonFactory.createObjectBuilder()
                            .add("1", "test")
                            .build())
                    .build());
            assertFalse(result.toString(), result.isSuccess());
        }
    }

    @Test
    public void rootRequired() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder()
                                .add("type", "string")
                                .build())
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", "number")
                                .build())
                        .build())
                .add("required", jsonFactory.createArrayBuilder().add("name").build())
                .build());

        final ValidationResult success = validator.apply(jsonFactory.createObjectBuilder().add("name", "ok").build());
        assertTrue(success.getErrors().toString(), success.isSuccess());

        final ValidationResult failure = validator.apply(jsonFactory.createObjectBuilder().addNull("name").build());
        assertFalse(failure.isSuccess());
        final Collection<ValidationResult.ValidationError> errors = failure.getErrors();
        assertEquals(1, errors.size());
        final ValidationResult.ValidationError error = errors.iterator().next();
        assertEquals("/", error.getField());
        assertEquals("name is required and is not present", error.getMessage());

        validator.close();
    }

    @Test
    public void rootType() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder()
                                .add("type", "string")
                                .build())
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", "number")
                                .build())
                        .build())
                .build());

        {
            final ValidationResult success = validator.apply(jsonFactory.createObjectBuilder().add("name", "ok").build());
            assertTrue(success.getErrors().toString(), success.isSuccess());
        }
        {
            final ValidationResult success = validator.apply(jsonFactory.createObjectBuilder().addNull("name").build());
            assertTrue(success.getErrors().toString(), success.isSuccess());
        }

        final ValidationResult failure = validator.apply(jsonFactory.createObjectBuilder().add("name", 5).build());
        assertFalse(failure.isSuccess());
        final Collection<ValidationResult.ValidationError> errors = failure.getErrors();
        assertEquals(1, errors.size());
        final ValidationResult.ValidationError error = errors.iterator().next();
        assertEquals("/name", error.getField());
        assertEquals("Expected [NULL, STRING] but got NUMBER", error.getMessage());

        validator.close();
    }

    @Test
    public void typeArray() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
             .add("type", "object")
             .add("properties", jsonFactory.createObjectBuilder()
                                           .add("name", jsonFactory.createObjectBuilder()
                                                                   .add("type", jsonFactory.createArrayBuilder()
                                                                        .add("string")
                                                                        .add("number"))
                                                                   .build())
                                           .build())
             .build());

        {
            final ValidationResult success = validator.apply(jsonFactory.createObjectBuilder().add("name", "ok").build());
            assertTrue(success.getErrors().toString(), success.isSuccess());
        }
        {
            final ValidationResult success = validator.apply(jsonFactory.createObjectBuilder().addNull("name").build());
            assertTrue(success.getErrors().toString(), success.isSuccess());
        }
        {
            final ValidationResult success = validator.apply(jsonFactory.createObjectBuilder().add("name", 5).build());
            assertTrue(success.getErrors().toString(), success.isSuccess());
        }

        final ValidationResult failure = validator.apply(jsonFactory.createObjectBuilder().add("name", true).build());
        assertFalse(failure.isSuccess());
        final Collection<ValidationResult.ValidationError> errors = failure.getErrors();
        assertEquals(1, errors.size());
        final ValidationResult.ValidationError error = errors.iterator().next();
        assertEquals("/name", error.getField());
        assertEquals("Expected [NULL, NUMBER, STRING] but got TRUE", error.getMessage());

        validator.close();
    }

    @Test
    public void nestedType() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("person", jsonFactory.createObjectBuilder()
                                .add("type", "object")
                                .add("properties", jsonFactory.createObjectBuilder()
                                        .add("name", jsonFactory.createObjectBuilder()
                                                .add("type", "string")
                                                .build())
                                        .add("age", jsonFactory.createObjectBuilder()
                                                .add("type", "number")
                                                .build())
                                        .build())
                                .build())
                        .build())
                .build());

        final ValidationResult success = validator.apply(jsonFactory.createObjectBuilder()
                .add("person", jsonFactory.createObjectBuilder()
                        .add("name", "ok")
                        .build())
                .build());
        assertTrue(success.getErrors().toString(), success.isSuccess());

        final ValidationResult failure = validator.apply(jsonFactory.createObjectBuilder()
                .add("person", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder().build())
                        .build())
                .build());
        assertFalse(failure.toString(), failure.isSuccess());
        final Collection<ValidationResult.ValidationError> errors = failure.getErrors();
        assertEquals(1, errors.size());
        final ValidationResult.ValidationError error = errors.iterator().next();
        assertEquals("/person/name", error.getField());
        assertEquals("Expected [NULL, STRING] but got OBJECT", error.getMessage());

        validator.close();
    }

    @Test
    public void enumValues() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder()
                                .add("type", "string")
                                .add("enum", jsonFactory.createArrayBuilder().add("a").add("b").build())
                                .build())
                        .build())
                .build());

        final ValidationResult success = validator.apply(jsonFactory.createObjectBuilder().add("name", "a").build());
        assertTrue(success.getErrors().toString(), success.isSuccess());

        final ValidationResult failure = validator.apply(jsonFactory.createObjectBuilder().add("name", 5).build());
        assertFalse(failure.isSuccess());
        final Collection<ValidationResult.ValidationError> errors = failure.getErrors();
        assertEquals(2, errors.size());
        final ValidationResult.ValidationError error = errors.iterator().next();
        assertEquals("/name", error.getField());
        assertEquals("Expected [NULL, STRING] but got NUMBER", error.getMessage());

        validator.close();
    }

    @Test
    public void multipleOf() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", "number")
                                .add("multipleOf", 5)
                                .build())
                        .build())
                .build());

        final ValidationResult success = validator.apply(jsonFactory.createObjectBuilder().add("age", 5).build());
        assertTrue(success.getErrors().toString(), success.isSuccess());

        final ValidationResult failure = validator.apply(jsonFactory.createObjectBuilder().add("age", 6).build());
        assertFalse(failure.isSuccess());
        final Collection<ValidationResult.ValidationError> errors = failure.getErrors();
        assertEquals(1, errors.size());
        final ValidationResult.ValidationError error = errors.iterator().next();
        assertEquals("/age", error.getField());
        assertEquals("6.0 is not a multiple of 5.0", error.getMessage());

        validator.close();
    }

    @Test
    public void minimum() {
        {
            final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                    .add("type", "object")
                    .add("properties", jsonFactory.createObjectBuilder()
                            .add("age", jsonFactory.createObjectBuilder()
                                    .add("type", "number")
                                    .add("minimum", 5)
                                    .build())
                            .build())
                    .build());

            assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 5).build()).isSuccess());
            assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 6).build()).isSuccess());

            final ValidationResult failure = validator.apply(jsonFactory.createObjectBuilder().add("age", 2).build());
            assertFalse(failure.isSuccess());
            final Collection<ValidationResult.ValidationError> errors = failure.getErrors();
            assertEquals(1, errors.size());
            final ValidationResult.ValidationError error = errors.iterator().next();
            assertEquals("/age", error.getField());
            assertEquals("2.0 is less than 5.0", error.getMessage());

            validator.close();
        }
        {
            final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                    .add("type", "object")
                    .add("properties", jsonFactory.createObjectBuilder()
                            .add("age", jsonFactory.createObjectBuilder()
                                    .add("type", "number")
                                    .add("minimum", -1)
                                    .build())
                            .build())
                    .build());

            assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 1).build()).isSuccess());
            assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 0).build()).isSuccess());
            assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", -1).build()).isSuccess());

            final ValidationResult failure = validator.apply(jsonFactory.createObjectBuilder().add("age", -2).build());
            assertFalse(failure.isSuccess());
            final Collection<ValidationResult.ValidationError> errors = failure.getErrors();
            assertEquals(1, errors.size());
            final ValidationResult.ValidationError error = errors.iterator().next();
            assertEquals("/age", error.getField());
            assertEquals("-2.0 is less than -1.0", error.getMessage());

            validator.close();
        }
    }

    @Test
    public void maximum() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", "number")
                                .add("maximum", 5)
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 5).build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 4).build()).isSuccess());

        final ValidationResult failure = validator.apply(jsonFactory.createObjectBuilder().add("age", 6).build());
        assertFalse(failure.isSuccess());
        final Collection<ValidationResult.ValidationError> errors = failure.getErrors();
        assertEquals(1, errors.size());
        final ValidationResult.ValidationError error = errors.iterator().next();
        assertEquals("/age", error.getField());
        assertEquals("6.0 is more than 5.0", error.getMessage());

        validator.close();
    }

    @Test
    public void exclusiveMinimum() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", "number")
                                .add("exclusiveMinimum", 5)
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 6).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", 5).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", 4).build()).isSuccess());
        validator.close();
    }

    @Test
    public void exclusiveMaximum() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", "number")
                                .add("exclusiveMaximum", 5)
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 4).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", 5).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", 6).build()).isSuccess());

        validator.close();
    }
    
    @Test
    public void integerType() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", "integer")
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 30).build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", -10).build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", BigInteger.valueOf(50)).build()).isSuccess());
        // check no decimal numbers allowed
        assertFalse(validator.apply(jsonFactory.createObjectBuilder() .add("age", 30.3f) .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder() .add("age", -7.4d) .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder() .add("age", BigDecimal.valueOf(50.35613d)).build()).isSuccess());
        
        validator.close();
    }

    @Test
    public void minLength() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder()
                                .add("type", "string")
                                .add("minLength", 2)
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("name", "ok").build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("name", "okk").build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("name", "-").build()).isSuccess());

        validator.close();
    }

    @Test
    public void maxLength() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder()
                                .add("type", "string")
                                .add("maxLength", 2)
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("name", "ok").build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("name", "-").build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("name", "fail").build()).isSuccess());

        validator.close();
    }

    @Test
    public void pattern() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder()
                                .add("type", "string")
                                .add("pattern", "[a-z]")
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("name", "ok").build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("name", "-").build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("name", "0").build()).isSuccess());

        validator.close();
    }

    @Test
    public void itemsObject() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("names", jsonFactory.createObjectBuilder()
                                .add("type", "array")
                                .add("items", jsonFactory.createObjectBuilder()
                                        .add("type", "string"))
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add("1")).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add(1)).build()).isSuccess());

        validator.close();
    }

    @Test
    public void itemsArray() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("names", jsonFactory.createObjectBuilder()
                                .add("type", "array")
                                .add("items", jsonFactory.createArrayBuilder().add(jsonFactory.createObjectBuilder()
                                        .add("type", "string"))
                                        .build()).build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add("1")).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add(1)).build()).isSuccess());

        validator.close();
    }

    @Test
    public void itemsValidatesObject() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("names", jsonFactory.createObjectBuilder()
                                .add("type", "array")
                                .add("items", jsonFactory.createObjectBuilder()
                                        .add("type", "object")
                                        .add("properties", jsonFactory.createObjectBuilder()
                                                .add("age", jsonFactory.createObjectBuilder()
                                                        .add("type", "number")
                                                        .add("maximum", 2)
                                                        .build())
                                                .build()))
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder()
                        .add(jsonFactory.createObjectBuilder().add("age", 2)))
                .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder()
                        .add(jsonFactory.createArrayBuilder().build()))
                .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder()
                        .add(jsonFactory.createObjectBuilder().add("age", 3)))
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void maxItems() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("names", jsonFactory.createObjectBuilder()
                                .add("type", "array")
                                .add("maxItems", 1)
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add(2))
                .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add(2).add(3))
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void minItems() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("names", jsonFactory.createObjectBuilder()
                                .add("type", "array")
                                .add("minItems", 1)
                                .build())
                        .build())
                .build());

        final ValidationResult result = validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add(2))
                .build());
        assertTrue(result.toString(), result.isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder())
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void uniqueItems() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("names", jsonFactory.createObjectBuilder()
                                .add("type", "array")
                                .add("uniqueItems", true)
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add(2))
                .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add(2).add(2))
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void containsItems() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("names", jsonFactory.createObjectBuilder()
                                .add("type", "array")
                                .add("contains", jsonFactory.createObjectBuilder().add("type", "number"))
                                .build())
                        .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add(2))
                .build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add(2).add("test"))
                .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("names", jsonFactory.createArrayBuilder().add("test"))
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void maxProperties() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("maxProperties", 1)
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("name", "test")
                .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("name", "test")
                .add("name2", "test")
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void minProperties() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("minProperties", 1)
                .build());

        assertFalse(validator.apply(jsonFactory.createObjectBuilder().build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("name", "test")
                .build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("name", "test")
                .add("name2", "test")
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void patternProperties() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("patternProperties", jsonFactory.createObjectBuilder()
                        .add("[0-9]+", jsonFactory.createObjectBuilder().add("type", "number"))
                    .build())
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("1", 1)
                .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("1", "test")
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void additionalProperties() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("additionalProperties", jsonFactory.createObjectBuilder().add("type", "number"))
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("1", 1)
                .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("1", "test")
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void additionalPropertiesSubSchemaSkipsDeclaredProperties() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder().add("type", "string")))
                .add("patternProperties", jsonFactory.createObjectBuilder()
                        .add("^x-", jsonFactory.createObjectBuilder().add("type", "string")))
                .add("additionalProperties", jsonFactory.createObjectBuilder().add("type", "number"))
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("name", "test")
                .add("x-vendor", "test")
                .add("other", 1)
                .build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder()
                .add("name", "test")
                .add("other", "not a number")
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void additionalPropertiesFalse() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder().add("type", "string")))
                .add("additionalProperties", false)
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("name", "alice").build()).isSuccess());

        final ValidationResult result = validator.apply(jsonFactory.createObjectBuilder()
                .add("name", "alice")
                .add("role", "admin")
                .build());
        assertFalse(result.isSuccess());
        final Collection<ValidationResult.ValidationError> errors = result.getErrors();
        assertEquals(1, errors.size());
        final ValidationResult.ValidationError error = errors.iterator().next();
        assertEquals("/role", error.getField());
        assertEquals("Additional property 'role' is not allowed", error.getMessage());

        validator.close();
    }

    @Test
    public void additionalPropertiesTrue() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder().add("type", "string")))
                .add("additionalProperties", true)
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("name", "alice")
                .add("role", "admin")
                .build()).isSuccess());

        validator.close();
    }

    @Test
    public void additionalPropertiesNested() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("nested", jsonFactory.createObjectBuilder()
                                .add("type", "object")
                                .add("properties", jsonFactory.createObjectBuilder()
                                        .add("name", jsonFactory.createObjectBuilder().add("type", "string")))
                                .add("additionalProperties", false)))
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("nested", jsonFactory.createObjectBuilder().add("name", "alice"))
                .build()).isSuccess());
        // a sibling of "nested" is not a property of "nested" so it must not be reported
        assertTrue(validator.apply(jsonFactory.createObjectBuilder()
                .add("nested", jsonFactory.createObjectBuilder().add("name", "alice"))
                .add("sibling", "whatever")
                .build()).isSuccess());

        final ValidationResult result = validator.apply(jsonFactory.createObjectBuilder()
                .add("nested", jsonFactory.createObjectBuilder()
                        .add("name", "alice")
                        .add("role", "admin"))
                .build());
        assertFalse(result.isSuccess());
        assertEquals("/nested/role", result.getErrors().iterator().next().getField());

        validator.close();
    }

    @Test
    public void nullableTypeArrayKeepsStringConstraints() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("name", jsonFactory.createObjectBuilder()
                                .add("type", jsonFactory.createArrayBuilder().add("string").add("null"))
                                .add("pattern", "^[a-z]+$")
                                .add("minLength", 2)
                                .add("maxLength", 5)))
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder().addNull("name").build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("name", "abc").build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("name", "a').drop()").build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("name", "a").build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("name", "abcdef").build()).isSuccess());

        validator.close();
    }

    @Test
    public void nullableTypeArrayKeepsNumberConstraints() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", jsonFactory.createArrayBuilder().add("number").add("null"))
                                .add("minimum", 0)
                                .add("maximum", 100)
                                .add("multipleOf", 5)))
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().addNull("age").build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 50).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", -5).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", 105).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", 51).build()).isSuccess());

        validator.close();
    }

    @Test
    public void nullableTypeArrayKeepsExclusiveBounds() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", jsonFactory.createArrayBuilder().add("number").add("null"))
                                .add("exclusiveMinimum", 0)
                                .add("exclusiveMaximum", 10)))
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 5).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", 0).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", 10).build()).isSuccess());

        validator.close();
    }

    @Test
    public void integerTypeKeepsNumberConstraints() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", "integer")
                                .add("minimum", 0)
                                .add("maximum", 100)))
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 30).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", -1).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", 101).build()).isSuccess());

        validator.close();
    }

    @Test
    public void nullableIntegerTypeArray() {
        final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder()
                .add("type", "object")
                .add("properties", jsonFactory.createObjectBuilder()
                        .add("age", jsonFactory.createObjectBuilder()
                                .add("type", jsonFactory.createArrayBuilder().add("integer").add("null"))))
                .build());

        assertTrue(validator.apply(jsonFactory.createObjectBuilder().addNull("age").build()).isSuccess());
        assertTrue(validator.apply(jsonFactory.createObjectBuilder().add("age", 30).build()).isSuccess());
        assertFalse(validator.apply(jsonFactory.createObjectBuilder().add("age", 30.3).build()).isSuccess());

        validator.close();
    }
}
