import { compileSchema } from "../compileSchema";
import { strict as assert } from "assert";
import { Draft, JsonError, SchemaNode } from "../types";
import { draft2019 } from "../draft2019";

const $schema = "draft-2019-09";

describe("compileSchema.validate (2019)", () => {
	describe("integer", () => {
		describe("exclusiveMaximum", () => {
			it("should fail if value is equal to 0", () => {
				const { errors } = compileSchema({
					$schema,
					exclusiveMaximum: 0
				}).validate(0);
				assert.deepEqual(errors.length, 1);
			});

			it("should succeed if value is below to 0", () => {
				const { errors } = compileSchema({
					$schema,
					exclusiveMaximum: 0
				}).validate(-1);
				assert.deepEqual(errors.length, 0);
			});
		});

		describe("exclusiveMinimum", () => {
			it("should fail if value is equal to 0", () => {
				const { errors } = compileSchema({
					$schema,
					exclusiveMinimum: 0
				}).validate(0);
				assert.deepEqual(errors.length, 1);
			});

			it("should succeed if value is above to 0", () => {
				const { errors } = compileSchema({
					$schema,
					exclusiveMinimum: 0
				}).validate(1);
				assert.deepEqual(errors.length, 0);
			});
		});

		describe("oneOf", () => {
			it("should validate on a matching oneOf definition", () => {
				const { errors } = compileSchema({
					$schema,
					oneOf: [{ type: "integer" }, { type: "string" }]
				}).validate(3);
				assert.deepEqual(errors.length, 0);
			});

			it("should return an error for multiple matching oneOf schemas", () => {
				const { errors } = compileSchema({
					$schema,
					oneOf: [{ type: "integer" }, { minimum: 2 }]
				}).validate(3);
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "multiple-one-of-error");
			});
		});

		describe("allOf", () => {
			it("should validate if all allOf-schemas are valid", () => {
				const { errors } = compileSchema({
					$schema,
					allOf: [{ type: "integer" }, { minimum: 2 }]
				}).validate(3);
				assert.deepEqual(errors.length, 0);
			});

			it("should return error if not all schemas match", () => {
				const { errors } = compileSchema({
					$schema,
					allOf: [{ type: "integer" }, { minimum: 4 }]
				}).validate(3);
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "minimum-error");
			});

			it("should return all errors for each non-matching schemas", () => {
				const { errors } = compileSchema({
					$schema,
					allOf: [{ type: "integer" }, { minimum: 4 }, { maximum: 2 }]
				}).validate(3);
				assert.deepEqual(errors.length, 2);
				assert.deepEqual(errors[0].code, "minimum-error");
				assert.deepEqual(errors[1].code, "maximum-error");
			});
		});

		describe("anyOf", () => {
			it("should validate if one schemas in anyOf validates", () => {
				const { errors } = compileSchema({
					$schema,
					anyOf: [{ minimum: 4 }, { maximum: 4 }]
				}).validate(3);
				assert.deepEqual(errors.length, 0);
			});

			it("should return error if not all schemas match", () => {
				const { errors } = compileSchema({
					$schema,
					anyOf: [{ minimum: 4 }, { maximum: 2 }]
				}).validate(3);
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "any-of-error");
			});

			it("should validate null", () => {
				const { errors } = compileSchema({
					$schema,
					anyOf: [{ type: "null" }]
				}).validate(null);
				assert.deepEqual(errors.length, 0);
			});

			it("should return error if invalid null", () => {
				const { errors } = compileSchema({
					$schema,
					anyOf: [{ type: "null" }]
				}).validate(3);
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "any-of-error");
			});

			it("should resolve references", () => {
				const { errors } = compileSchema({
					$schema,
					definitions: { integer: { type: "integer" } },
					anyOf: [{ type: "null" }, { $ref: "#/definitions/integer" }]
				}).validate(3);
				assert.deepEqual(errors.length, 0);
			});
		});
	});

	describe("if-then-else", () => {
		it("should validate if-then constructs", () => {
			const node = compileSchema({
				$schema,
				if: { exclusiveMaximum: 0 }, // if this schema matches
				then: { minimum: -10 } // also test this schema
			});
			assert.deepEqual(node.validate(-1).errors.length, 0, "valid through then");
			assert.deepEqual(node.validate(-100).errors.length, 1, "invalid through then");
			assert.deepEqual(node.validate(3).errors.length, 0, "valid when if test fails");
		});

		it("should validate if-else constructs", () => {
			const node = compileSchema({
				$schema,
				if: { exclusiveMaximum: 0 }, // valid if 'if' valid
				else: { multipleOf: 2 } // test if 'if' fails
			});
			assert.deepEqual(node.validate(-1).errors.length, 0, "valid when if test passes");
			assert.deepEqual(node.validate(4).errors.length, 0, "valid through else");
			assert.deepEqual(node.validate(3).errors.length, 1, "invalid through else");
		});
	});

	describe("object", () => {
		it("should still be valid for missing type", () => {
			const { errors } = compileSchema({
				$schema,
				maxProperties: 1,
				minProperties: 1
			}).validate({ a: 1 });
			assert.deepEqual(errors.length, 0);
		});

		it("should return all errors", () => {
			const { errors } = compileSchema({
				$schema,
				type: "object",
				additionalProperties: false,
				properties: {
					a: { type: "string" },
					id: { type: "string", pattern: /^first$/ }
				}
			}).validate({ id: "first", a: "correct", b: "notallowed", c: false });

			assert.deepEqual(errors.length, 2);
			assert.deepEqual(errors[0].code, "no-additional-properties-error");
			assert.deepEqual(errors[1].code, "no-additional-properties-error");
		});

		describe("min/maxProperties", () => {
			it("should return min-properties-error for too few properties", () => {
				const { errors } = compileSchema({
					$schema,
					type: "object",
					minProperties: 2
				}).validate({ a: 1 });
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "min-properties-error");
			});

			it("should return max-properties-error for too many properties", () => {
				const { errors } = compileSchema({
					$schema,
					type: "object",
					maxProperties: 1
				}).validate({ a: 1, b: 2 });
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "max-properties-error");
			});

			it("should be valid if property count is within range", () => {
				const { errors } = compileSchema({
					$schema,
					type: "object",
					maxProperties: 1,
					minProperties: 1
				}).validate({ a: 1 });
				assert.deepEqual(errors.length, 0);
			});
		});

		describe("not", () => {
			it("should be invalid if 'not' keyword does match", () => {
				const { errors } = compileSchema({
					$schema,
					type: "object",
					not: { type: "object", properties: { a: { type: "number" } } }
				}).validate({ a: 1 });
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "not-error");
			});
		});

		describe("dependencies", () => {
			it("should ignore any dependencies if the property is no set", () => {
				const { errors } = compileSchema({
					$schema,
					type: "object",
					properties: { title: { type: "string" }, url: { type: "string" }, target: { type: "string" } },
					dependencies: { url: ["target"] }
				}).validate({ title: "Check this out" });

				assert.deepEqual(errors.length, 0);
			});

			it("should return a 'missing-dependency-error' if the dependent property is missing", () => {
				const { errors } = compileSchema({
					$schema,
					type: "object",
					properties: { title: { type: "string" }, url: { type: "string" }, target: { type: "string" } },
					dependencies: {
						url: ["target"]
					}
				}).validate({ title: "Check this out", url: "http://example.com" });

				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "missing-dependency-error");
			});

			it("should return a 'missing-dependency-error' if the dependent counterpart is missing", () => {
				const { errors } = compileSchema({
					$schema,
					type: "object",
					properties: { title: { type: "string" }, url: { type: "string" }, target: { type: "string" } },
					dependencies: { url: ["target"], target: ["url"] }
				}).validate({ title: "Check this out", target: "_blank" });

				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "missing-dependency-error");
			});

			it("should be valid for a matching schema dependency", () => {
				const { errors } = compileSchema({
					$schema,
					type: "object",
					properties: { title: { type: "string" }, url: { type: "string" }, target: { type: "string" } },
					dependencies: { url: { properties: { target: { type: "string" } } } }
				}).validate({ title: "Check this out", url: "http://example.com", target: "_blank" });

				assert.deepEqual(errors.length, 0);
			});

			it("should return validation error for a non-matching schema dependency", () => {
				const { errors } = compileSchema({
					$schema,
					type: "object",
					properties: { title: { type: "string" }, url: { type: "string" }, target: { type: "string" } },
					dependencies: { url: { required: ["target"], properties: { target: { type: "string" } } } }
				}).validate({ url: "http://example.com" });
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "required-property-error");
			});

			it("should return correct error for invalid dependency", () => {
				const { errors } = compileSchema({
					$schema,
					type: "object",
					properties: {
						nested: {
							type: "object",
							properties: { test: { type: "string" } },
							dependencies: {
								test: {
									required: ["dynamic"],
									properties: { dynamic: { type: "string", minLength: 1 } }
								}
							}
						}
					}
				}).validate({ nested: { test: "with then", dynamic: "" } });
				assert.deepEqual(errors.length, 1, "should have returned an error");
				assert.deepEqual(errors[0].data.pointer, "#/nested/dynamic");
			});
		});
	});

	describe("array", () => {
		it("should return error for invalid index", () => {
			const { errors } = compileSchema({
				$schema,
				type: "array",
				items: [{ type: "string" }]
			}).validate([1]);
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "type-error");
		});

		it("should be valid for matching indices", () => {
			const { errors } = compileSchema({
				$schema,
				type: "array",
				items: [{ type: "string" }, { type: "number" }]
			}).validate(["1", 2]);
			assert.deepEqual(errors.length, 0);
		});

		it("should return all errors", () => {
			const { errors } = compileSchema({
				$schema,
				type: "array",
				items: { type: "string" },
				maxItems: 1
			}).validate(["1", 2]);

			assert.deepEqual(errors.length, 2);
			assert.deepEqual(errors[0].code, "type-error");
			assert.deepEqual(errors[1].code, "max-items-error");
		});

		describe("min/maxItems", () => {
			it("should return MinItemsError for too few items", () => {
				const { errors } = compileSchema({
					$schema,
					type: "array",
					minItems: 2
				}).validate([1]);
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "min-items-error");
			});

			it("should return MaxItemsError for too many items", () => {
				const { errors } = compileSchema({
					$schema,
					type: "array",
					maxItems: 1
				}).validate([1, 2]);
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "max-items-error");
			});

			it("should be valid if item count is within range", () => {
				const { errors } = compileSchema({
					$schema,
					type: "array",
					minItems: 2,
					maxItems: 2
				}).validate([1, 2]);
				assert.deepEqual(errors.length, 0);
			});

			it("should still be valid for missing type", () => {
				const { errors } = compileSchema({
					$schema,
					minItems: 2,
					maxItems: 2
				}).validate([1, 2]);
				assert.deepEqual(errors.length, 0);
			});
		});

		describe("not", () => {
			it("should be invalid if 'not' keyword does match", () => {
				const { errors } = compileSchema({
					$schema,
					type: "array",
					items: [{ type: "string" }, { type: "number" }],
					additionalItems: { type: "object" },
					not: { items: {} }
				}).validate(["1", 2, {}]);
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "not-error");
			});
		});

		describe("uniqueItems", () => {
			it("should not validate for duplicated values", () => {
				const { errors } = compileSchema({
					$schema,
					type: "array",
					uniqueItems: true
				}).validate([1, 2, 3, 4, 3]);

				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "unique-items-error");
			});

			it("should not validate for duplicated objects", () => {
				const { errors } = compileSchema({
					$schema,
					type: "array",
					uniqueItems: true
				}).validate([{ id: "first" }, { id: "second" }, { id: "first" }]);

				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "unique-items-error");
			});

			it("should validate for mismatching objects with equal properties", () => {
				const { errors } = compileSchema({
					$schema,
					type: "array",
					uniqueItems: true
				}).validate([
					{ id: "first", val: 1 },
					{ id: "first", val: 2 },
					{ id: "first", val: 3 }
				]);

				assert.deepEqual(errors.length, 0);
			});
		});

		describe("oneOf", () => {
			it("should return no error for valid oneOf items", () => {
				const { errors } = compileSchema({
					$schema,
					type: "array",
					items: {
						oneOf: [
							{ type: "number" },
							{ type: "object", properties: { a: { type: "string" } }, additionalProperties: false }
						]
					}
				}).validate([100, { a: "string" }]);
				assert.deepEqual(errors.length, 0);
			});

			it("should return error if no item does match", () => {
				const { errors } = compileSchema({
					$schema,
					type: "array",
					items: {
						oneOf: [
							{ type: "number" },
							{ type: "object", properties: { a: { type: "string" } }, additionalProperties: false }
						]
					},
					additionalItems: false
				}).validate([100, { a: "correct", b: "not correct" }]);
				assert.deepEqual(errors.length, 1);
			});

			it("should return multiple-one-of-error if multiple oneOf definitions match the given value", () => {
				const { errors } = compileSchema({
					$schema,
					type: "array",
					items: { oneOf: [{ type: "integer" }, { minimum: 2 }] }
				}).validate([3]);
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "multiple-one-of-error");
			});
		});
	});

	describe("string", () => {
		it("should return min-length-error if string is too short", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				minLength: 2
			}).validate("a");
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "min-length-error");
		});

		it("should return max-length-error if string is too long", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				maxLength: 2
			}).validate("abc");
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "max-length-error");
		});

		it("should be valid if string is within range", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				minLength: 2,
				maxLength: 2
			}).validate("ab");
			assert.deepEqual(errors.length, 0);
		});

		it("should still be valid for missing type", () => {
			const { errors } = compileSchema({
				$schema,
				minLength: 2,
				maxLength: 2
			}).validate("ab");
			assert.deepEqual(errors.length, 0);
		});

		it("should return enum-error if value is not within enum list", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				enum: ["a", "c"]
			}).validate("b");
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "enum-error");
		});

		it("should be valid if value is within enum list", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				enum: ["a", "b", "c"]
			}).validate("b");
			assert.deepEqual(errors.length, 0);
		});

		it("should be invalid if 'not' keyword does match", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				not: { type: "string", pattern: "^b$" }
			}).validate("b");
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "not-error");
		});
	});

	describe("number", () => {
		it("should return minimum-error if number is too small", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				minimum: 2
			}).validate(1);
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "minimum-error");
		});

		it("should return minimum-error if number is equal and exclusiveMinimum is set", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				minimum: 2,
				exclusiveMinimum: true
			}).validate(2);
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "minimum-error");
		});

		it("should return maximum-error if number is too large", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				maximum: 1
			}).validate(2);
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "maximum-error");
		});

		it("should return maximum-error if number same and exclusiveMaximum is set", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				maximum: 2,
				exclusiveMaximum: true
			}).validate(2);
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "maximum-error");
		});

		it("should be valid if number is within range", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				minimum: 1,
				maximum: 1
			}).validate(1);
			assert.deepEqual(errors.length, 0);
		});

		it("should still be valid for missing type", () => {
			const { errors } = compileSchema({
				$schema,
				minimum: 1,
				maximum: 1
			}).validate(1);
			assert.deepEqual(errors.length, 0);
		});

		it("should validate NaN", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number"
			}).validate(parseInt("a"));
			assert.deepEqual(errors.length, 0);
		});

		it("should return enum-error if value is not within enum list", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				enum: [21, 27, 42]
			}).validate(13);
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "enum-error");
		});

		it("should be valid if value is within enum list", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				enum: [21, 27, 42]
			}).validate(27);
			assert.deepEqual(errors.length, 0);
		});

		it("should return error if value is not multiple of 1.5", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				multipleOf: 1.5
			}).validate(4);
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "multiple-of-error");
		});

		it("should be valid if value if a multiple of 1.5", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				multipleOf: 1.5
			}).validate(4.5);
			assert.deepEqual(errors.length, 0);
		});

		it("should be valid if 'multipleOf' is not a number", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				multipleOf: "non-number"
			}).validate(4.5);
			assert.deepEqual(errors.length, 0);
		});

		it("should be invalid if 'not' keyword does match", () => {
			const { errors } = compileSchema({
				$schema,
				type: "number",
				not: { type: "number", minimum: 4 }
			}).validate(4.5);
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "not-error");
		});
	});

	describe("arrays of types", () => {
		it("should not return an error for a valid type", () => {
			assert(compileSchema({ $schema, type: ["object", "null"] }).validate({}).valid);
			assert(compileSchema({ $schema, type: ["object", "null"] }).validate(null).valid);
		});

		it("should return a TypeError if passed type is not within array", () => {
			const { errors } = compileSchema({
				$schema,
				type: ["object", "null"]
			}).validate([]);
			assert.deepEqual(errors.length, 1);
			assert.deepEqual(errors[0].code, "type-error");
		});

		it("should support 'integer' as a valid type within array", () => {
			const { errors } = compileSchema({
				$schema,
				type: ["integer", "null"]
			}).validate(1);
			assert.deepEqual(errors.length, 0);
		});
	});

	describe("heterogeneous types", () => {
		describe("enum", () => {
			it("should validate a matching value within enum", () => {
				const { errors } = compileSchema({
					$schema,
					enum: [1, "second", []]
				}).validate("second");
				assert.deepEqual(errors.length, 0);
			});

			it("should validate a matching array within enum", () => {
				const { errors } = compileSchema({
					$schema,
					enum: [1, "second", []]
				}).validate([]);
				assert.deepEqual(errors.length, 0);
			});

			it("should validate a matching object within enum", () => {
				const { errors } = compileSchema({
					$schema,
					enum: [1, "second", { id: "third" }]
				}).validate({ id: "third" });
				assert.deepEqual(errors.length, 0);
			});

			it("should return error for non-matching object", () => {
				const { errors } = compileSchema({
					$schema,
					enum: [1, "second", { id: "third" }]
				}).validate({ id: "first" });
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "enum-error");
			});

			it("should return error for invalid null", () => {
				const { errors } = compileSchema({
					$schema,
					enum: [1, "second", { id: "third" }]
				}).validate(null);
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "enum-error");
			});
		});

		describe("$ref", () => {
			it("should correctly validate data through nested $ref", () => {
				const { errors } = compileSchema({
					$schema,
					$ref: "#/definitions/c",
					definitions: {
						a: { type: "integer" },
						b: { $ref: "#/definitions/a" },
						c: { $ref: "#/definitions/b" }
					}
				}).validate("a");
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "type-error");
			});

			it("should correctly validate combination of remote, allOf, and allOf-$ref", () => {
				const draft04Meta = require("../../remotes/draft04.json");
				const { errors } = compileSchema({
					$schema,
					$ref: "http://json-schema.org/draft-04/schema#",
					_id: "input"
				})
					.addRemoteSchema("http://json-schema.org/draft-04/schema", draft04Meta)
					.validate({ minLength: -1 });
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "minimum-error");
			});

			it("should correctly resolve local remote url", () => {
				const { errors } = compileSchema({
					$schema,
					$ref: "http://localhost:1234/integer.json",
					_id: "input"
				})
					.addRemoteSchema(
						"http://localhost:1234/integer.json",
						require("json-schema-test-suite/remotes/integer.json")
					)
					.validate("not an integer");
				assert.deepEqual(errors.length, 1);
				assert.deepEqual(errors[0].code, "type-error");
			});

			it("spec/unevaluatedProperties : dynamic evalation inside nested refs : should validate a", () => {
				const node = compileSchema({
					$schema: "https://json-schema.org/draft/2019-09/schema",
					$defs: {
						one: {
							oneOf: [
								{ $ref: "#/$defs/two" },
								{ required: ["b"], properties: { b: true } },
								{ required: ["xx"], patternProperties: { x: true } },
								{ required: ["all"], unevaluatedProperties: true }
							]
						},
						two: {
							oneOf: [
								{ required: ["c"], properties: { c: true } },
								{ required: ["d"], properties: { d: true } }
							]
						}
					},
					oneOf: [{ $ref: "#/$defs/one" }, { required: ["a"], properties: { a: true } }],
					unevaluatedProperties: false
				});
				const { errors } = node.validate({ a: 1 });
				assert(errors.length === 0);
			});
		});
	});

	describe("recursiveRef (spec)", () => {
		describe("$recursiveRef without using nesting", () => {
			it("integer does not match as a property value", () => {
				// how it should resolve
				// { foo } » root:anyOf: [false, ?]
				//      1. resolve http://localhost:4242/draft2019-09/recursiveRef2/schema.json#/$defs/myobject
				//      => domain + local path (fragments 2) => myobject-schema
				//      2. { foo } » anyOf: [false, true + ?]
				//          3. { foo } » myObject:anyof:additionalProperties => recursiveRef
				//          => recursiveAnchor = myObject
				//          4. 1 » anyOf: [false, false] => error
				const node = compileSchema({
					$schema: "https://json-schema.org/draft/2019-09/schema",
					$id: "http://localhost:4242/draft2019-09/recursiveRef2/schema.json",
					$defs: {
						myobject: {
							$id: "myobject.json",
							$recursiveAnchor: true,
							anyOf: [
								{ type: "string" },
								{
									type: "object",
									additionalProperties: { $recursiveRef: "#" }
								}
							]
						}
					},
					anyOf: [{ type: "integer" }, { $ref: "#/$defs/myobject" }]
				});

				const { errors } = node.validate({ foo: 1 });

				assert(errors.length > 0, "should have returned error for invalid integer");
			});
		});

		describe("$recursiveRef with $recursiveAnchor: false works like $ref", () => {
			let node: SchemaNode;
			beforeEach(() => {
				node = compileSchema({
					$schema: "https://json-schema.org/draft/2019-09/schema",
					$id: "http://localhost:4242/draft2019-09/recursiveRef4/schema.json",
					$recursiveAnchor: false,
					$defs: {
						myobject: {
							$id: "myobject.json",
							$recursiveAnchor: false,
							anyOf: [
								{ type: "string" },
								{
									type: "object",
									additionalProperties: { $recursiveRef: "#" }
								}
							]
						}
					},
					anyOf: [{ type: "integer" }, { $ref: "#/$defs/myobject" }]
				});
			});

			it("single level match", () => {
				// how it should resolve
				// { foo } » root:anyOf: [false, ?]
				//      1. resolve http://localhost:4242/draft2019-09/recursiveRef2/schema.json#/$defs/myobject
				//      => domain + local path (fragments 2) => myobject-schema
				//      2. { foo } » anyOf: [false, true + ?]
				//          3. { foo } » myObject:anyof:additionalProperties => recursiveRef
				//          => recursiveAnchor = myObject
				//          4. 1 » anyOf: [false, false] => error
				const { errors } = node.validate({ foo: "hi" });
				assert(errors.length === 0, "should have validated data");
			});

			it("integer does not match as a property value", () => {
				const { errors } = node.validate({ foo: 1 });
				assert(errors.length > 0, "should have returned error for integer");
			});
		});
	});
});

