/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const { JsonSchemaValidator } = ChromeUtils.importESModule(
  "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs"
);

add_task(async function test_boolean_values() {
  let schema = {
    type: "boolean",
  };

  // valid values
  validate({
    value: true,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: true,
    },
  });
  validate({
    value: false,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: false,
    },
  });
  validate({
    value: 0,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: false,
    },
  });
  validate({
    value: 1,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: true,
    },
  });

  // Invalid values:
  validate({
    value: "0",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "0",
      error: {
        invalidValue: "0",
        invalidPropertyNameComponents: [],
        message: `The value '"0"' does not match the expected type 'boolean'`,
      },
    },
  });
  validate({
    value: "true",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "true",
      error: {
        invalidValue: "true",
        invalidPropertyNameComponents: [],
        message: `The value '"true"' does not match the expected type 'boolean'`,
      },
    },
  });
  validate({
    value: 2,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: 2,
      error: {
        invalidValue: 2,
        invalidPropertyNameComponents: [],
        message: `The value '2' does not match the expected type 'boolean'`,
      },
    },
  });
  validate({
    value: undefined,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: undefined,
        invalidPropertyNameComponents: [],
        message: `The value 'undefined' does not match the expected type 'boolean'`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: {},
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match the expected type 'boolean'`,
      },
    },
  });
  validate({
    value: null,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: null,
      error: {
        invalidValue: null,
        invalidPropertyNameComponents: [],
        message: `The value 'null' does not match the expected type 'boolean'`,
      },
    },
  });
});

add_task(async function test_number_values() {
  let schema = {
    type: "number",
  };

  validate({
    value: 1,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: 1,
    },
  });

  // Invalid values:
  validate({
    value: "1",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "1",
      error: {
        invalidValue: "1",
        invalidPropertyNameComponents: [],
        message: `The value '"1"' does not match the expected type 'number'`,
      },
    },
  });
  validate({
    value: true,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: true,
      error: {
        invalidValue: true,
        invalidPropertyNameComponents: [],
        message: `The value 'true' does not match the expected type 'number'`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: {},
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match the expected type 'number'`,
      },
    },
  });
  validate({
    value: null,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: null,
      error: {
        invalidValue: null,
        invalidPropertyNameComponents: [],
        message: `The value 'null' does not match the expected type 'number'`,
      },
    },
  });
});

add_task(async function test_integer_values() {
  // Integer is an alias for number
  let schema = {
    type: "integer",
  };

  validate({
    value: 1,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: 1,
    },
  });

  // Invalid values:
  validate({
    value: "1",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "1",
      error: {
        invalidValue: "1",
        invalidPropertyNameComponents: [],
        message: `The value '"1"' does not match the expected type 'integer'`,
      },
    },
  });
  validate({
    value: true,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: true,
      error: {
        invalidValue: true,
        invalidPropertyNameComponents: [],
        message: `The value 'true' does not match the expected type 'integer'`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: {},
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match the expected type 'integer'`,
      },
    },
  });
  validate({
    value: null,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: null,
      error: {
        invalidValue: null,
        invalidPropertyNameComponents: [],
        message: `The value 'null' does not match the expected type 'integer'`,
      },
    },
  });
});

add_task(async function test_null_values() {
  let schema = {
    type: "null",
  };

  validate({
    value: null,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: null,
    },
  });

  // Invalid values:
  validate({
    value: 1,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: 1,
      error: {
        invalidValue: 1,
        invalidPropertyNameComponents: [],
        message: `The value '1' does not match the expected type 'null'`,
      },
    },
  });
  validate({
    value: "1",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "1",
      error: {
        invalidValue: "1",
        invalidPropertyNameComponents: [],
        message: `The value '"1"' does not match the expected type 'null'`,
      },
    },
  });
  validate({
    value: true,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: true,
      error: {
        invalidValue: true,
        invalidPropertyNameComponents: [],
        message: `The value 'true' does not match the expected type 'null'`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: {},
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match the expected type 'null'`,
      },
    },
  });
  validate({
    value: [],
    schema,
    expectedResult: {
      valid: false,
      parsedValue: [],
      error: {
        invalidValue: [],
        invalidPropertyNameComponents: [],
        message: `The value '[]' does not match the expected type 'null'`,
      },
    },
  });
});

