"use strict";

let json = [
  {
    namespace: "MV2",
    max_manifest_version: 2,

    properties: {
      PROP1: { value: 20 },
    },
  },
  {
    namespace: "MV3",
    min_manifest_version: 3,
    properties: {
      PROP1: { value: 20 },
    },
  },
  {
    namespace: "mixed",

    properties: {
      PROP_any: { value: 20 },
      PROP_mv3: {
        $ref: "submodule",
      },
    },
    types: [
      {
        id: "manifestTest",
        type: "object",
        properties: {
          // An example of extending the base type for permissions
          permissions: {
            type: "array",
            items: {
              $ref: "BaseType",
            },
            optional: true,
            default: [],
          },
          // An example of differentiating versions of a manifest entry
          multiple_choice: {
            optional: true,
            choices: [
              {
                max_manifest_version: 2,
                type: "array",
                items: {
                  type: "string",
                },
              },
              {
                min_manifest_version: 3,
                type: "array",
                items: {
                  type: "boolean",
                },
              },
              {
                type: "array",
                items: {
                  type: "object",
                  properties: {
                    value: { type: "boolean" },
                  },
                },
              },
            ],
          },
          accepting_unrecognized_props: {
            optional: true,
            type: "object",
            properties: {
              mv2_only_prop: {
                type: "string",
                optional: true,
                max_manifest_version: 2,
              },
              mv3_only_prop: {
                type: "string",
                optional: true,
                min_manifest_version: 3,
              },
              mv2_only_prop_with_default: {
                type: "string",
                optional: true,
                default: "only in MV2",
                max_manifest_version: 2,
              },
              mv3_only_prop_with_default: {
                type: "string",
                optional: true,
                default: "only in MV3",
                min_manifest_version: 3,
              },
            },
            additionalProperties: { $ref: "UnrecognizedProperty" },
          },
        },
      },
      {
        id: "submodule",
        type: "object",
        min_manifest_version: 3,
        functions: [
          {
            name: "sub_foo",
            type: "function",
            parameters: [],
            returns: { type: "integer" },
          },
          {
            name: "sub_no_match",
            type: "function",
            max_manifest_version: 2,
            parameters: [],
            returns: { type: "integer" },
          },
        ],
      },
      {
        id: "BaseType",
        choices: [
          {
            type: "string",
            enum: ["base"],
          },
        ],
      },
      {
        id: "type_any",
        type: "string",
        enum: ["value1", "value2", "value3"],
      },
      {
        id: "type_mv2",
        max_manifest_version: 2,
        type: "string",
        enum: ["value1", "value2", "value3"],
      },
      {
        id: "type_mv3",
        min_manifest_version: 3,
        type: "string",
        enum: ["value1", "value2", "value3"],
      },
      {
        id: "param_type_changed",
        type: "array",
        items: {
          choices: [
            { max_manifest_version: 2, type: "string" },
            {
              min_manifest_version: 3,
              type: "boolean",
            },
          ],
        },
      },
      {
        id: "object_type_changed",
        type: "object",
        properties: {
          prop_mv2: {
            type: "string",
            max_manifest_version: 2,
          },
          prop_mv3: {
            type: "string",
            min_manifest_version: 3,
          },
          prop_any: {
            type: "string",
          },
        },
      },
      {
        id: "no_valid_choices",
        type: "array",
        items: {
          choices: [
            { max_manifest_version: 1, type: "string" },
            {
              min_manifest_version: 4,
              type: "boolean",
            },
          ],
        },
      },
    ],

    functions: [
      {
        name: "fun_param_type_versioned",
        type: "function",
        parameters: [{ name: "arg1", $ref: "param_type_changed" }],
      },
      {
        name: "fun_mv2",
        max_manifest_version: 2,
        type: "function",
        parameters: [
          { name: "arg1", type: "integer", optional: true, default: 99 },
          { name: "arg2", type: "boolean", optional: true },
        ],
      },
      {
        name: "fun_mv3",
        min_manifest_version: 3,
        type: "function",
        parameters: [
          { name: "arg1", type: "integer", optional: true, default: 99 },
          { name: "arg2", type: "boolean", optional: true },
        ],
      },
      {
        name: "fun_param_change",
        type: "function",
        parameters: [{ name: "arg1", $ref: "object_type_changed" }],
      },
      {
        name: "fun_no_valid_param",
        type: "function",
        parameters: [{ name: "arg1", $ref: "no_valid_choices" }],
      },
      {
        name: "fun_mixed_with_fallback",
        type: "function",
        max_manifest_version: 2,
        parameters: [
          {
            name: "arg1",
            type: "integer",
          },
          {
            name: "arg2_mv2",
            type: "boolean",
          },
          {
            name: "arg3",
            type: "string",
            optional: true,
            default: "hello",
          },
        ],
      },
      {
        name: "fun_mixed_with_fallback",
        type: "function",
        min_manifest_version: 3,
        parameters: [
          {
            name: "arg1",
            type: "integer",
          },
          {
            name: "arg2_mv3",
            type: "integer",
            optional: true,
            default: 5,
          },
          {
            name: "arg3",
            type: "string",
            optional: true,
            default: "hello",
          },
        ],
      },
    ],
    events: [
      {
        name: "onEvent_any",
        type: "function",
      },
      {
        name: "onEvent_mv2",
        max_manifest_version: 2,
        type: "function",
      },
      {
        name: "onEvent_mv3",
        min_manifest_version: 3,
        type: "function",
      },
      {
        name: "onEvent_mixed_with_fallback",
        type: "function",
        max_manifest_version: 2,
        parameters: [],
        extraParameters: [
          {
            name: "extra_arg1_mv2",
            type: "string",
          },
        ],
      },
      {
        name: "onEvent_mixed_with_fallback",
        type: "function",
        min_manifest_version: 3,
        parameters: [],
        extraParameters: [
          {
            name: "extra_arg1_mv3",
            type: "boolean",
          },
        ],
      },
    ],
  },
  {
    namespace: "multiple_fallbacks",
    functions: [
      {
        name: "fun_with_multiple_fallbacks",
        type: "function",
        max_manifest_version: 3,
        parameters: [
          {
            name: "arg1",
            type: "integer",
          },
          {
            name: "arg2_mv2",
            type: "boolean",
          },
          {
            name: "arg3",
            type: "string",
            optional: true,
            default: "hello",
          },
        ],
      },
      {
        name: "fun_with_multiple_fallbacks",
        type: "function",
        min_manifest_version: 3,
        parameters: [
          {
            name: "arg1",
            type: "integer",
          },
          {
            name: "arg2_mv3",
            type: "integer",
            optional: true,
            default: 5,
          },
          {
            name: "arg3",
            type: "string",
            optional: true,
            default: "hello",
          },
        ],
      },
    ],
  },
  {
    namespace: "mixed",
    types: [
      {
        $extend: "BaseType",
        choices: [
          {
            min_manifest_version: 3,
            type: "string",
            enum: ["extended"],
          },
        ],
      },
    ],
  },
  {
    namespace: "mixed",
    types: [
      {
        $extend: "manifestTest",
        properties: {
          versioned_extend: {
            optional: true,
            // just a simple type here
            type: "string",
            max_manifest_version: 2,
          },
        },
      },
    ],
  },
];

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

  // We want the actual errors thrown here, and not warnings recast as errors.
  ExtensionTestUtils.failOnSchemaWarnings(false);
});