describe("compileSchema.validate : format", () => {
	describe("time", () => {
		it("should validate HH:mm:ss-HH:mm", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				format: "time"
			}).validate("15:31:12-02:30");
			assert.deepEqual(errors, []);
		});

		it("should validate HH:mm:ssZ", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				format: "time"
			}).validate("15:31:12Z");
			assert.deepEqual(errors, []);
		});

		it("should not validate minutes above 59", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				format: "time"
			}).validate("15:60:12");
			assert.equal(errors.length, 1);
		});

		it("should not validate seconds above 59", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				format: "time"
			}).validate("15:31:60");
			assert.equal(errors.length, 1);
		});

		it("should not validate HH:mm", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				format: "time"
			}).validate("15:31");
			assert.equal(errors.length, 1);
		});
	});

	describe("url", () => {
		it("should validate format url", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				format: "url"
			}).validate("https://developer.mozilla.org/en-US/");
			assert.deepEqual(errors, []);
		});

		it("should return error UrlFormatError for invalid urls", () => {
			const { errors } = compileSchema({
				$schema,
				type: "string",
				format: "url"
			}).validate("123");
			assert.equal(errors.length, 1);
			assert.equal(errors[0].code, "format-url-error");
		});
	});
});

describe("async validation", () => {
	let draft: Draft;
	beforeEach(() => {
		draft = {
			...draft2019,
			keywords: [
				...draft2019.keywords,
				{
					id: "async",
					keyword: "async-error",
					addValidate: (node) => node.schema.asyncError != null,
					validate: async ({ node }): Promise<JsonError> => {
						if (node.schema.asyncError === false) {
							return undefined;
						}
						return node.createError("type-error", {
							schema: {},
							pointer: "",
							value: ""
						});
					}
				}
			]
		};

		it("should resolve async validation returning no error", async () => {
			const { errors, errorsAsync } = compileSchema(
				{ type: "number", asyncError: false },
				{ drafts: [draft] }
			).validate(4);
			const asyncErrors = await Promise.all(errorsAsync);
			assert.deepEqual(errors.length, 0);
			assert.deepEqual(asyncErrors.length, 0);
		});

		it("should resolve async validation errors", async () => {
			const { errorsAsync } = compileSchema({ type: "number", asyncError: true }, { drafts: [draft] }).validate(
				4
			);
			const asyncErrors = await Promise.all(errorsAsync);
			assert.deepEqual(asyncErrors.length, 1);
			assert.deepEqual(asyncErrors[0].code, "type-error");
		});
	});
});