add_task(async function test_string_values() {
  let schema = {
    type: "string",
  };

  validate({
    value: "foobar",
    schema,
    expectedResult: {
      valid: true,
      parsedValue: "foobar",
    },
  });

  // Invalid values:
  validate({
    value: 1,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: 1,
      error: {
        invalidValue: 1,
        invalidPropertyNameComponents: [],
        message: `The value '1' does not match the expected type 'string'`,
      },
    },
  });
  validate({
    value: true,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: true,
      error: {
        invalidValue: true,
        invalidPropertyNameComponents: [],
        message: `The value 'true' does not match the expected type 'string'`,
      },
    },
  });
  validate({
    value: undefined,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: undefined,
        invalidPropertyNameComponents: [],
        message: `The value 'undefined' does not match the expected type 'string'`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: {},
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match the expected type 'string'`,
      },
    },
  });
  validate({
    value: null,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: null,
      error: {
        invalidValue: null,
        invalidPropertyNameComponents: [],
        message: `The value 'null' does not match the expected type 'string'`,
      },
    },
  });
});

add_task(async function test_URL_values() {
  let schema = {
    type: "URL",
  };

  let result = validate({
    value: "https://www.example.com/foo#bar",
    schema,
    expectedResult: {
      valid: true,
      parsedValue: new URL("https://www.example.com/foo#bar"),
    },
  });
  Assert.ok(URL.isInstance(result.parsedValue), "parsedValue is a URL");
  Assert.equal(
    result.parsedValue.origin,
    "https://www.example.com",
    "origin is correct"
  );
  Assert.equal(
    result.parsedValue.pathname + result.parsedValue.hash,
    "/foo#bar",
    "pathname is correct"
  );

  // Invalid values:
  validate({
    value: "",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "",
      error: {
        invalidValue: "",
        invalidPropertyNameComponents: [],
        message: `The value '""' does not match the expected type 'URL'`,
      },
    },
  });
  validate({
    value: "www.example.com",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "www.example.com",
      error: {
        invalidValue: "www.example.com",
        invalidPropertyNameComponents: [],
        message:
          `The value '"www.example.com"' does not match the expected ` +
          `type 'URL'`,
      },
    },
  });
  validate({
    value: "https://:!$%",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "https://:!$%",
      error: {
        invalidValue: "https://:!$%",
        invalidPropertyNameComponents: [],
        message: `The value '"https://:!$%"' does not match the expected type 'URL'`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: {},
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match the expected type 'URL'`,
      },
    },
  });
});

