// @tags: [
//   requires_non_retryable_commands,
// ]

/**
 * 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);
}());