add_task(async function test_inject_V2() {
  // Test injecting into a V2 context.
  let wrapper = getContextWrapper(2);

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

  // Test elements available to both
  Assert.equal(root.mixed.type_any.VALUE1, "value1", "type_any exists");
  Assert.equal(root.mixed.PROP_any, 20, "mixed value property");

  // Test elements available to MV2
  Assert.equal(root.MV2.PROP1, 20, "MV2 value property");
  Assert.equal(root.mixed.type_mv2.VALUE2, "value2", "type_mv2 exists");

  // Test MV3 elements not available
  Assert.equal(root.MV3, undefined, "MV3 not injected");
  Assert.ok(!("MV3" in root), "MV3 not enumerable");
  Assert.equal(
    root.mixed.PROP_mv3,
    undefined,
    "mixed submodule property does not exist"
  );
  Assert.ok(
    !("PROP_mv3" in root.mixed),
    "mixed submodule property not enumerable"
  );
  Assert.equal(root.mixed.type_mv3, undefined, "type_mv3 does not exist");

  // Function tests
  Assert.ok(
    "fun_param_type_versioned" in root.mixed,
    "fun_param_type_versioned exists"
  );
  Assert.ok(
    !!root.mixed.fun_param_type_versioned,
    "fun_param_type_versioned exists"
  );
  Assert.ok("fun_mv2" in root.mixed, "fun_mv2 exists");
  Assert.ok(!!root.mixed.fun_mv2, "fun_mv2 exists");
  Assert.ok(!("fun_mv3" in root.mixed), "fun_mv3 does not exist");
  Assert.ok(!root.mixed.fun_mv3, "fun_mv3 does not exist");

  // Event tests
  Assert.ok("onEvent_any" in root.mixed, "onEvent_any exists");
  Assert.ok(!!root.mixed.onEvent_any, "onEvent_any exists");
  Assert.ok("onEvent_mv2" in root.mixed, "onEvent_mv2 exists");
  Assert.ok(!!root.mixed.onEvent_mv2, "onEvent_mv2 exists");
  Assert.ok(!("onEvent_mv3" in root.mixed), "onEvent_mv3 does not exist");
  Assert.ok(!root.mixed.onEvent_mv3, "onEvent_mv3 does not exist");

  // Function call tests
  root.mixed.fun_param_type_versioned(["hello"]);
  wrapper.verify("call", "mixed", "fun_param_type_versioned", [["hello"]]);
  Assert.throws(
    () => root.mixed.fun_param_type_versioned([true]),
    /Expected string instead of true/,
    "fun_param_type_versioned should throw for invalid type"
  );

  let propObj = { prop_any: "prop_any", prop_mv2: "prop_mv2" };
  root.mixed.fun_param_change(propObj);
  wrapper.verify("call", "mixed", "fun_param_change", [propObj]);

  // Still throw same error as we did before we knew of the MV3 property.
  Assert.throws(
    () => root.mixed.fun_param_change({ prop_mv3: "prop_mv3", ...propObj }),
    /Type error for parameter arg1 \(Unexpected property "prop_mv3"\)/,
    "generic unexpected property message for MV3 property in MV2 extension"
  );

  // But print the more specific and descriptive warning message to console.
  wrapper.checkErrors([
    `Property "prop_mv3" is unsupported in Manifest Version 2`,
  ]);

  Assert.throws(
    () => root.mixed.fun_no_valid_param("anything"),
    /Incorrect argument types for mixed.fun_no_valid_param/,
    "fun_no_valid_param should throw for versioned type"
  );

  // Multiple function definitions with fallback. Provide all 3 MV2 parameters
  root.mixed.fun_mixed_with_fallback(1, false, "good bye");
  wrapper.verify("call", "mixed", "fun_mixed_with_fallback", [
    1,
    false,
    "good bye",
  ]);
  // Leave out the optional 3rd parameter.
  root.mixed.fun_mixed_with_fallback(2, true);
  wrapper.verify("call", "mixed", "fun_mixed_with_fallback", [
    2,
    true,
    "hello",
  ]);
  // In MV2 the second argument is not optional, should throw if left out.
  Assert.throws(
    () => root.mixed.fun_mixed_with_fallback(2, "good bye"),
    /Error: Incorrect argument types for mixed.fun_mixed_with_fallback/,
    "fun_mixed_with_fallback should throw"
  );

  // Multiple event definitions with fallback.
  root.mixed.onEvent_mixed_with_fallback.addListener(() => {}, "hello");
  // Use wrong types and check that it throws.
  Assert.throws(
    () => root.mixed.onEvent_mixed_with_fallback.addListener(() => {}, true),
    /Error: Incorrect argument types for mixed.onEvent_mixed_with_fallback/,
    "onEvent_mixed_with_fallback should throw"
  );
});

