// @tags: [requires_non_retryable_commands, rocks_requires_fcv36]

/**
 * Tests for the JSON Schema logical keywords, including:
 *
 *  - allOf
 *  - anyOf
 *  - oneOf
 *  - not
 *  - enum
 */
(function() {
    "use strict";

    load("jstests/libs/assert_schema_match.js");

    const coll = db.jstests_json_schema_logical;

    // Test that $jsonSchema fails to parse if the values for the allOf, anyOf, and oneOf
    // keywords are not arrays of valid schema.
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {allOf: {maximum: "0"}}}}}).itcount();
    });
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {allOf: [0]}}}}).itcount();
    });
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {allOf: [{invalid: "0"}]}}}}).itcount();
    });
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {anyOf: {maximum: "0"}}}}}).itcount();
    });
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {anyOf: [0]}}}}).itcount();
    });
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {anyOf: [{invalid: "0"}]}}}}).itcount();
    });
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {oneOf: {maximum: "0"}}}}}).itcount();
    });
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {oneOf: [0]}}}}).itcount();
    });
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {oneOf: [{invalid: "0"}]}}}}).itcount();
    });

    // Test that $jsonSchema fails to parse if the value for the 'not' keyword is not a
    // valid schema object.
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {not: {maximum: "0"}}}}}).itcount();
    });
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {not: [0]}}}}).itcount();
    });
    assert.throws(function() {
        coll.find({$jsonSchema: {properties: {foo: {not: [{}]}}}}).itcount();
    });

    // Test that the 'allOf' keyword correctly returns documents that match every schema in
    // the array.
    let schema = {properties: {foo: {allOf: [{minimum: 1}]}}};
    assertSchemaMatch(coll, schema, {foo: 1}, true);
    assertSchemaMatch(coll, schema, {foo: 0}, false);
    assertSchemaMatch(coll, schema, {foo: "string"}, true);

    schema = {properties: {foo: {allOf: [{}]}}};
    assertSchemaMatch(coll, schema, {foo: {}}, true);
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: 0}, true);

    schema = {properties: {foo: {allOf: [{type: 'number'}, {minimum: 0}]}}};
    assertSchemaMatch(coll, schema, {foo: 0}, true);
    assertSchemaMatch(coll, schema, {foo: "string"}, false);
    assertSchemaMatch(coll, schema, {foo: [0]}, false);

    // Test that a top-level 'allOf' keyword matches the correct documents.
    assertSchemaMatch(coll, {allOf: [{}]}, {}, true);
    assertSchemaMatch(coll, {allOf: [{}]}, {foo: 0}, true);
    assertSchemaMatch(coll, {allOf: [{type: 'string'}]}, {}, false);
    assertSchemaMatch(coll, {allOf: [{properties: {foo: {type: 'string'}}}]}, {foo: "str"}, true);
    assertSchemaMatch(coll, {allOf: [{properties: {foo: {type: 'string'}}}]}, {foo: 1}, false);

    // Test that 'allOf' in conjunction with another keyword matches the correct documents.
    assertSchemaMatch(
        coll, {properties: {foo: {type: "number", allOf: [{minimum: 1}]}}}, {foo: 1}, true);
    assertSchemaMatch(
        coll, {properties: {foo: {type: "number", allOf: [{minimum: 1}]}}}, {foo: "str"}, false);

    // Test that the 'anyOf' keyword correctly returns documents that match at least one schema
    // in the array.
    schema = {properties: {foo: {anyOf: [{type: 'string'}, {type: 'number', minimum: 1}]}}};
    assertSchemaMatch(coll, schema, {foo: "str"}, true);
    assertSchemaMatch(coll, schema, {foo: 1}, true);
    assertSchemaMatch(coll, schema, {foo: 0}, false);

    schema = {properties: {foo: {anyOf: [{type: 'string'}, {type: 'object'}]}}};
    assertSchemaMatch(coll, schema, {foo: {}}, true);
    assertSchemaMatch(coll, schema, {foo: "str"}, true);
    assertSchemaMatch(coll, schema, {foo: [{}]}, false);

    schema = {properties: {foo: {anyOf: [{}]}}};
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: {}}, true);
    assertSchemaMatch(coll, schema, {foo: 0}, true);

    // Test that a top-level 'anyOf' keyword matches the correct documents.
    schema = {anyOf: [{}]};
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: 1}, true);

    schema = {anyOf: [{properties: {foo: {type: 'string'}}}]};
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: "str"}, true);
    assertSchemaMatch(coll, schema, {foo: 1}, false);

    // Test that 'anyOf' in conjunction with another keyword matches the correct documents.
    schema = {properties: {foo: {type: "number", anyOf: [{minimum: 1}]}}};
    assertSchemaMatch(coll, schema, {foo: 1}, true);
    assertSchemaMatch(coll, schema, {foo: "str"}, false);

    // Test that the 'oneOf' keyword correctly returns documents that match exactly one schema
    // in the array.
    schema = {properties: {foo: {oneOf: [{minimum: 0}, {maximum: 3}]}}};
    assertSchemaMatch(coll, schema, {foo: 4}, true);
    assertSchemaMatch(coll, schema, {foo: 1}, false);
    assertSchemaMatch(coll, schema, {foo: "str"}, false);

    schema = {properties: {foo: {oneOf: [{type: 'string'}, {pattern: "ing"}]}}};
    assertSchemaMatch(coll, schema, {foo: "str"}, true);
    assertSchemaMatch(coll, schema, {foo: "string"}, false);

    schema = {properties: {foo: {oneOf: [{}]}}};
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: 1}, true);

    // Test that a top-level 'oneOf' keyword matches the correct documents.
    schema = {oneOf: [{}]};
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: 1}, true);

    schema = {oneOf: [{properties: {foo: {type: 'string'}}}]};
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: "str"}, true);
    assertSchemaMatch(coll, schema, {foo: 1}, false);

    assertSchemaMatch(coll, {oneOf: [{}, {}]}, {}, false);

    // Test that 'oneOf' in conjunction with another keyword matches the correct documents.
    schema = {properties: {foo: {type: "number", oneOf: [{minimum: 4}]}}};
    assertSchemaMatch(coll, schema, {foo: 4}, true);
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: "str"}, false);

    // Test that the 'not' keyword correctly returns documents that do not match any schema
    // in the array.
    schema = {properties: {foo: {not: {type: 'number'}}}};
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: "str"}, true);
    assertSchemaMatch(coll, schema, {foo: 1}, false);

    schema = {properties: {foo: {not: {}}}};
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: 1}, false);

    // Test that a top-level 'not' keyword matches the correct documents.
    assertSchemaMatch(coll, {not: {}}, {}, false);

    schema = {not: {properties: {foo: {type: 'string'}}}};
    assertSchemaMatch(coll, schema, {foo: 1}, true);
    assertSchemaMatch(coll, schema, {foo: "str"}, false);
    assertSchemaMatch(coll, schema, {}, false);

    // Test that 'not' in conjunction with another keyword matches the correct documents.
    schema = {properties: {foo: {type: "string", not: {maxLength: 4}}}};
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {foo: "string"}, true);
    assertSchemaMatch(coll, schema, {foo: "str"}, false);
    assertSchemaMatch(coll, schema, {foo: 1}, false);

    // Test that the 'enum' keyword correctly matches scalar values.
    schema = {properties: {a: {enum: ["str", 5]}}};
    assertSchemaMatch(coll, schema, {a: "str"}, true);
    assertSchemaMatch(coll, schema, {a: 5}, true);
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {a: ["str"]}, false);

    // Test that the 'enum' keyword with a null value correctly matches literal null elements, but
    // not 'missing' or 'undefined.
    schema = {properties: {a: {enum: [null]}}};
    assertSchemaMatch(coll, schema, {a: null}, true);
    assertSchemaMatch(coll, schema, {a: undefined}, false);
    assertSchemaMatch(coll, schema, {a: 1}, false);
    assertSchemaMatch(coll, {properties: {a: {enum: [null]}}, required: ['a']}, {}, false);

    // Test that the 'enum' keyword correctly matches array values.
    schema = {properties: {a: {enum: [[1, 2, "3"]]}}};
    assertSchemaMatch(coll, schema, {a: [1, 2, "3"]}, true);
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {a: [2, "3", 1]}, false);

    schema = {properties: {a: {enum: [[]]}}};
    assertSchemaMatch(coll, schema, {a: []}, true);
    assertSchemaMatch(coll, schema, {}, true);
    assertSchemaMatch(coll, schema, {a: [1]}, false);

    // Test that the 'enum' keyword does not traverse arrays when matching.
    schema = {properties: {a: {enum: ["str", 1]}}};
    assertSchemaMatch(coll, schema, {a: ["str"]}, false);
    assertSchemaMatch(coll, schema, {a: [1]}, false);

    // Test that the 'enum' keyword matches objects regardless of the field ordering.
    schema = {properties: {a: {enum: [{name: "tiny", size: "large"}]}}};
    assertSchemaMatch(coll, schema, {a: {name: "tiny", size: "large"}}, true);
    assertSchemaMatch(coll, schema, {a: {size: "large", name: "tiny"}}, true);

    // Test that the 'enum' keyword does not match documents with additional fields.
    assertSchemaMatch(coll,
                      {properties: {a: {enum: [{name: "tiny"}]}}},
                      {a: {size: "large", name: "tiny"}},
                      false);

    // Test that a top-level 'enum' matches the correct documents.
    assertSchemaMatch(coll, {enum: [{_id: 0}]}, {_id: 0}, true);
    assertSchemaMatch(coll, {enum: [{_id: 0, a: "str"}]}, {_id: 0, a: "str"}, true);
    assertSchemaMatch(coll, {enum: [{}]}, {}, false);
    assertSchemaMatch(coll, {enum: [null]}, {}, false);
    assertSchemaMatch(coll, {enum: [{_id: 0, a: "str"}]}, {_id: 0, a: "str", b: 1}, false);
    assertSchemaMatch(coll, {enum: [1, 2]}, {}, false);
}());
