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

"use strict";

// Test that onEnumProperties returns the expected data
// when passing `ignoreNonIndexedProperties` and `ignoreIndexedProperties` options
// with various objects. (See Bug 1403065)

const DO_NOT_CHECK_VALUE = Symbol();

Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
registerCleanupFunction(() => {
  Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
});

add_task(
  threadFrontTest(async ({ threadFront, debuggee, client }) => {
    debuggee.eval(
      function stopMe(arg1) {
        debugger;
      }.toString()
    );

    const testCases = [
      {
        evaledObject: { a: 10 },
        expectedIndexedProperties: [],
        expectedNonIndexedProperties: [["a", 10]],
      },
      {
        evaledObject: { length: 10 },
        expectedIndexedProperties: [],
        expectedNonIndexedProperties: [["length", 10]],
      },
      {
        evaledObject: { a: 10, 0: "indexed" },
        expectedIndexedProperties: [["0", "indexed"]],
        expectedNonIndexedProperties: [["a", 10]],
      },
      {
        evaledObject: { 1: 1, length: 42, a: 10 },
        expectedIndexedProperties: [["1", 1]],
        expectedNonIndexedProperties: [
          ["length", 42],
          ["a", 10],
        ],
      },
      {
        evaledObject: { 1: 1, length: 2.34, a: 10 },
        expectedIndexedProperties: [["1", 1]],
        expectedNonIndexedProperties: [
          ["length", 2.34],
          ["a", 10],
        ],
      },
      {
        evaledObject: { 1: 1, length: -0, a: 10 },
        expectedIndexedProperties: [["1", 1]],
        expectedNonIndexedProperties: [
          ["length", -0],
          ["a", 10],
        ],
      },
      {
        evaledObject: { 1: 1, length: -10, a: 10 },
        expectedIndexedProperties: [["1", 1]],
        expectedNonIndexedProperties: [
          ["length", -10],
          ["a", 10],
        ],
      },
      {
        evaledObject: { 1: 1, length: true, a: 10 },
        expectedIndexedProperties: [["1", 1]],
        expectedNonIndexedProperties: [
          ["length", true],
          ["a", 10],
        ],
      },
      {
        evaledObject: { 1: 1, length: null, a: 10 },
        expectedIndexedProperties: [["1", 1]],
        expectedNonIndexedProperties: [
          ["length", DO_NOT_CHECK_VALUE],
          ["a", 10],
        ],
      },
      {
        evaledObject: { 1: 1, length: Math.pow(2, 53), a: 10 },
        expectedIndexedProperties: [["1", 1]],
        expectedNonIndexedProperties: [
          ["length", 9007199254740992],
          ["a", 10],
        ],
      },
      {
        evaledObject: { 1: 1, length: "fake", a: 10 },
        expectedIndexedProperties: [["1", 1]],
        expectedNonIndexedProperties: [
          ["length", "fake"],
          ["a", 10],
        ],
      },
      {
        evaledObject: { 1: 1, length: Infinity, a: 10 },
        expectedIndexedProperties: [["1", 1]],
        expectedNonIndexedProperties: [
          ["length", DO_NOT_CHECK_VALUE],
          ["a", 10],
        ],
      },
      {
        evaledObject: { 0: 0, length: 0 },
        expectedIndexedProperties: [["0", 0]],
        expectedNonIndexedProperties: [["length", 0]],
      },
      {
        evaledObject: { 0: 0, 1: 1, length: 1 },
        expectedIndexedProperties: [
          ["0", 0],
          ["1", 1],
        ],
        expectedNonIndexedProperties: [["length", 1]],
      },
      {
        evaledObject: { length: 0 },
        expectedIndexedProperties: [],
        expectedNonIndexedProperties: [["length", 0]],
      },
      {
        evaledObject: { 1: 1 },
        expectedIndexedProperties: [["1", 1]],
        expectedNonIndexedProperties: [],
      },
      {
        evaledObject: { a: 1, [2 ** 32 - 2]: 2, [2 ** 32 - 1]: 3 },
        expectedIndexedProperties: [["4294967294", 2]],
        expectedNonIndexedProperties: [
          ["a", 1],
          ["4294967295", 3],
        ],
      },
      {
        evaledObject: `(() => {
          x = [12, 42];
          x.foo = 90;
          return x;
        })()`,
        expectedIndexedProperties: [
          ["0", 12],
          ["1", 42],
        ],
        expectedNonIndexedProperties: [
          ["length", 2],
          ["foo", 90],
        ],
      },
      {
        evaledObject: `(() => {
          x = [12, 42];
          x.length = 3;
          return x;
        })()`,
        expectedIndexedProperties: [
          ["0", 12],
          ["1", 42],
          ["2", undefined],
        ],
        expectedNonIndexedProperties: [["length", 3]],
      },
      {
        evaledObject: `(() => {
          x = [12, 42];
          x.length = 1;
          return x;
        })()`,
        expectedIndexedProperties: [["0", 12]],
        expectedNonIndexedProperties: [["length", 1]],
      },
      {
        evaledObject: `(() => {
          x = [, 42,,];
          x.foo = 90;
          return x;
        })()`,
        expectedIndexedProperties: [
          ["0", undefined],
          ["1", 42],
          ["2", undefined],
        ],
        expectedNonIndexedProperties: [
          ["length", 3],
          ["foo", 90],
        ],
      },
      {
        evaledObject: `(() => {
          x = Array(2);
          x.foo = "bar";
          x.bar = "foo";
          return x;
        })()`,
        expectedIndexedProperties: [
          ["0", undefined],
          ["1", undefined],
        ],
        expectedNonIndexedProperties: [
          ["length", 2],
          ["foo", "bar"],
          ["bar", "foo"],
        ],
      },
      {
        evaledObject: `(() => {
          x = new Int8Array(new ArrayBuffer(2));
          x.foo = "bar";
          x.bar = "foo";
          return x;
        })()`,
        expectedIndexedProperties: [
          ["0", 0],
          ["1", 0],
        ],
        expectedNonIndexedProperties: [
          ["foo", "bar"],
          ["bar", "foo"],
          ["length", 2],
          ["buffer", DO_NOT_CHECK_VALUE],
          ["byteLength", 2],
          ["byteOffset", 0],
        ],
      },
      {
        evaledObject: `(() => {
          x = new Int8Array([1, 2]);
          Object.defineProperty(x, 'length', {value: 0});
          return x;
        })()`,
        expectedIndexedProperties: [
          ["0", 1],
          ["1", 2],
        ],
        expectedNonIndexedProperties: [
          ["length", 0],
          ["buffer", DO_NOT_CHECK_VALUE],
          ["byteLength", 2],
          ["byteOffset", 0],
        ],
      },
      {
        evaledObject: `(() => {
          x = new Int32Array([1, 2]);
          Object.setPrototypeOf(x, null);
          return x;
        })()`,
        expectedIndexedProperties: [
          ["0", 1],
          ["1", 2],
        ],
        expectedNonIndexedProperties: [],
      },
      {
        evaledObject: `(() => {
          return new (class extends Int8Array {})([1, 2]);
        })()`,
        expectedIndexedProperties: [
          ["0", 1],
          ["1", 2],
        ],
        expectedNonIndexedProperties: [
          ["length", 2],
          ["buffer", DO_NOT_CHECK_VALUE],
          ["byteLength", 2],
          ["byteOffset", 0],
        ],
      },
    ];

    for (const test of testCases) {
      await test_object_grip(debuggee, client, threadFront, test);
    }
  })
);