function normalizeTest(manifest, test, wrapper) {
  let normalized = Schemas.normalize(manifest, "mixed.manifestTest", wrapper);
  test(normalized);
  // The test function should call wrapper.checkErrors if it expected errors.
  // Here we call checkErrors again to ensure that there are not any unexpected
  // errors left.
  wrapper.checkErrors([]);
}

add_task(async function test_normalize_V2() {
  let wrapper = getContextWrapper(2);

  // Test normalize additions to the manifest structure
  normalizeTest(
    {
      versioned_extend: "test",
    },
    normalized => {
      Assert.equal(
        normalized.value.versioned_extend,
        "test",
        "resources normalized"
      );
    },
    wrapper
  );

  // Test normalizing baseType
  normalizeTest(
    {
      permissions: ["base"],
    },
    normalized => {
      Assert.equal(
        normalized.value.permissions[0],
        "base",
        "resources normalized"
      );
    },
    wrapper
  );

  normalizeTest(
    {
      permissions: ["extended"],
    },
    normalized => {
      Assert.ok(
        normalized.error.startsWith("Error processing permissions.0"),
        `manifest normalized error ${normalized.error}`
      );
    },
    wrapper
  );

  // Test normalizing a value
  normalizeTest(
    {
      multiple_choice: ["foo.html"],
    },
    normalized => {
      Assert.equal(
        normalized.value.multiple_choice[0],
        "foo.html",
        "resources normalized"
      );
    },
    wrapper
  );

  normalizeTest(
    {
      multiple_choice: [true],
    },
    normalized => {
      Assert.ok(
        normalized.error.startsWith("Error processing multiple_choice"),
        "manifest error"
      );
    },
    wrapper
  );

  normalizeTest(
    {
      multiple_choice: [
        {
          value: true,
        },
      ],
    },
    normalized => {
      Assert.ok(
        normalized.value.multiple_choice[0].value,
        "resources normalized"
      );
    },
    wrapper
  );

  // Tests that object definitions including additionalProperties can
  // successfully accept objects from another manifest version, while ignoring
  // the actual value from the non-matching manifest value.
  normalizeTest(
    {
      accepting_unrecognized_props: {
        mv2_only_prop: "mv2 here",
        mv3_only_prop: "mv3 here",
      },
    },
    normalized => {
      equal(normalized.error, undefined, "no normalization error");
      Assert.deepEqual(
        normalized.value.accepting_unrecognized_props,
        {
          mv2_only_prop: "mv2 here",
          mv2_only_prop_with_default: "only in MV2",
        },
        "Normalized object for MV2, without MV3-specific props"
      );
      wrapper.checkErrors([
        `Property "mv3_only_prop" is unsupported in Manifest Version 2`,
      ]);
    },
    wrapper
  );
});