add_task(async function test_URLorEmpty_values() {
  let schema = {
    type: "URLorEmpty",
  };

  let result = validate({
    value: "https://www.example.com/foo#bar",
    schema,
    expectedResult: {
      valid: true,
      parsedValue: new URL("https://www.example.com/foo#bar"),
    },
  });
  Assert.ok(URL.isInstance(result.parsedValue), "parsedValue is a URL");
  Assert.equal(
    result.parsedValue.origin,
    "https://www.example.com",
    "origin is correct"
  );
  Assert.equal(
    result.parsedValue.pathname + result.parsedValue.hash,
    "/foo#bar",
    "pathname is correct"
  );

  // Test that this type also accept empty strings
  result = validate({
    value: "",
    schema,
    expectedResult: {
      valid: true,
      parsedValue: "",
    },
  });
  Assert.equal(typeof result.parsedValue, "string", "parsedValue is a string");

  // Invalid values:
  validate({
    value: " ",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: " ",
      error: {
        invalidValue: " ",
        invalidPropertyNameComponents: [],
        message: `The value '" "' does not match the expected type 'URLorEmpty'`,
      },
    },
  });
  validate({
    value: "www.example.com",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "www.example.com",
      error: {
        invalidValue: "www.example.com",
        invalidPropertyNameComponents: [],
        message:
          `The value '"www.example.com"' does not match the expected ` +
          `type 'URLorEmpty'`,
      },
    },
  });
  validate({
    value: "https://:!$%",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "https://:!$%",
      error: {
        invalidValue: "https://:!$%",
        invalidPropertyNameComponents: [],
        message:
          `The value '"https://:!$%"' does not match the expected ` +
          `type 'URLorEmpty'`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: {},
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match the expected type 'URLorEmpty'`,
      },
    },
  });
});

add_task(async function test_origin_values() {
  // Origin is a URL that doesn't contain a path/query string (i.e., it's only scheme + host + port)
  let schema = {
    type: "origin",
  };

  let result = validate({
    value: "https://www.example.com",
    schema,
    expectedResult: {
      valid: true,
      parsedValue: new URL("https://www.example.com/"),
    },
  });
  Assert.ok(URL.isInstance(result.parsedValue), "parsedValue is a URL");
  Assert.equal(
    result.parsedValue.origin,
    "https://www.example.com",
    "origin is correct"
  );
  Assert.equal(
    result.parsedValue.pathname + result.parsedValue.hash,
    "/",
    "pathname is correct"
  );

  // Invalid values:
  validate({
    value: "https://www.example.com/foobar",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: new URL("https://www.example.com/foobar"),
      error: {
        invalidValue: "https://www.example.com/foobar",
        invalidPropertyNameComponents: [],
        message:
          `The value '"https://www.example.com/foobar"' does not match the ` +
          `expected type 'origin'`,
      },
    },
  });
  validate({
    value: "https://:!$%",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "https://:!$%",
      error: {
        invalidValue: "https://:!$%",
        invalidPropertyNameComponents: [],
        message:
          `The value '"https://:!$%"' does not match the expected ` +
          `type 'origin'`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: {},
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match the expected type 'origin'`,
      },
    },
  });
});

add_task(async function test_origin_file_values() {
  // File URLs can also be origins
  let schema = {
    type: "origin",
  };

  let result = validate({
    value: "file:///foo/bar",
    schema,
    expectedResult: {
      valid: true,
      parsedValue: new URL("file:///foo/bar"),
    },
  });
  Assert.ok(URL.isInstance(result.parsedValue), "parsedValue is a URL");
  Assert.equal(
    result.parsedValue.href,
    "file:///foo/bar",
    "Should get what we passed in"
  );
});

add_task(async function test_origin_file_values() {
  // File URLs can also be origins
  let schema = {
    type: "origin",
  };

  let result = validate({
    value: "file:///foo/bar/foobar.html",
    schema,
    expectedResult: {
      valid: true,
      parsedValue: new URL("file:///foo/bar/foobar.html"),
    },
  });
  Assert.ok(URL.isInstance(result.parsedValue), "parsedValue is a URL");
  Assert.equal(
    result.parsedValue.href,
    "file:///foo/bar/foobar.html",
    "Should get what we passed in"
  );
});

