"use strict";

const global = this;

let json = [
  {
    namespace: "testing",

    properties: {
      PROP1: { value: 20 },
      prop2: { type: "string" },
      prop3: {
        $ref: "submodule",
      },
      prop4: {
        $ref: "submodule",
        unsupported: true,
      },
    },

    types: [
      {
        id: "type1",
        type: "string",
        enum: ["value1", "value2", "value3"],
      },

      {
        id: "type2",
        type: "object",
        properties: {
          prop1: { type: "integer" },
          prop2: { type: "array", items: { $ref: "type1" } },
        },
      },

      {
        id: "basetype1",
        type: "object",
        properties: {
          prop1: { type: "string" },
        },
      },

      {
        id: "basetype2",
        choices: [{ type: "integer" }],
      },

      {
        $extend: "basetype1",
        properties: {
          prop2: { type: "string" },
        },
      },

      {
        $extend: "basetype2",
        choices: [{ type: "string" }],
      },

      {
        id: "basetype3",
        type: "object",
        properties: {
          baseprop: { type: "string" },
        },
      },

      {
        id: "derivedtype1",
        type: "object",
        $import: "basetype3",
        properties: {
          derivedprop: { type: "string" },
        },
      },

      {
        id: "derivedtype2",
        type: "object",
        $import: "basetype3",
        properties: {
          derivedprop: { type: "integer" },
        },
      },

      {
        id: "submodule",
        type: "object",
        functions: [
          {
            name: "sub_foo",
            type: "function",
            parameters: [],
            returns: { type: "integer" },
          },
        ],
      },
    ],

    functions: [
      {
        name: "foo",
        type: "function",
        parameters: [
          { name: "arg1", type: "integer", optional: true, default: 99 },
          { name: "arg2", type: "boolean", optional: true },
        ],
      },

      {
        name: "bar",
        type: "function",
        parameters: [
          { name: "arg1", type: "integer", optional: true },
          { name: "arg2", type: "boolean" },
        ],
      },

      {
        name: "baz",
        type: "function",
        parameters: [
          {
            name: "arg1",
            type: "object",
            properties: {
              prop1: { type: "string" },
              prop2: { type: "integer", optional: true },
              prop3: { type: "integer", unsupported: true },
            },
          },
        ],
      },

      {
        name: "qux",
        type: "function",
        parameters: [{ name: "arg1", $ref: "type1" }],
      },

      {
        name: "quack",
        type: "function",
        parameters: [{ name: "arg1", $ref: "type2" }],
      },

      {
        name: "quora",
        type: "function",
        parameters: [{ name: "arg1", type: "function" }],
      },

      {
        name: "quileute",
        type: "function",
        parameters: [
          { name: "arg1", type: "integer", optional: true },
          { name: "arg2", type: "integer" },
        ],
      },

      {
        name: "queets",
        type: "function",
        unsupported: true,
        parameters: [],
      },

      {
        name: "quintuplets",
        type: "function",
        parameters: [
          {
            name: "obj",
            type: "object",
            properties: [],
            additionalProperties: { type: "integer" },
          },
        ],
      },

      {
        name: "quasar",
        type: "function",
        parameters: [
          {
            name: "abc",
            type: "object",
            properties: {
              func: {
                type: "function",
                parameters: [{ name: "x", type: "integer" }],
              },
            },
          },
        ],
      },

      {
        name: "quosimodo",
        type: "function",
        parameters: [
          {
            name: "xyz",
            type: "object",
            additionalProperties: { type: "any" },
          },
        ],
      },

      {
        name: "patternprop",
        type: "function",
        parameters: [
          {
            name: "obj",
            type: "object",
            properties: { prop1: { type: "string", pattern: "^\\d+$" } },
            patternProperties: {
              "(?i)^prop\\d+$": { type: "string" },
              "^foo\\d+$": { type: "string" },
            },
          },
        ],
      },

      {
        name: "pattern",
        type: "function",
        parameters: [
          { name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$" },
        ],
      },

      {
        name: "format",
        type: "function",
        parameters: [
          {
            name: "arg",
            type: "object",
            properties: {
              hostname: { type: "string", format: "hostname", optional: true },
              canonicalDomain: {
                type: "string",
                format: "canonicalDomain",
                optional: "omit-key-if-missing",
              },
              url: { type: "string", format: "url", optional: true },
              origin: { type: "string", format: "origin", optional: true },
              relativeUrl: {
                type: "string",
                format: "relativeUrl",
                optional: true,
              },
              strictRelativeUrl: {
                type: "string",
                format: "strictRelativeUrl",
                optional: true,
              },
              imageDataOrStrictRelativeUrl: {
                type: "string",
                format: "imageDataOrStrictRelativeUrl",
                optional: true,
              },
            },
          },
        ],
      },

      {
        name: "formatDate",
        type: "function",
        parameters: [
          {
            name: "arg",
            type: "object",
            properties: {
              date: { type: "string", format: "date", optional: true },
            },
          },
        ],
      },

      {
        name: "deep",
        type: "function",
        parameters: [
          {
            name: "arg",
            type: "object",
            properties: {
              foo: {
                type: "object",
                properties: {
                  bar: {
                    type: "array",
                    items: {
                      type: "object",
                      properties: {
                        baz: {
                          type: "object",
                          properties: {
                            required: { type: "integer" },
                            optional: { type: "string", optional: true },
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
          },
        ],
      },

      {
        name: "errors",
        type: "function",
        parameters: [
          {
            name: "arg",
            type: "object",
            properties: {
              warn: {
                type: "string",
                pattern: "^\\d+$",
                optional: true,
                onError: "warn",
              },
              ignore: {
                type: "string",
                pattern: "^\\d+$",
                optional: true,
                onError: "ignore",
              },
              default: {
                type: "string",
                pattern: "^\\d+$",
                optional: true,
              },
            },
          },
        ],
      },

      {
        name: "localize",
        type: "function",
        parameters: [
          {
            name: "arg",
            type: "object",
            properties: {
              foo: { type: "string", preprocess: "localize", optional: true },
              bar: { type: "string", optional: true },
              url: {
                type: "string",
                preprocess: "localize",
                format: "url",
                optional: true,
              },
            },
          },
        ],
      },

      {
        name: "extended1",
        type: "function",
        parameters: [{ name: "val", $ref: "basetype1" }],
      },

      {
        name: "extended2",
        type: "function",
        parameters: [{ name: "val", $ref: "basetype2" }],
      },

      {
        name: "callderived1",
        type: "function",
        parameters: [{ name: "value", $ref: "derivedtype1" }],
      },

      {
        name: "callderived2",
        type: "function",
        parameters: [{ name: "value", $ref: "derivedtype2" }],
      },
    ],

    events: [
      {
        name: "onFoo",
        type: "function",
      },

      {
        name: "onBar",
        type: "function",
        extraParameters: [
          {
            name: "filter",
            type: "integer",
            optional: true,
            default: 1,
          },
        ],
      },
    ],
  },
  {
    namespace: "foreign",
    properties: {
      foreignRef: { $ref: "testing.submodule" },
    },
  },
  {
    namespace: "inject",
    properties: {
      PROP1: { value: "should inject" },
    },
  },
  {
    namespace: "do-not-inject",
    properties: {
      PROP1: { value: "should not inject" },
    },
  },
];

add_task(async function () {
  let wrapper = getContextWrapper();
  let url = "data:," + JSON.stringify(json);
  Schemas._rootSchema = null;
  await Schemas.load(url);

  let root = {};
  Schemas.inject(root, wrapper);

  Assert.equal(root.testing.PROP1, 20, "simple value property");
  Assert.equal(root.testing.type1.VALUE1, "value1", "enum type");
  Assert.equal(root.testing.type1.VALUE2, "value2", "enum type");

  Assert.equal("inject" in root, true, "namespace 'inject' should be injected");
  Assert.equal(
    root["do-not-inject"],
    undefined,
    "namespace 'do-not-inject' should not be injected"
  );

  root.testing.foo(11, true);
  wrapper.verify("call", "testing", "foo", [11, true]);

  root.testing.foo(true);
  wrapper.verify("call", "testing", "foo", [99, true]);

  root.testing.foo(null, true);
  wrapper.verify("call", "testing", "foo", [99, true]);

  root.testing.foo(undefined, true);
  wrapper.verify("call", "testing", "foo", [99, true]);

  root.testing.foo(11);
  wrapper.verify("call", "testing", "foo", [11, null]);

  Assert.throws(
    () => root.testing.bar(11),
    /Incorrect argument types/,
    "should throw without required arg"
  );

  Assert.throws(
    () => root.testing.bar(11, true, 10),
    /Incorrect argument types/,
    "should throw with too many arguments"
  );

  root.testing.bar(true);
  wrapper.verify("call", "testing", "bar", [null, true]);

  root.testing.baz({ prop1: "hello", prop2: 22 });
  wrapper.verify("call", "testing", "baz", [{ prop1: "hello", prop2: 22 }]);

  root.testing.baz({ prop1: "hello" });
  wrapper.verify("call", "testing", "baz", [{ prop1: "hello", prop2: null }]);

  root.testing.baz({ prop1: "hello", prop2: null });
  wrapper.verify("call", "testing", "baz", [{ prop1: "hello", prop2: null }]);

  Assert.throws(
    () => root.testing.baz({ prop2: 12 }),
    /Property "prop1" is required/,
    "should throw without required property"
  );

  Assert.throws(
    () => root.testing.baz({ prop1: "hi", prop3: 12 }),
    /Property "prop3" is unsupported by Firefox/,
    "should throw with unsupported property"
  );

  Assert.throws(
    () => root.testing.baz({ prop1: "hi", prop4: 12 }),
    /Unexpected property "prop4"/,
    "should throw with unexpected property"
  );

  Assert.throws(
    () => root.testing.baz({ prop1: 12 }),
    /Expected string instead of 12/,
    "should throw with wrong type"
  );

  root.testing.qux("value2");
  wrapper.verify("call", "testing", "qux", ["value2"]);

  Assert.throws(
    () => root.testing.qux("value4"),
    /Invalid enumeration value "value4"/,
    "should throw for invalid enum value"
  );

  root.testing.quack({ prop1: 12, prop2: ["value1", "value3"] });
  wrapper.verify("call", "testing", "quack", [
    { prop1: 12, prop2: ["value1", "value3"] },
  ]);

  Assert.throws(
    () =>
      root.testing.quack({ prop1: 12, prop2: ["value1", "value3", "value4"] }),
    /Invalid enumeration value "value4"/,
    "should throw for invalid array type"
  );

  function f() {}
  root.testing.quora(f);
  Assert.equal(
    JSON.stringify(wrapper.tallied.slice(0, -1)),
    JSON.stringify(["call", "testing", "quora"])
  );
  Assert.equal(wrapper.tallied[3][0], f);
  wrapper.tallied = null;

  let g = () => 0;
  root.testing.quora(g);
  Assert.equal(
    JSON.stringify(wrapper.tallied.slice(0, -1)),
    JSON.stringify(["call", "testing", "quora"])
  );
  Assert.equal(wrapper.tallied[3][0], g);
  wrapper.tallied = null;

  root.testing.quileute(10);
  wrapper.verify("call", "testing", "quileute", [null, 10]);

  Assert.throws(
    () => root.testing.queets(),
    /queets is not a function/,
    "should throw for unsupported functions"
  );

  root.testing.quintuplets({ a: 10, b: 20, c: 30 });
  wrapper.verify("call", "testing", "quintuplets", [{ a: 10, b: 20, c: 30 }]);

  Assert.throws(
    () => root.testing.quintuplets({ a: 10, b: 20, c: 30, d: "hi" }),
    /Expected integer instead of "hi"/,
    "should throw for wrong additionalProperties type"
  );

  root.testing.quasar({ func: f });
  Assert.equal(
    JSON.stringify(wrapper.tallied.slice(0, -1)),
    JSON.stringify(["call", "testing", "quasar"])
  );
  Assert.equal(wrapper.tallied[3][0].func, f);

  root.testing.quosimodo({ a: 10, b: 20, c: 30 });
  wrapper.verify("call", "testing", "quosimodo", [{ a: 10, b: 20, c: 30 }]);

  Assert.throws(
    () => root.testing.quosimodo(10),
    /Incorrect argument types/,
    "should throw for wrong type"
  );

  root.testing.patternprop({
    prop1: "12",
    prop2: "42",
    Prop3: "43",
    foo1: "x",
  });
  wrapper.verify("call", "testing", "patternprop", [
    { prop1: "12", prop2: "42", Prop3: "43", foo1: "x" },
  ]);

  root.testing.patternprop({ prop1: "12" });
  wrapper.verify("call", "testing", "patternprop", [{ prop1: "12" }]);

  Assert.throws(
    () => root.testing.patternprop({ prop1: "12", foo1: null }),
    /Expected string instead of null/,
    "should throw for wrong property type"
  );

  Assert.throws(
    () => root.testing.patternprop({ prop1: "xx", prop2: "yy" }),
    /String "xx" must match \/\^\\d\+\$\//,
    "should throw for wrong property type"
  );

  Assert.throws(
    () => root.testing.patternprop({ prop1: "12", prop2: 42 }),
    /Expected string instead of 42/,
    "should throw for wrong property type"
  );

  Assert.throws(
    () => root.testing.patternprop({ prop1: "12", prop2: null }),
    /Expected string instead of null/,
    "should throw for wrong property type"
  );

  Assert.throws(
    () => root.testing.patternprop({ prop1: "12", propx: "42" }),
    /Unexpected property "propx"/,
    "should throw for unexpected property"
  );

  Assert.throws(
    () => root.testing.patternprop({ prop1: "12", Foo1: "x" }),
    /Unexpected property "Foo1"/,
    "should throw for unexpected property"
  );

  root.testing.pattern("DEADbeef");
  wrapper.verify("call", "testing", "pattern", ["DEADbeef"]);

  Assert.throws(
    () => root.testing.pattern("DEADcow"),
    /String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
    "should throw for non-match"
  );

  root.testing.format({ hostname: "foo" });
  wrapper.verify("call", "testing", "format", [
    {
      hostname: "foo",
      imageDataOrStrictRelativeUrl: null,
      origin: null,
      relativeUrl: null,
      strictRelativeUrl: null,
      url: null,
    },
  ]);

  for (let invalid of ["", " ", "http://foo", "foo/bar", "foo.com/", "foo?"]) {
    Assert.throws(
      () => root.testing.format({ hostname: invalid }),
      /Invalid hostname/,
      "should throw for invalid hostname"
    );
    Assert.throws(
      () => root.testing.format({ canonicalDomain: invalid }),
      /Invalid domain /,
      `should throw for invalid canonicalDomain (${invalid})`
    );
  }

  for (let invalid of [
    "%61", // ASCII should not be URL-encoded.
    "foo:12345", // It is a common mistake to use .host instead of .hostname.
    "2", // Single digit is an IPv4 address, but should be written as 0.0.0.2.
    "::1", // IPv6 addresses should have brackets.
    "[::1A]", // not lowercase.
    "[::ffff:127.0.0.1]", // not a canonical IPv6 representation.
    "UPPERCASE", // not lowercase.
    "straß.de", // not punycode.
  ]) {
    Assert.throws(
      () => root.testing.format({ canonicalDomain: invalid }),
      /Invalid domain /,
      `should throw for invalid canonicalDomain (${invalid})`
    );
  }

  for (let valid of ["0.0.0.2", "[::1]", "[::1a]", "lowercase", "."]) {
    root.testing.format({ canonicalDomain: valid });
    wrapper.verify("call", "testing", "format", [
      {
        canonicalDomain: valid,
        hostname: null,
        imageDataOrStrictRelativeUrl: null,
        origin: null,
        relativeUrl: null,
        strictRelativeUrl: null,
        url: null,
      },
    ]);
  }

  for (let valid of [
    "https://example.com",
    "http://example.com",
    "https://foo.bar.栃木.jp",
  ]) {
    root.testing.format({ origin: valid });
  }

  for (let invalid of [
    "https://example.com/testing",
    "file:/foo/bar",
    "file:///foo/bar",
    "",
    " ",
    "https://foo.bar.栃木.jp/",
    "https://user:pass@example.com",
    "https://*.example.com",
    "https://example.com#test",
    "https://example.com?test",
  ]) {
    Assert.throws(
      () => root.testing.format({ origin: invalid }),
      /Invalid origin/,
      "should throw for invalid origin"
    );
  }

  root.testing.format({ url: "http://foo/bar", relativeUrl: "http://foo/bar" });
  wrapper.verify("call", "testing", "format", [
    {
      hostname: null,
      imageDataOrStrictRelativeUrl: null,
      origin: null,
      relativeUrl: "http://foo/bar",
      strictRelativeUrl: null,
      url: "http://foo/bar",
    },
  ]);

  root.testing.format({
    relativeUrl: "foo.html",
    strictRelativeUrl: "foo.html",
  });
  wrapper.verify("call", "testing", "format", [
    {
      hostname: null,
      imageDataOrStrictRelativeUrl: null,
      origin: null,
      relativeUrl: `${wrapper.url}foo.html`,
      strictRelativeUrl: `${wrapper.url}foo.html`,
      url: null,
    },
  ]);

  root.testing.format({
    imageDataOrStrictRelativeUrl: "data:image/png;base64,A",
  });
  wrapper.verify("call", "testing", "format", [
    {
      hostname: null,
      imageDataOrStrictRelativeUrl: "data:image/png;base64,A",
      origin: null,
      relativeUrl: null,
      strictRelativeUrl: null,
      url: null,
    },
  ]);

  root.testing.format({
    imageDataOrStrictRelativeUrl: "data:image/jpeg;base64,A",
  });
  wrapper.verify("call", "testing", "format", [
    {
      hostname: null,
      imageDataOrStrictRelativeUrl: "data:image/jpeg;base64,A",
      origin: null,
      relativeUrl: null,
      strictRelativeUrl: null,
      url: null,
    },
  ]);

  root.testing.format({ imageDataOrStrictRelativeUrl: "foo.html" });
  wrapper.verify("call", "testing", "format", [
    {
      hostname: null,
      imageDataOrStrictRelativeUrl: `${wrapper.url}foo.html`,
      origin: null,
      relativeUrl: null,
      strictRelativeUrl: null,
      url: null,
    },
  ]);

  for (let format of ["url", "relativeUrl"]) {
    Assert.throws(
      () => root.testing.format({ [format]: "chrome://foo/content/" }),
      /Access denied/,
      "should throw for access denied"
    );
  }

  for (let urlString of ["//foo.html", "http://foo/bar.html"]) {
    Assert.throws(
      () => root.testing.format({ strictRelativeUrl: urlString }),
      /must be a relative URL/,
      "should throw for non-relative URL"
    );
  }

  Assert.throws(
    () =>
      root.testing.format({
        imageDataOrStrictRelativeUrl: "data:image/svg+xml;utf8,A",
      }),
    /must be a relative or PNG or JPG data:image URL/,
    "should throw for non-relative or non PNG/JPG data URL"
  );

  const dates = [
    "2016-03-04",
    "2016-03-04T08:00:00Z",
    "2016-03-04T08:00:00.000Z",
    "2016-03-04T08:00:00-08:00",
    "2016-03-04T08:00:00.000-08:00",
    "2016-03-04T08:00:00+08:00",
    "2016-03-04T08:00:00.000+08:00",
    "2016-03-04T08:00:00+0800",
    "2016-03-04T08:00:00-0800",
  ];
  dates.forEach(str => {
    root.testing.formatDate({ date: str });
    wrapper.verify("call", "testing", "formatDate", [{ date: str }]);
  });

  // Make sure that a trivial change to a valid date invalidates it.
  dates.forEach(str => {
    Assert.throws(
      () => root.testing.formatDate({ date: "0" + str }),
      /Invalid date string/,
      "should throw for invalid iso date string"
    );
    Assert.throws(
      () => root.testing.formatDate({ date: str + "0" }),
      /Invalid date string/,
      "should throw for invalid iso date string"
    );
  });

  const badDates = [
    "I do not look anything like a date string",
    "2016-99-99",
    "2016-03-04T25:00:00Z",
  ];
  badDates.forEach(str => {
    Assert.throws(
      () => root.testing.formatDate({ date: str }),
      /Invalid date string/,
      "should throw for invalid iso date string"
    );
  });

  root.testing.deep({
    foo: { bar: [{ baz: { required: 12, optional: "42" } }] },
  });
  wrapper.verify("call", "testing", "deep", [
    { foo: { bar: [{ baz: { optional: "42", required: 12 } }] } },
  ]);

  Assert.throws(
    () => root.testing.deep({ foo: { bar: [{ baz: { optional: "42" } }] } }),
    /Type error for parameter arg \(Error processing foo\.bar\.0\.baz: Property "required" is required\) for testing\.deep/,
    "should throw with the correct object path"
  );

  Assert.throws(
    () =>
      root.testing.deep({
        foo: { bar: [{ baz: { optional: 42, required: 12 } }] },
      }),
    /Type error for parameter arg \(Error processing foo\.bar\.0\.baz\.optional: Expected string instead of 42\) for testing\.deep/,
    "should throw with the correct object path"
  );

  wrapper.talliedErrors.length = 0;

  root.testing.errors({ default: "0123", ignore: "0123", warn: "0123" });
  wrapper.verify("call", "testing", "errors", [
    { default: "0123", ignore: "0123", warn: "0123" },
  ]);
  wrapper.checkErrors([]);

  root.testing.errors({ default: "0123", ignore: "x123", warn: "0123" });
  wrapper.verify("call", "testing", "errors", [
    { default: "0123", ignore: null, warn: "0123" },
  ]);
  wrapper.checkErrors([]);

  ExtensionTestUtils.failOnSchemaWarnings(false);
  root.testing.errors({ default: "0123", ignore: "0123", warn: "x123" });
  ExtensionTestUtils.failOnSchemaWarnings(true);
  wrapper.verify("call", "testing", "errors", [
    { default: "0123", ignore: "0123", warn: null },
  ]);
  wrapper.checkErrors(['String "x123" must match /^\\d+$/']);

  root.testing.onFoo.addListener(f);
  Assert.equal(
    JSON.stringify(wrapper.tallied.slice(0, -1)),
    JSON.stringify(["addListener", "testing", "onFoo"])
  );
  Assert.equal(wrapper.tallied[3][0], f);
  Assert.equal(JSON.stringify(wrapper.tallied[3][1]), JSON.stringify([]));
  wrapper.tallied = null;

  root.testing.onFoo.removeListener(f);
  Assert.equal(
    JSON.stringify(wrapper.tallied.slice(0, -1)),
    JSON.stringify(["removeListener", "testing", "onFoo"])
  );
  Assert.equal(wrapper.tallied[3][0], f);
  wrapper.tallied = null;

  root.testing.onFoo.hasListener(f);
  Assert.equal(
    JSON.stringify(wrapper.tallied.slice(0, -1)),
    JSON.stringify(["hasListener", "testing", "onFoo"])
  );
  Assert.equal(wrapper.tallied[3][0], f);
  wrapper.tallied = null;

  Assert.throws(
    () => root.testing.onFoo.addListener(10),
    /Invalid listener/,
    "addListener with non-function should throw"
  );

  root.testing.onBar.addListener(f, 10);
  Assert.equal(
    JSON.stringify(wrapper.tallied.slice(0, -1)),
    JSON.stringify(["addListener", "testing", "onBar"])
  );
  Assert.equal(wrapper.tallied[3][0], f);
  Assert.equal(JSON.stringify(wrapper.tallied[3][1]), JSON.stringify([10]));
  wrapper.tallied = null;

  root.testing.onBar.addListener(f);
  Assert.equal(
    JSON.stringify(wrapper.tallied.slice(0, -1)),
    JSON.stringify(["addListener", "testing", "onBar"])
  );
  Assert.equal(wrapper.tallied[3][0], f);
  Assert.equal(JSON.stringify(wrapper.tallied[3][1]), JSON.stringify([1]));
  wrapper.tallied = null;

  Assert.throws(
    () => root.testing.onBar.addListener(f, "hi"),
    /Incorrect argument types/,
    "addListener with wrong extra parameter should throw"
  );

  let target = { prop1: 12, prop2: ["value1", "value3"] };
  let proxy = new Proxy(target, {});
  Assert.throws(
    () => root.testing.quack(proxy),
    /Expected a plain JavaScript object, got a Proxy/,
    "should throw when passing a Proxy"
  );

  if (Symbol.toStringTag) {
    let stringTarget = { prop1: 12, prop2: ["value1", "value3"] };
    stringTarget[Symbol.toStringTag] = () => "[object Object]";
    let stringProxy = new Proxy(stringTarget, {});
    Assert.throws(
      () => root.testing.quack(stringProxy),
      /Expected a plain JavaScript object, got a Proxy/,
      "should throw when passing a Proxy"
    );
  }

  root.testing.localize({
    foo: "__MSG_foo__",
    bar: "__MSG_foo__",
    url: "__MSG_http://example.com/__",
  });
  wrapper.verify("call", "testing", "localize", [
    { bar: "__MSG_foo__", foo: "FOO", url: "http://example.com/" },
  ]);

  Assert.throws(
    () => root.testing.localize({ url: "__MSG_/foo/bar__" }),
    /\/FOO\/BAR is not a valid URL\./,
    "should throw for invalid URL"
  );

  root.testing.extended1({ prop1: "foo", prop2: "bar" });
  wrapper.verify("call", "testing", "extended1", [
    { prop1: "foo", prop2: "bar" },
  ]);

  Assert.throws(
    () => root.testing.extended1({ prop1: "foo", prop2: 12 }),
    /Expected string instead of 12/,
    "should throw for wrong property type"
  );

  Assert.throws(
    () => root.testing.extended1({ prop1: "foo" }),
    /Property "prop2" is required/,
    "should throw for missing property"
  );

  Assert.throws(
    () => root.testing.extended1({ prop1: "foo", prop2: "bar", prop3: "xxx" }),
    /Unexpected property "prop3"/,
    "should throw for extra property"
  );

  root.testing.extended2("foo");
  wrapper.verify("call", "testing", "extended2", ["foo"]);

  root.testing.extended2(12);
  wrapper.verify("call", "testing", "extended2", [12]);

  Assert.throws(
    () => root.testing.extended2(true),
    /Incorrect argument types/,
    "should throw for wrong argument type"
  );

  root.testing.prop3.sub_foo();
  wrapper.verify("call", "testing.prop3", "sub_foo", []);

  Assert.throws(
    () => root.testing.prop4.sub_foo(),
    /root.testing.prop4 is undefined/,
    "should throw for unsupported submodule"
  );

  root.foreign.foreignRef.sub_foo();
  wrapper.verify("call", "foreign.foreignRef", "sub_foo", []);

  root.testing.callderived1({ baseprop: "s1", derivedprop: "s2" });
  wrapper.verify("call", "testing", "callderived1", [
    { baseprop: "s1", derivedprop: "s2" },
  ]);

  Assert.throws(
    () => root.testing.callderived1({ baseprop: "s1", derivedprop: 42 }),
    /Error processing derivedprop: Expected string/,
    "Two different objects may $import the same base object"
  );
  Assert.throws(
    () => root.testing.callderived1({ baseprop: "s1" }),
    /Property "derivedprop" is required/,
    "Object using $import has its local properites"
  );
  Assert.throws(
    () => root.testing.callderived1({ derivedprop: "s2" }),
    /Property "baseprop" is required/,
    "Object using $import has imported properites"
  );

  root.testing.callderived2({ baseprop: "s1", derivedprop: 42 });
  wrapper.verify("call", "testing", "callderived2", [
    { baseprop: "s1", derivedprop: 42 },
  ]);

  Assert.throws(
    () => root.testing.callderived2({ baseprop: "s1", derivedprop: "s2" }),
    /Error processing derivedprop: Expected integer/,
    "Two different objects may $import the same base object"
  );
  Assert.throws(
    () => root.testing.callderived2({ baseprop: "s1" }),
    /Property "derivedprop" is required/,
    "Object using $import has its local properites"
  );
  Assert.throws(
    () => root.testing.callderived2({ derivedprop: 42 }),
    /Property "baseprop" is required/,
    "Object using $import has imported properites"
  );
});

let deprecatedJson = [
  {
    namespace: "deprecated",

    properties: {
      accessor: {
        type: "string",
        writable: true,
        deprecated: "This is not the property you are looking for",
      },
    },

    types: [
      {
        id: "Type",
        type: "string",
      },
    ],

    functions: [
      {
        name: "property",
        type: "function",
        parameters: [
          {
            name: "arg",
            type: "object",
            properties: {
              foo: {
                type: "string",
              },
            },
            additionalProperties: {
              type: "any",
              deprecated: "Unknown property",
            },
          },
        ],
      },

      {
        name: "value",
        type: "function",
        parameters: [
          {
            name: "arg",
            choices: [
              {
                type: "integer",
              },
              {
                type: "string",
                deprecated: "Please use an integer, not ${value}",
              },
            ],
          },
        ],
      },

      {
        name: "choices",
        type: "function",
        parameters: [
          {
            name: "arg",
            deprecated: "You have no choices",
            choices: [
              {
                type: "integer",
              },
            ],
          },
        ],
      },

      {
        name: "ref",
        type: "function",
        parameters: [
          {
            name: "arg",
            choices: [
              {
                $ref: "Type",
                deprecated: "Deprecated alias",
              },
            ],
          },
        ],
      },

      {
        name: "method",
        type: "function",
        deprecated: "Do not call this method",
        parameters: [],
      },
    ],

    events: [
      {
        name: "onDeprecated",
        type: "function",
        deprecated: "This event does not work",
      },
    ],
  },
];

add_task(async function testDeprecation() {
  let wrapper = getContextWrapper();
  // This whole test expects deprecation warnings.
  ExtensionTestUtils.failOnSchemaWarnings(false);

  let url = "data:," + JSON.stringify(deprecatedJson);
  Schemas._rootSchema = null;
  await Schemas.load(url);

  let root = {};
  Schemas.inject(root, wrapper);

  root.deprecated.property({ foo: "bar", xxx: "any", yyy: "property" });
  wrapper.verify("call", "deprecated", "property", [
    { foo: "bar", xxx: "any", yyy: "property" },
  ]);
  wrapper.checkErrors([
    "Warning processing xxx: Unknown property",
    "Warning processing yyy: Unknown property",
  ]);

  root.deprecated.value(12);
  wrapper.verify("call", "deprecated", "value", [12]);
  wrapper.checkErrors([]);

  root.deprecated.value("12");
  wrapper.verify("call", "deprecated", "value", ["12"]);
  wrapper.checkErrors(['Please use an integer, not "12"']);

  root.deprecated.choices(12);
  wrapper.verify("call", "deprecated", "choices", [12]);
  wrapper.checkErrors(["You have no choices"]);

  root.deprecated.ref("12");
  wrapper.verify("call", "deprecated", "ref", ["12"]);
  wrapper.checkErrors(["Deprecated alias"]);

  root.deprecated.method();
  wrapper.verify("call", "deprecated", "method", []);
  wrapper.checkErrors(["Do not call this method"]);

  void root.deprecated.accessor;
  wrapper.verify("get", "deprecated", "accessor", null);
  wrapper.checkErrors(["This is not the property you are looking for"]);

  root.deprecated.accessor = "x";
  wrapper.verify("set", "deprecated", "accessor", "x");
  wrapper.checkErrors(["This is not the property you are looking for"]);

  root.deprecated.onDeprecated.addListener(() => {});
  wrapper.checkErrors(["This event does not work"]);

  root.deprecated.onDeprecated.removeListener(() => {});
  wrapper.checkErrors(["This event does not work"]);

  root.deprecated.onDeprecated.hasListener(() => {});
  wrapper.checkErrors(["This event does not work"]);

  ExtensionTestUtils.failOnSchemaWarnings(true);

  Assert.throws(
    () => root.deprecated.onDeprecated.hasListener(() => {}),
    /This event does not work/,
    "Deprecation warning with extensions.webextensions.warnings-as-errors=true"
  );
});

let choicesJson = [
  {
    namespace: "choices",

    types: [],

    functions: [
      {
        name: "meh",
        type: "function",
        parameters: [
          {
            name: "arg",
            choices: [
              {
                type: "string",
                enum: ["foo", "bar", "baz"],
              },
              {
                type: "string",
                pattern: "florg.*meh",
              },
              {
                type: "integer",
                minimum: 12,
                maximum: 42,
              },
            ],
          },
        ],
      },

      {
        name: "foo",
        type: "function",
        parameters: [
          {
            name: "arg",
            choices: [
              {
                type: "object",
                properties: {
                  blurg: {
                    type: "string",
                    unsupported: true,
                    optional: true,
                  },
                },
                additionalProperties: {
                  type: "string",
                },
              },
              {
                type: "string",
              },
              {
                type: "array",
                minItems: 2,
                maxItems: 3,
                items: {
                  type: "integer",
                },
              },
            ],
          },
        ],
      },

      {
        name: "bar",
        type: "function",
        parameters: [
          {
            name: "arg",
            choices: [
              {
                type: "object",
                properties: {
                  baz: {
                    type: "string",
                  },
                },
              },
              {
                type: "array",
                items: {
                  type: "integer",
                },
              },
            ],
          },
        ],
      },
    ],
  },
];

add_task(async function testChoices() {
  let wrapper = getContextWrapper();
  let url = "data:," + JSON.stringify(choicesJson);
  Schemas._rootSchema = null;
  await Schemas.load(url);

  let root = {};
  Schemas.inject(root, wrapper);

  Assert.throws(
    () => root.choices.meh("frog"),
    /Value "frog" must either: be one of \["foo", "bar", "baz"\], match the pattern \/florg\.\*meh\/, or be an integer value/
  );

  Assert.throws(
    () => root.choices.meh(4),
    /be a string value, or be at least 12/
  );

  Assert.throws(
    () => root.choices.meh(43),
    /be a string value, or be no greater than 42/
  );

  Assert.throws(
    () => root.choices.foo([]),
    /be an object value, be a string value, or have at least 2 items/
  );

  Assert.throws(
    () => root.choices.foo([1, 2, 3, 4]),
    /be an object value, be a string value, or have at most 3 items/
  );

  Assert.throws(
    () => root.choices.foo({ foo: 12 }),
    /.foo must be a string value, be a string value, or be an array value/
  );

  Assert.throws(
    () => root.choices.foo({ blurg: "foo" }),
    /not contain an unsupported "blurg" property, be a string value, or be an array value/
  );

  Assert.throws(
    () => root.choices.bar({}),
    /contain the required "baz" property, or be an array value/
  );

  Assert.throws(
    () => root.choices.bar({ baz: "x", quux: "y" }),
    /not contain an unexpected "quux" property, or be an array value/
  );

  Assert.throws(
    () => root.choices.bar({ baz: "x", quux: "y", foo: "z" }),
    /not contain the unexpected properties \[foo, quux\], or be an array value/
  );
});

let permissionsJson = [
  {
    namespace: "noPerms",

    types: [],

    functions: [
      {
        name: "noPerms",
        type: "function",
        parameters: [],
      },

      {
        name: "fooPerm",
        type: "function",
        permissions: ["foo"],
        parameters: [],
      },
    ],
  },

  {
    namespace: "fooPerm",

    permissions: ["foo"],

    types: [],

    functions: [
      {
        name: "noPerms",
        type: "function",
        parameters: [],
      },

      {
        name: "fooBarPerm",
        type: "function",
        permissions: ["foo.bar"],
        parameters: [],
      },
    ],
  },
];

add_task(async function testPermissions() {
  let url = "data:," + JSON.stringify(permissionsJson);
  Schemas._rootSchema = null;
  await Schemas.load(url);

  let wrapper = getContextWrapper();

  let root = {};
  Schemas.inject(root, wrapper);

  equal(typeof root.noPerms, "object", "noPerms namespace should exist");
  equal(
    typeof root.noPerms.noPerms,
    "function",
    "noPerms.noPerms method should exist"
  );

  equal(
    root.noPerms.fooPerm,
    undefined,
    "noPerms.fooPerm should not method exist"
  );

  equal(root.fooPerm, undefined, "fooPerm namespace should not exist");

  info('Add "foo" permission');
  wrapper.permissions.add("foo");

  root = {};
  Schemas.inject(root, wrapper);

  equal(typeof root.noPerms, "object", "noPerms namespace should exist");
  equal(
    typeof root.noPerms.noPerms,
    "function",
    "noPerms.noPerms method should exist"
  );
  equal(
    typeof root.noPerms.fooPerm,
    "function",
    "noPerms.fooPerm method should exist"
  );

  equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
  equal(
    typeof root.fooPerm.noPerms,
    "function",
    "noPerms.noPerms method should exist"
  );

  equal(
    root.fooPerm.fooBarPerm,
    undefined,
    "fooPerm.fooBarPerm method should not exist"
  );

  info('Add "foo.bar" permission');
  wrapper.permissions.add("foo.bar");

  root = {};
  Schemas.inject(root, wrapper);

  equal(typeof root.noPerms, "object", "noPerms namespace should exist");
  equal(
    typeof root.noPerms.noPerms,
    "function",
    "noPerms.noPerms method should exist"
  );
  equal(
    typeof root.noPerms.fooPerm,
    "function",
    "noPerms.fooPerm method should exist"
  );

  equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
  equal(
    typeof root.fooPerm.noPerms,
    "function",
    "noPerms.noPerms method should exist"
  );
  equal(
    typeof root.fooPerm.fooBarPerm,
    "function",
    "noPerms.fooBarPerm method should exist"
  );
});

let nestedNamespaceJson = [
  {
    namespace: "nested.namespace",
    types: [
      {
        id: "CustomType",
        type: "object",
        events: [
          {
            name: "onEvent",
            type: "function",
          },
        ],
        properties: {
          url: {
            type: "string",
          },
        },
        functions: [
          {
            name: "functionOnCustomType",
            type: "function",
            parameters: [
              {
                name: "title",
                type: "string",
              },
            ],
          },
        ],
      },
    ],
    properties: {
      instanceOfCustomType: {
        $ref: "CustomType",
      },
    },
    functions: [
      {
        name: "create",
        type: "function",
        parameters: [
          {
            name: "title",
            type: "string",
          },
        ],
      },
    ],
  },
];

add_task(async function testNestedNamespace() {
  let url = "data:," + JSON.stringify(nestedNamespaceJson);
  let wrapper = getContextWrapper();

  Schemas._rootSchema = null;
  await Schemas.load(url);

  let root = {};
  Schemas.inject(root, wrapper);

  ok(root.nested, "The root object contains the first namespace level");
  ok(
    root.nested.namespace,
    "The first level object contains the second namespace level"
  );

  ok(
    root.nested.namespace.create,
    "Got the expected function in the nested namespace"
  );
  equal(
    typeof root.nested.namespace.create,
    "function",
    "The property is a function as expected"
  );

  let { instanceOfCustomType } = root.nested.namespace;

  ok(
    instanceOfCustomType,
    "Got the expected instance of the CustomType defined in the schema"
  );
  ok(
    instanceOfCustomType.functionOnCustomType,
    "Got the expected method in the CustomType instance"
  );
  ok(
    instanceOfCustomType.onEvent &&
      instanceOfCustomType.onEvent.addListener &&
      typeof instanceOfCustomType.onEvent.addListener == "function",
    "Got the expected event defined in the CustomType instance"
  );

  instanceOfCustomType.functionOnCustomType("param_value");
  wrapper.verify(
    "call",
    "nested.namespace.instanceOfCustomType",
    "functionOnCustomType",
    ["param_value"]
  );

  let fakeListener = () => {};
  instanceOfCustomType.onEvent.addListener(fakeListener);
  wrapper.verify(
    "addListener",
    "nested.namespace.instanceOfCustomType",
    "onEvent",
    [fakeListener, []]
  );
  instanceOfCustomType.onEvent.removeListener(fakeListener);
  wrapper.verify(
    "removeListener",
    "nested.namespace.instanceOfCustomType",
    "onEvent",
    [fakeListener]
  );

  // TODO: test support properties in a SubModuleType defined in the schema,
  // once implemented, e.g.:
  // ok("url" in instanceOfCustomType,
  //   "Got the expected property defined in the CustomType instance");
});

let $importJson = [
  {
    namespace: "from_the",
    $import: "future",
  },
  {
    namespace: "future",
    properties: {
      PROP1: { value: "original value" },
      PROP2: { value: "second original" },
    },
    types: [
      {
        id: "Colour",
        type: "string",
        enum: ["red", "white", "blue"],
      },
    ],
    functions: [
      {
        name: "dye",
        type: "function",
        parameters: [{ name: "arg", $ref: "Colour" }],
      },
    ],
  },
  {
    namespace: "embrace",
    $import: "future",
    properties: {
      PROP2: { value: "overridden value" },
    },
    types: [
      {
        id: "Colour",
        type: "string",
        enum: ["blue", "orange"],
      },
    ],
  },
];

add_task(async function test_$import() {
  let wrapper = getContextWrapper();
  let url = "data:," + JSON.stringify($importJson);
  Schemas._rootSchema = null;
  await Schemas.load(url);

  let root = {};
  Schemas.inject(root, wrapper);

  equal(root.from_the.PROP1, "original value", "imported property");
  equal(root.from_the.PROP2, "second original", "second imported property");
  equal(root.from_the.Colour.RED, "red", "imported enum type");
  equal(typeof root.from_the.dye, "function", "imported function");

  root.from_the.dye("white");
  wrapper.verify("call", "from_the", "dye", ["white"]);

  Assert.throws(
    () => root.from_the.dye("orange"),
    /Invalid enumeration value/,
    "original imported argument type Colour doesn't include 'orange'"
  );

  equal(root.embrace.PROP1, "original value", "imported property");
  equal(root.embrace.PROP2, "overridden value", "overridden property");
  equal(root.embrace.Colour.ORANGE, "orange", "overridden enum type");
  equal(typeof root.embrace.dye, "function", "imported function");

  root.embrace.dye("orange");
  wrapper.verify("call", "embrace", "dye", ["orange"]);

  Assert.throws(
    () => root.embrace.dye("white"),
    /Invalid enumeration value/,
    "overridden argument type Colour doesn't include 'white'"
  );
});

add_task(async function testLocalAPIImplementation() {
  let countGet2 = 0;
  let countProp3 = 0;
  let countProp3SubFoo = 0;

  let testingApiObj = {
    get PROP1() {
      // PROP1 is a schema-defined constant.
      throw new Error("Unexpected get PROP1");
    },
    get prop2() {
      ++countGet2;
      return "prop2 val";
    },
    get prop3() {
      throw new Error("Unexpected get prop3");
    },
    set prop3(v) {
      // prop3 is a submodule, defined as a function, so the API should not pass
      // through assignment to prop3.
      throw new Error("Unexpected set prop3");
    },
  };
  let submoduleApiObj = {
    get sub_foo() {
      ++countProp3;
      return () => {
        return ++countProp3SubFoo;
      };
    },
  };

  let localWrapper = {
    manifestVersion: 2,
    cloneScope: global,
    shouldInject(ns, name) {
      return name == "testing" || ns == "testing" || ns == "testing.prop3";
    },
    getImplementation(ns, name) {
      Assert.ok(ns == "testing" || ns == "testing.prop3");
      if (ns == "testing.prop3" && name == "sub_foo") {
        // It is fine to use `null` here because we don't call async functions.
        return new LocalAPIImplementation(submoduleApiObj, name, null);
      }
      // It is fine to use `null` here because we don't call async functions.
      return new LocalAPIImplementation(testingApiObj, name, null);
    },
  };

  let root = {};
  Schemas.inject(root, localWrapper);
  Assert.equal(countGet2, 0);
  Assert.equal(countProp3, 0);
  Assert.equal(countProp3SubFoo, 0);

  Assert.equal(root.testing.PROP1, 20);

  Assert.equal(root.testing.prop2, "prop2 val");
  Assert.equal(countGet2, 1);

  Assert.equal(root.testing.prop2, "prop2 val");
  Assert.equal(countGet2, 2);

  info(JSON.stringify(root.testing));
  Assert.equal(root.testing.prop3.sub_foo(), 1);
  Assert.equal(countProp3, 1);
  Assert.equal(countProp3SubFoo, 1);

  Assert.equal(root.testing.prop3.sub_foo(), 2);
  Assert.equal(countProp3, 2);
  Assert.equal(countProp3SubFoo, 2);

  root.testing.prop3.sub_foo = () => {
    return "overwritten";
  };
  Assert.equal(root.testing.prop3.sub_foo(), "overwritten");

  root.testing.prop3 = {
    sub_foo() {
      return "overwritten again";
    },
  };
  Assert.equal(root.testing.prop3.sub_foo(), "overwritten again");
  Assert.equal(countProp3SubFoo, 2);
});

let defaultsJson = [
  {
    namespace: "defaultsJson",

    types: [],

    functions: [
      {
        name: "defaultFoo",
        type: "function",
        parameters: [
          {
            name: "arg",
            type: "object",
            optional: true,
            properties: {
              prop1: { type: "integer", optional: true },
            },
            default: { prop1: 1 },
          },
        ],
        returns: {
          type: "object",
          additionalProperties: true,
        },
      },
    ],
  },
];

add_task(async function testDefaults() {
  let url = "data:," + JSON.stringify(defaultsJson);
  Schemas._rootSchema = null;
  await Schemas.load(url);

  let testingApiObj = {
    defaultFoo: function (arg) {
      if (Object.keys(arg) != "prop1") {
        throw new Error(
          `Received the expected default object, default: ${JSON.stringify(
            arg
          )}`
        );
      }
      arg.newProp = 1;
      return arg;
    },
  };

  let localWrapper = {
    manifestVersion: 2,
    cloneScope: global,
    shouldInject(ns) {
      return true;
    },
    getImplementation(ns, name) {
      return new LocalAPIImplementation(testingApiObj, name, null);
    },
  };

  let root = {};
  Schemas.inject(root, localWrapper);

  deepEqual(root.defaultsJson.defaultFoo(), { prop1: 1, newProp: 1 });
  deepEqual(root.defaultsJson.defaultFoo({ prop1: 2 }), {
    prop1: 2,
    newProp: 1,
  });
  deepEqual(root.defaultsJson.defaultFoo(), { prop1: 1, newProp: 1 });
});

let returnsJson = [
  {
    namespace: "returns",
    types: [
      {
        id: "Widget",
        type: "object",
        properties: {
          size: { type: "integer" },
          colour: { type: "string", optional: true },
        },
      },
    ],
    functions: [
      {
        name: "complete",
        type: "function",
        returns: { $ref: "Widget" },
        parameters: [],
      },
      {
        name: "optional",
        type: "function",
        returns: { $ref: "Widget" },
        parameters: [],
      },
      {
        name: "invalid",
        type: "function",
        returns: { $ref: "Widget" },
        parameters: [],
      },
    ],
  },
];

add_task(async function testReturns() {
  const url = "data:," + JSON.stringify(returnsJson);
  Schemas._rootSchema = null;
  await Schemas.load(url);

  const apiObject = {
    complete() {
      return { size: 3, colour: "orange" };
    },
    optional() {
      return { size: 4 };
    },
    invalid() {
      return {};
    },
  };

  const localWrapper = {
    manifestVersion: 2,
    cloneScope: global,
    shouldInject(ns) {
      return true;
    },
    getImplementation(ns, name) {
      return new LocalAPIImplementation(apiObject, name, null);
    },
  };

  const root = {};
  Schemas.inject(root, localWrapper);

  deepEqual(root.returns.complete(), { size: 3, colour: "orange" });
  deepEqual(
    root.returns.optional(),
    { size: 4 },
    "Missing optional properties is allowed"
  );

  if (AppConstants.DEBUG) {
    Assert.throws(
      () => root.returns.invalid(),
      /Type error for result value \(Property "size" is required\)/,
      "Should throw for invalid result in DEBUG builds"
    );
  } else {
    deepEqual(
      root.returns.invalid(),
      {},
      "Doesn't throw for invalid result value in release builds"
    );
  }
});

let booleanEnumJson = [
  {
    namespace: "booleanEnum",

    types: [
      {
        id: "enumTrue",
        type: "boolean",
        enum: [true],
      },
    ],
    functions: [
      {
        name: "paramMustBeTrue",
        type: "function",
        parameters: [{ name: "arg", $ref: "enumTrue" }],
      },
    ],
  },
];

add_task(async function testBooleanEnum() {
  let wrapper = getContextWrapper();

  let url = "data:," + JSON.stringify(booleanEnumJson);
  Schemas._rootSchema = null;
  await Schemas.load(url);

  let root = {};
  Schemas.inject(root, wrapper);

  ok(root.booleanEnum, "namespace exists");
  root.booleanEnum.paramMustBeTrue(true);
  wrapper.verify("call", "booleanEnum", "paramMustBeTrue", [true]);
  Assert.throws(
    () => root.booleanEnum.paramMustBeTrue(false),
    /Type error for parameter arg \(Invalid value false\) for booleanEnum\.paramMustBeTrue\./,
    "should throw because enum of the type restricts parameter to true"
  );
});

let xoriginJson = [
  {
    namespace: "xorigin",
    types: [],
    functions: [
      {
        name: "foo",
        type: "function",
        parameters: [
          {
            name: "arg",
            type: "any",
          },
        ],
      },
      {
        name: "crossFoo",
        type: "function",
        allowCrossOriginArguments: true,
        parameters: [
          {
            name: "arg",
            type: "any",
          },
        ],
      },
    ],
  },
];

add_task(async function testCrossOriginArguments() {
  let url = "data:," + JSON.stringify(xoriginJson);
  Schemas._rootSchema = null;
  await Schemas.load(url);

  let sandbox = new Cu.Sandbox("http://test.com");

  let testingApiObj = {
    foo(arg) {
      sandbox.result = JSON.stringify(arg);
    },
    crossFoo(arg) {
      sandbox.xResult = JSON.stringify(arg);
    },
  };

  let localWrapper = {
    manifestVersion: 2,
    cloneScope: sandbox,
    shouldInject(ns) {
      return true;
    },
    getImplementation(ns, name) {
      return new LocalAPIImplementation(testingApiObj, name, null);
    },
  };

  let root = {};
  Schemas.inject(root, localWrapper);

  Assert.throws(
    () => root.xorigin.foo({ key: 13 }),
    /Permission denied to pass object/
  );
  equal(sandbox.result, undefined, "Foo can't read cross origin object.");

  root.xorigin.crossFoo({ answer: 42 });
  equal(sandbox.xResult, '{"answer":42}', "Can read cross origin object.");
});