add_task(async function test_inject_V3() {
  // Test injecting into a V3 context.
  let wrapper = getContextWrapper(3);

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

  // Test elements available to both
  Assert.equal(root.mixed.type_any.VALUE1, "value1", "type_any exists");
  Assert.equal(root.mixed.PROP_any, 20, "mixed value property");

  // Test elements available to MV2
  Assert.equal(root.MV2, undefined, "MV2 value property");
  Assert.ok(!("MV2" in root), "MV2 not enumerable");
  Assert.equal(root.mixed.type_mv2, undefined, "type_mv2 does not exist");
  Assert.ok(!("type_mv2" in root.mixed), "type_mv2 not enumerable");

  // Test MV3 elements not available
  Assert.equal(root.MV3.PROP1, 20, "MV3 injected");
  Assert.ok(!!root.mixed.PROP_mv3, "mixed submodule property exists");
  Assert.equal(root.mixed.type_mv3.VALUE3, "value3", "type_mv3 exists");

  // Versioned submodule
  Assert.ok(!!root.mixed.PROP_mv3.sub_foo, "mixed submodule sub_foo exists");
  Assert.ok(
    !root.mixed.PROP_mv3.sub_no_match,
    "mixed submodule sub_no_match does not exist"
  );
  Assert.ok(
    !("sub_no_match" in root.mixed.PROP_mv3),
    "mixed submodule sub_no_match is not enumerable"
  );

  // Function tests
  Assert.ok(
    "fun_param_type_versioned" in root.mixed,
    "fun_param_type_versioned exists"
  );
  Assert.ok(
    !!root.mixed.fun_param_type_versioned,
    "fun_param_type_versioned exists"
  );
  Assert.ok(!("fun_mv2" in root.mixed), "fun_mv2 does not exist");
  Assert.ok(!root.mixed.fun_mv2, "fun_mv2 does not exist");
  Assert.ok("fun_mv3" in root.mixed, "fun_mv3 exists");
  Assert.ok(!!root.mixed.fun_mv3, "fun_mv3 exists");

  // Event tests
  Assert.ok("onEvent_any" in root.mixed, "onEvent_any exists");
  Assert.ok(!!root.mixed.onEvent_any, "onEvent_any exists");
  Assert.ok(!("onEvent_mv2" in root.mixed), "onEvent_mv2 not enumerable");
  Assert.ok(!root.mixed.onEvent_mv2, "onEvent_mv2 does not exist");
  Assert.ok("onEvent_mv3" in root.mixed, "onEvent_mv3 exists");
  Assert.ok(!!root.mixed.onEvent_mv3, "onEvent_mv3 exists");

  // Function call tests
  root.mixed.fun_param_type_versioned([true]);
  wrapper.verify("call", "mixed", "fun_param_type_versioned", [[true]]);
  Assert.throws(
    () => root.mixed.fun_param_type_versioned(["hello"]),
    /Expected boolean instead of "hello"/,
    "should throw for invalid type"
  );

  let propObj = { prop_any: "prop_any", prop_mv3: "prop_mv3" };
  root.mixed.fun_param_change(propObj);
  wrapper.verify("call", "mixed", "fun_param_change", [propObj]);
  Assert.throws(
    () => root.mixed.fun_param_change({ prop_mv2: "prop_mv2", ...propObj }),
    /Unexpected property "prop_mv2"/,
    "should throw for versioned type"
  );
  wrapper.checkErrors([
    `Property "prop_mv2" is unsupported in Manifest Version 3`,
  ]);

  root.mixed.PROP_mv3.sub_foo();
  wrapper.verify("call", "mixed.PROP_mv3", "sub_foo", []);
  Assert.throws(
    () => root.mixed.PROP_mv3.sub_no_match(),
    /TypeError: root.mixed.PROP_mv3.sub_no_match is not a function/,
    "sub_no_match should throw"
  );

  // Multiple function definitions with fallback. Provide all 3 MV3 parameters.
  root.mixed.fun_mixed_with_fallback(1, 3, "good bye");
  wrapper.verify("call", "mixed", "fun_mixed_with_fallback", [
    1,
    3,
    "good bye",
  ]);
  // Leave out the 3rd parameter.
  root.mixed.fun_mixed_with_fallback(2, 3);
  wrapper.verify("call", "mixed", "fun_mixed_with_fallback", [2, 3, "hello"]);
  // Leave out the 2nd parameter, which is optional only in MV3.
  root.mixed.fun_mixed_with_fallback(2, "good bye");
  wrapper.verify("call", "mixed", "fun_mixed_with_fallback", [
    2,
    5,
    "good bye",
  ]);
  // Leave out 2nd and 3rd parameter.
  root.mixed.fun_mixed_with_fallback(3);
  wrapper.verify("call", "mixed", "fun_mixed_with_fallback", [3, 5, "hello"]);

  // Multiple event definitions with fallback.
  root.mixed.onEvent_mixed_with_fallback.addListener(() => {}, true);
  // Use wrong types and check that it throws.
  Assert.throws(
    () => root.mixed.onEvent_mixed_with_fallback.addListener(() => {}, "hello"),
    /Error: Incorrect argument types for mixed.onEvent_mixed_with_fallback/,
    "onEvent_mixed_with_fallback should throw"
  );

  // A namespace with multiple fallbacks should fail to inject in DEBUG builds.
  if (AppConstants.DEBUG) {
    Assert.throws(
      () => root.multiple_fallbacks.fun_with_multiple_fallbacks(3),
      /Namespace multiple_fallbacks has multiple definitions for fun_with_multiple_fallbacks for manifest version 3/,
      "fun_with_multiple_fallbacks should throw"
    );
  }
});