add_task(async function test_array_values() {
  // The types inside an array object must all be the same
  let schema = {
    type: "array",
    items: {
      type: "number",
    },
  };

  validate({
    value: [1, 2, 3],
    schema,
    expectedResult: {
      valid: true,
      parsedValue: [1, 2, 3],
    },
  });

  // An empty array is also valid
  validate({
    value: [],
    schema,
    expectedResult: {
      valid: true,
      parsedValue: [],
    },
  });

  // Invalid values:
  validate({
    value: [1, true, 3],
    schema,
    expectedResult: {
      valid: false,
      parsedValue: true,
      error: {
        invalidValue: true,
        invalidPropertyNameComponents: [1],
        message:
          `The value 'true' does not match the expected type 'number'. The ` +
          `invalid value is property '1' in [1,true,3]`,
      },
    },
  });
  validate({
    value: 2,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: 2,
        invalidPropertyNameComponents: [],
        message: `The value '2' does not match the expected type 'array'`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match the expected type 'array'`,
      },
    },
  });
});

add_task(async function test_non_strict_arrays() {
  // Non-strict arrays ignores invalid values (don't include
  // them in the parsed output), instead of failing the validation.
  // Note: invalid values might still report errors to the console.
  let schema = {
    type: "array",
    strict: false,
    items: {
      type: "string",
    },
  };

  validate({
    value: ["valid1", "valid2", false, 3, "valid3"],
    schema,
    expectedResult: {
      valid: true,
      parsedValue: ["valid1", "valid2", "valid3"],
    },
  });

  // Checks that strict defaults to true;
  delete schema.strict;
  validate({
    value: ["valid1", "valid2", false, 3, "valid3"],
    schema,
    expectedResult: {
      valid: false,
      parsedValue: false,
      error: {
        invalidValue: false,
        invalidPropertyNameComponents: [2],
        message:
          `The value 'false' does not match the expected type 'string'. The ` +
          `invalid value is property '2' in ` +
          `["valid1","valid2",false,3,"valid3"]`,
      },
    },
  });

  // Pass allowArrayNonMatchingItems, should be valid
  validate({
    value: ["valid1", "valid2", false, 3, "valid3"],
    schema,
    options: {
      allowArrayNonMatchingItems: true,
    },
    expectedResult: {
      valid: true,
      parsedValue: ["valid1", "valid2", "valid3"],
    },
  });
});

add_task(async function test_object_values() {
  // valid values below

  validate({
    value: {
      foo: "hello",
      bar: 123,
    },
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "string",
        },
        bar: {
          type: "number",
        },
      },
    },
    expectedResult: {
      valid: true,
      parsedValue: {
        foo: "hello",
        bar: 123,
      },
    },
  });

  validate({
    value: {
      foo: "hello",
      bar: {
        baz: 123,
      },
    },
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "string",
        },
        bar: {
          type: "object",
          properties: {
            baz: {
              type: "number",
            },
          },
        },
      },
    },
    expectedResult: {
      valid: true,
      parsedValue: {
        foo: "hello",
        bar: {
          baz: 123,
        },
      },
    },
  });

  validate({
    value: {
      foo: "hello",
      bar: 123,
      baz: "an additional property",
    },
    schema: {
      type: "object",
      additionalProperties: true,
      properties: {
        foo: {
          type: "string",
        },
        bar: {
          type: "number",
        },
      },
    },
    expectedResult: {
      valid: true,
      parsedValue: {
        foo: "hello",
        bar: 123,
      },
    },
  });

  // allowAdditionalProperties
  let result = validate({
    value: {
      url: "https://www.example.com/foo#bar",
      title: "Foo",
      alias: "Bar",
    },
    schema: {
      type: "object",
      properties: {
        url: {
          type: "URL",
        },
        title: {
          type: "string",
        },
      },
    },
    options: {
      allowAdditionalProperties: true,
    },
    expectedResult: {
      valid: true,
      parsedValue: {
        url: new URL("https://www.example.com/foo#bar"),
        title: "Foo",
      },
    },
  });
  Assert.ok(
    URL.isInstance(result.parsedValue.url),
    "types inside the object are also parsed"
  );
  Assert.equal(
    result.parsedValue.url.href,
    "https://www.example.com/foo#bar",
    "URL was correctly parsed"
  );

  // allowExplicitUndefinedProperties
  validate({
    value: {
      foo: undefined,
    },
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "string",
        },
      },
    },
    options: {
      allowExplicitUndefinedProperties: true,
    },
    expectedResult: {
      valid: true,
      parsedValue: {},
    },
  });

  // allowNullAsUndefinedProperties
  validate({
    value: {
      foo: null,
    },
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "string",
        },
      },
    },
    options: {
      allowNullAsUndefinedProperties: true,
    },
    expectedResult: {
      valid: true,
      parsedValue: {},
    },
  });

  // invalid values below

  validate({
    value: null,
    schema: {
      type: "object",
    },
    expectedResult: {
      valid: false,
      parsedValue: null,
      error: {
        invalidValue: null,
        invalidPropertyNameComponents: [],
        message: `The value 'null' does not match the expected type 'object'`,
      },
    },
  });

  validate({
    value: {
      url: "not a URL",
    },
    schema: {
      type: "object",
      properties: {
        url: {
          type: "URL",
        },
      },
    },
    expectedResult: {
      valid: false,
      parsedValue: "not a URL",
      error: {
        invalidValue: "not a URL",
        invalidPropertyNameComponents: ["url"],
        message:
          `The value '"not a URL"' does not match the expected type 'URL'. ` +
          `The invalid value is property 'url' in {"url":"not a URL"}`,
      },
    },
  });

  validate({
    value: "test",
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "string",
        },
      },
    },
    expectedResult: {
      valid: false,
      error: {
        invalidValue: "test",
        invalidPropertyNameComponents: [],
        message: `The value '"test"' does not match the expected type 'object'`,
      },
    },
  });

  validate({
    value: {
      foo: 123,
    },
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "string",
        },
      },
    },
    expectedResult: {
      valid: false,
      parsedValue: 123,
      error: {
        invalidValue: 123,
        invalidPropertyNameComponents: ["foo"],
        message:
          `The value '123' does not match the expected type 'string'. ` +
          `The invalid value is property 'foo' in {"foo":123}`,
      },
    },
  });

  validate({
    value: {
      foo: {
        bar: 456,
      },
    },
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "object",
          properties: {
            bar: {
              type: "string",
            },
          },
        },
      },
    },
    expectedResult: {
      valid: false,
      parsedValue: 456,
      error: {
        invalidValue: 456,
        invalidPropertyNameComponents: ["foo", "bar"],
        message:
          `The value '456' does not match the expected type 'string'. ` +
          `The invalid value is property 'foo.bar' in {"foo":{"bar":456}}`,
      },
    },
  });

  // null non-required property with strict=true: invalid
  validate({
    value: {
      foo: null,
    },
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "string",
        },
      },
    },
    expectedResult: {
      valid: false,
      parsedValue: null,
      error: {
        invalidValue: null,
        invalidPropertyNameComponents: ["foo"],
        message:
          `The value 'null' does not match the expected type 'string'. ` +
          `The invalid value is property 'foo' in {"foo":null}`,
      },
    },
  });
  validate({
    value: {
      foo: null,
    },
    schema: {
      type: "object",
      strict: true,
      properties: {
        foo: {
          type: "string",
        },
      },
    },
    options: {
      allowNullAsUndefinedProperties: true,
    },
    expectedResult: {
      valid: false,
      parsedValue: null,
      error: {
        invalidValue: null,
        invalidPropertyNameComponents: ["foo"],
        message:
          `The value 'null' does not match the expected type 'string'. ` +
          `The invalid value is property 'foo' in {"foo":null}`,
      },
    },
  });

  // non-null falsey non-required property with strict=false: invalid
  validate({
    value: {
      foo: false,
    },
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "string",
        },
      },
    },
    options: {
      allowExplicitUndefinedProperties: true,
      allowNullAsUndefinedProperties: true,
    },
    expectedResult: {
      valid: false,
      parsedValue: false,
      error: {
        invalidValue: false,
        invalidPropertyNameComponents: ["foo"],
        message:
          `The value 'false' does not match the expected type 'string'. ` +
          `The invalid value is property 'foo' in {"foo":false}`,
      },
    },
  });
  validate({
    value: {
      foo: false,
    },
    schema: {
      type: "object",
      strict: false,
      properties: {
        foo: {
          type: "string",
        },
      },
    },
    expectedResult: {
      valid: false,
      parsedValue: false,
      error: {
        invalidValue: false,
        invalidPropertyNameComponents: ["foo"],
        message:
          `The value 'false' does not match the expected type 'string'. ` +
          `The invalid value is property 'foo' in {"foo":false}`,
      },
    },
  });

  validate({
    value: {
      bogus: "test",
    },
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "string",
        },
      },
    },
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: { bogus: "test" },
        invalidPropertyNameComponents: [],
        message: `Object has unexpected property 'bogus'`,
      },
    },
  });

  validate({
    value: {
      foo: {
        bogus: "test",
      },
    },
    schema: {
      type: "object",
      properties: {
        foo: {
          type: "object",
          properties: {
            bar: {
              type: "string",
            },
          },
        },
      },
    },
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: { bogus: "test" },
        invalidPropertyNameComponents: ["foo"],
        message:
          `Object has unexpected property 'bogus'. The invalid value is ` +
          `property 'foo' in {"foo":{"bogus":"test"}}`,
      },
    },
  });
});

add_task(async function test_array_of_objects() {
  // This schema is used, for example, for bookmarks
  let schema = {
    type: "array",
    items: {
      type: "object",
      properties: {
        url: {
          type: "URL",
        },
        title: {
          type: "string",
        },
      },
    },
  };

  validate({
    value: [
      {
        url: "https://www.example.com/bookmark1",
        title: "Foo",
      },
      {
        url: "https://www.example.com/bookmark2",
        title: "Bar",
      },
    ],
    schema,
    expectedResult: {
      valid: true,
      parsedValue: [
        {
          url: new URL("https://www.example.com/bookmark1"),
          title: "Foo",
        },
        {
          url: new URL("https://www.example.com/bookmark2"),
          title: "Bar",
        },
      ],
    },
  });
});

add_task(async function test_missing_arrays_inside_objects() {
  let schema = {
    type: "object",
    properties: {
      allow: {
        type: "array",
        items: {
          type: "boolean",
        },
      },
      block: {
        type: "array",
        items: {
          type: "boolean",
        },
      },
    },
  };

  validate({
    value: {
      allow: [true, true, true],
    },
    schema,
    expectedResult: {
      valid: true,
      parsedValue: {
        allow: [true, true, true],
      },
    },
  });
});

add_task(async function test_required_vs_nonrequired_properties() {
  let schema = {
    type: "object",
    properties: {
      "non-required-property": {
        type: "number",
      },

      "required-property": {
        type: "number",
      },
    },
    required: ["required-property"],
  };

  validate({
    value: {
      "required-property": 5,
      "non-required-property": undefined,
    },
    schema,
    options: {
      allowExplicitUndefinedProperties: true,
    },
    expectedResult: {
      valid: true,
      parsedValue: {
        "required-property": 5,
      },
    },
  });

  validate({
    value: {
      "non-required-property": 5,
    },
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: {
          "non-required-property": 5,
        },
        invalidPropertyNameComponents: [],
        message: `Object is missing required property 'required-property'`,
      },
    },
  });
});

add_task(async function test_number_or_string_values() {
  let schema = {
    type: ["number", "string"],
  };

  validate({
    value: 1,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: 1,
    },
  });
  validate({
    value: "foobar",
    schema,
    expectedResult: {
      valid: true,
      parsedValue: "foobar",
    },
  });
  validate({
    value: "1",
    schema,
    expectedResult: {
      valid: true,
      parsedValue: "1",
    },
  });

  // Invalid values:
  validate({
    value: true,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: true,
        invalidPropertyNameComponents: [],
        message: `The value 'true' does not match any type in ["number","string"]`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match any type in ["number","string"]`,
      },
    },
  });
  validate({
    value: null,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: null,
        invalidPropertyNameComponents: [],
        message: `The value 'null' does not match any type in ["number","string"]`,
      },
    },
  });
});