async function test_object_grip(
  debuggee,
  dbgClient,
  threadFront,
  testData = {}
) {
  const {
    evaledObject,
    expectedIndexedProperties,
    expectedNonIndexedProperties,
  } = testData;

  const packet = await executeOnNextTickAndWaitForPause(eval_code, threadFront);

  const [grip] = packet.frame.arguments;

  const objClient = threadFront.pauseGrip(grip);

  info(`
        Check enumProperties response for
        ${
          typeof evaledObject === "string"
            ? evaledObject
            : JSON.stringify(evaledObject)
        }
      `);

  // Checks the result of enumProperties.
  let response = await objClient.enumProperties({
    ignoreNonIndexedProperties: true,
  });
  await check_enum_properties(response, expectedIndexedProperties);

  response = await objClient.enumProperties({
    ignoreIndexedProperties: true,
  });
  await check_enum_properties(response, expectedNonIndexedProperties);

  await threadFront.resume();

  function eval_code() {
    // Be sure to run debuggee code in its own HTML 'task', so that when we call
    // the onDebuggerStatement hook, the test's own microtasks don't get suspended
    // along with the debuggee's.
    do_timeout(0, () => {
      debuggee.eval(`
        stopMe(${
          typeof evaledObject === "string"
            ? evaledObject
            : JSON.stringify(evaledObject)
        });
      `);
    });
  }
}

async function check_enum_properties(iterator, expected = []) {
  equal(
    iterator.count,
    expected.length,
    "iterator.count has the expected value"
  );

  info("Check iterator.slice response for all properties");
  const sliceResponse = await iterator.slice(0, iterator.count);
  ok(
    sliceResponse &&
      Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"),
    "The response object has an ownProperties property"
  );

  const { ownProperties } = sliceResponse;
  const names = Object.getOwnPropertyNames(ownProperties);
  equal(
    names.length,
    expected.length,
    "The response has the expected number of properties"
  );
  for (let i = 0; i < names.length; i++) {
    const name = names[i];
    const [key, value] = expected[i];
    equal(name, key, "Property has the expected name");
    const property = ownProperties[name];

    if (value === DO_NOT_CHECK_VALUE) {
      return;
    }

    if (value === undefined) {
      equal(
        property,
        undefined,
        `Response has no value for the "${key}" property`
      );
    } else {
      const propValue = property.hasOwnProperty("value")
        ? property.value
        : property.getterValue;
      equal(propValue, value, `Property "${key}" has the expected value`);
    }
  }
}