add_task(async function test_normalize_V3() {
  let wrapper = getContextWrapper(3);

  // Test normalize additions to the manifest structure
  normalizeTest(
    {
      versioned_extend: "test",
    },
    normalized => {
      Assert.equal(
        normalized.error,
        `Unexpected property "versioned_extend"`,
        "expected manifest error"
      );
      wrapper.checkErrors([
        `Property "versioned_extend" is unsupported in Manifest Version 3`,
      ]);
    },
    wrapper
  );

  // Test normalizing baseType
  normalizeTest(
    {
      permissions: ["base"],
    },
    normalized => {
      Assert.equal(
        normalized.value.permissions[0],
        "base",
        "resources normalized"
      );
    },
    wrapper
  );

  normalizeTest(
    {
      permissions: ["extended"],
    },
    normalized => {
      Assert.equal(
        normalized.value.permissions[0],
        "extended",
        "resources normalized"
      );
    },
    wrapper
  );

  // Test normalizing a value
  normalizeTest(
    {
      multiple_choice: ["foo.html"],
    },
    normalized => {
      Assert.ok(
        normalized.error.startsWith("Error processing multiple_choice"),
        "manifest error"
      );
    },
    wrapper
  );

  normalizeTest(
    {
      multiple_choice: [true],
    },
    normalized => {
      Assert.equal(
        normalized.value.multiple_choice[0],
        true,
        "resources normalized"
      );
    },
    wrapper
  );

  normalizeTest(
    {
      multiple_choice: [
        {
          value: true,
        },
      ],
    },
    normalized => {
      Assert.ok(
        normalized.value.multiple_choice[0].value,
        "resources normalized"
      );
    },
    wrapper
  );

  wrapper.tallied = null;

  normalizeTest(
    {},
    normalized => {
      ok(!normalized.error, "manifest normalized");
    },
    wrapper
  );

  // Tests that object definitions including additionalProperties can
  // successfully accept objects from another manifest version, while ignoring
  // the actual value from the non-matching manifest value.
  normalizeTest(
    {
      accepting_unrecognized_props: {
        mv2_only_prop: "mv2 here",
        mv3_only_prop: "mv3 here",
      },
    },
    normalized => {
      equal(normalized.error, undefined, "no normalization error");
      Assert.deepEqual(
        normalized.value.accepting_unrecognized_props,
        {
          mv3_only_prop: "mv3 here",
          mv3_only_prop_with_default: "only in MV3",
        },
        "Normalized object for MV3, without MV2-specific props"
      );
      wrapper.checkErrors([
        `Property "mv2_only_prop" is unsupported in Manifest Version 3`,
      ]);
    },
    wrapper
  );
});