add_task(async function test_number_or_array_values() {
  let schema = {
    type: ["number", "array"],
    items: {
      type: "number",
    },
  };

  validate({
    value: 1,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: 1,
    },
  });
  validate({
    value: [1, 2, 3],
    schema,
    expectedResult: {
      valid: true,
      parsedValue: [1, 2, 3],
    },
  });

  // Invalid values:
  validate({
    value: true,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: true,
        invalidPropertyNameComponents: [],
        message: `The value 'true' does not match any type in ["number","array"]`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match any type in ["number","array"]`,
      },
    },
  });
  validate({
    value: null,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: null,
        invalidPropertyNameComponents: [],
        message: `The value 'null' does not match any type in ["number","array"]`,
      },
    },
  });
  validate({
    value: ["a", "b"],
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: ["a", "b"],
        invalidPropertyNameComponents: [],
        message: `The value '["a","b"]' does not match any type in ["number","array"]`,
      },
    },
  });
  validate({
    value: [[]],
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: [[]],
        invalidPropertyNameComponents: [],
        message: `The value '[[]]' does not match any type in ["number","array"]`,
      },
    },
  });
  validate({
    value: [0, 1, [2, 3]],
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: [0, 1, [2, 3]],
        invalidPropertyNameComponents: [],
        message:
          `The value '[0,1,[2,3]]' does not match any type in ` +
          `["number","array"]`,
      },
    },
  });
});

add_task(function test_number_or_null_Values() {
  let schema = {
    type: ["number", "null"],
  };

  validate({
    value: 1,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: 1,
    },
  });
  validate({
    value: null,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: null,
    },
  });

  // Invalid values:
  validate({
    value: true,
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: true,
        invalidPropertyNameComponents: [],
        message: `The value 'true' does not match any type in ["number","null"]`,
      },
    },
  });
  validate({
    value: "string",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: "string",
        invalidPropertyNameComponents: [],
        message: `The value '"string"' does not match any type in ["number","null"]`,
      },
    },
  });
  validate({
    value: {},
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: {},
        invalidPropertyNameComponents: [],
        message: `The value '{}' does not match any type in ["number","null"]`,
      },
    },
  });
  validate({
    value: ["a", "b"],
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: ["a", "b"],
        invalidPropertyNameComponents: [],
        message: `The value '["a","b"]' does not match any type in ["number","null"]`,
      },
    },
  });
});

add_task(async function test_patternProperties() {
  let schema = {
    type: "object",
    properties: {
      "S-bool-property": { type: "boolean" },
    },
    patternProperties: {
      "^S-": { type: "string" },
      "^N-": { type: "number" },
      "^B-": { type: "boolean" },
    },
  };

  validate({
    value: {
      "S-string": "test",
      "N-number": 5,
      "B-boolean": true,
      "S-bool-property": false,
    },
    schema,
    expectedResult: {
      valid: true,
      parsedValue: {
        "S-string": "test",
        "N-number": 5,
        "B-boolean": true,
        "S-bool-property": false,
      },
    },
  });

  validate({
    value: {
      "N-string": "test",
    },
    schema,
    expectedResult: {
      valid: false,
      parsedValue: "test",
      error: {
        invalidValue: "test",
        invalidPropertyNameComponents: ["N-string"],
        message:
          `The value '"test"' does not match the expected type 'number'. ` +
          `The invalid value is property 'N-string' in {"N-string":"test"}`,
      },
    },
  });

  validate({
    value: {
      "S-number": 5,
    },
    schema,
    expectedResult: {
      valid: false,
      parsedValue: 5,
      error: {
        invalidValue: 5,
        invalidPropertyNameComponents: ["S-number"],
        message:
          `The value '5' does not match the expected type 'string'. ` +
          `The invalid value is property 'S-number' in {"S-number":5}`,
      },
    },
  });

  schema = {
    type: "object",
    patternProperties: {
      "[": { " type": "string" },
    },
  };

  Assert.throws(
    () => JsonSchemaValidator.validate({}, schema),
    /Invalid property pattern/,
    "Checking that invalid property patterns throw"
  );
});

add_task(async function test_JSON_type() {
  let schema = {
    type: "JSON",
  };

  validate({
    value: {
      a: "b",
    },
    schema,
    expectedResult: {
      valid: true,
      parsedValue: {
        a: "b",
      },
    },
  });
  validate({
    value: '{"a": "b"}',
    schema,
    expectedResult: {
      valid: true,
      parsedValue: {
        a: "b",
      },
    },
  });

  validate({
    value: "{This{is{not{JSON}}}}",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: "{This{is{not{JSON}}}}",
        invalidPropertyNameComponents: [],
        message: `JSON string could not be parsed: "{This{is{not{JSON}}}}"`,
      },
    },
  });
  validate({
    value: "0",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: "0",
        invalidPropertyNameComponents: [],
        message: `JSON was not an object: "0"`,
      },
    },
  });
  validate({
    value: "true",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: "true",
        invalidPropertyNameComponents: [],
        message: `JSON was not an object: "true"`,
      },
    },
  });
});

add_task(async function test_enum() {
  let schema = {
    type: "string",
    enum: ["one", "two"],
  };

  validate({
    value: "one",
    schema,
    expectedResult: {
      valid: true,
      parsedValue: "one",
    },
  });

  validate({
    value: "three",
    schema,
    expectedResult: {
      valid: false,
      parsedValue: undefined,
      error: {
        invalidValue: "three",
        invalidPropertyNameComponents: [],
        message:
          `The value '"three"' is not one of the enumerated values ` +
          `["one","two"]`,
      },
    },
  });
});

add_task(async function test_bool_enum() {
  let schema = {
    type: "boolean",
    enum: ["one", "two"],
  };

  // `enum` is ignored because `type` is boolean.
  validate({
    value: true,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: true,
    },
  });
});

add_task(async function test_boolint_enum() {
  let schema = {
    type: "boolean",
    enum: ["one", "two"],
  };

  // `enum` is ignored because `type` is boolean and the integer value was
  // coerced to boolean.
  validate({
    value: 1,
    schema,
    expectedResult: {
      valid: true,
      parsedValue: true,
    },
  });
});

/**
 * Validates a value against a schema and asserts that the result is as
 * expected.
 *
 * @param {object} params
 * @param {*} params.value
 *   The value to validate.
 * @param {object} params.schema
 *   The schema to validate against.
 * @param {object} params.expectedResult
 *   The expected result.  See JsonSchemaValidator.validate for what this object
 *   should look like.  If the expected result is invalid, then this object
 *   should have an `error` property with all the properties of validation
 *   errors, including `message`, except that `rootValue` and `rootSchema` are
 *   unnecessary because this function will add them for you.
 * @param {object} [params.options]
 *   Options to pass to JsonSchemaValidator.validate.
 * @return {object} The return value of JsonSchemaValidator.validate, which is
 *   a result.
 */
function validate({ value, schema, expectedResult, options = undefined }) {
  let result = JsonSchemaValidator.validate(value, schema, options);

  checkObject(
    result,
    expectedResult,
    {
      valid: false,
      parsedValue: true,
    },
    "Checking result property: "
  );

  Assert.equal("error" in result, "error" in expectedResult, "result.error");
  if (result.error && expectedResult.error) {
    expectedResult.error = Object.assign(expectedResult.error, {
      rootValue: value,
      rootSchema: schema,
    });
    checkObject(
      result.error,
      expectedResult.error,
      {
        rootValue: true,
        rootSchema: false,
        invalidPropertyNameComponents: false,
        invalidValue: true,
        message: false,
      },
      "Checking result.error property: "
    );
  }

  return result;
}

/**
 * Asserts that an object is the same as an expected object.
 *
 * @param {*} actual
 *   The actual object.
 * @param {*} expected
 *   The expected object.
 * @param {object} properties
 *   The properties to compare in the two objects.  This value should be an
 *   object.  The keys are the names of properties in the two objects.  The
 *   values are booleans: true means that the property should be compared using
 *   strict equality and false means deep equality.  Deep equality is used if
 *   the property is an object.
 */
function checkObject(actual, expected, properties, message) {
  for (let [name, strict] of Object.entries(properties)) {
    let assertFunc =
      !strict || typeof expected[name] == "object"
        ? "deepEqual"
        : "strictEqual";
    Assert[assertFunc](actual[name], expected[name], message + name);
  }
}
