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

"use strict";
const {
  FallibleJSPropertyProvider: JSPropertyProvider,
} = require("resource://devtools/shared/webconsole/js-property-provider.js");

const { addDebuggerToGlobal } = ChromeUtils.importESModule(
  "resource://gre/modules/jsdebugger.sys.mjs"
);
addDebuggerToGlobal(globalThis);

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

  const testArray = `var testArray = [
    {propA: "A"},
    {
      propB: "B",
      propC: [
        "D"
      ]
    },
    [
      {propE: "E"}
    ]
  ]`;

  const testObject = 'var testObject = {"propA": [{"propB": "B"}]}';
  const testHyphenated = 'var testHyphenated = {"prop-A": "res-A"}';
  const testLet = "let foobar = {a: ''}; const blargh = {a: 1};";

  const testGenerators = `
  // Test with generator using a named function.
  function* genFunc() {
    for (let i = 0; i < 10; i++) {
      yield i;
    }
  }
  let gen1 = genFunc();
  gen1.next();

  // Test with generator using an anonymous function.
  let gen2 = (function* () {
    for (let i = 0; i < 10; i++) {
      yield i;
    }
  })();`;

  const testGetters = `
    var testGetters = {
      get x() {
        return Object.create(null, Object.getOwnPropertyDescriptors({
          hello: "",
          world: "",
        }));
      },
      get y() {
        return Object.create(null, Object.getOwnPropertyDescriptors({
          get y() {
            return "plop";
          },
        }));
      }
    };
  `;

  const testProxies = `
    var testSelfPrototypeProxy = new Proxy({
      hello: 1
    }, {
      getPrototypeOf: () => testProxy
    });
    var testArrayPrototypeProxy = new Proxy({
      world: 2
    }, {
      getPrototypeOf: () => Array.prototype
    })
  `;

  const sandbox = Cu.Sandbox("http://example.com");
  const dbg = new Debugger();
  const dbgObject = dbg.addDebuggee(sandbox);
  const dbgEnv = dbgObject.asEnvironment();
  Cu.evalInSandbox(
    `
    const hello = Object.create(null, Object.getOwnPropertyDescriptors({world: 1}));
    String.prototype.hello = hello;
    Number.prototype.hello = hello;
    Array.prototype.hello = hello;
  `,
    sandbox
  );
  Cu.evalInSandbox(testArray, sandbox);
  Cu.evalInSandbox(testObject, sandbox);
  Cu.evalInSandbox(testHyphenated, sandbox);
  Cu.evalInSandbox(testLet, sandbox);
  Cu.evalInSandbox(testGenerators, sandbox);
  Cu.evalInSandbox(testGetters, sandbox);
  Cu.evalInSandbox(testProxies, sandbox);

  info("Running tests with dbgObject");
  runChecks(dbgObject, null, sandbox);

  info("Running tests with dbgEnv");
  runChecks(null, dbgEnv, sandbox);
}

function runChecks(dbgObject, environment, sandbox) {
  const propertyProvider = (inputValue, options) =>
    JSPropertyProvider({
      dbgObject,
      environment,
      inputValue,
      ...options,
    });

  info("Test that suggestions are given for 'this'");
  let results = propertyProvider("t");
  test_has_result(results, "this");

  if (dbgObject != null) {
    info("Test that suggestions are given for 'this.'");
    results = propertyProvider("this.");
    test_has_result(results, "testObject");

    info("Test that suggestions are given for '(this).'");
    results = propertyProvider("(this).");
    test_has_result(results, "testObject");

    info("Test that suggestions are given for deep 'this' properties access");
    results = propertyProvider("(this).testObject.propA.");
    test_has_result(results, "shift");

    results = propertyProvider("(this).testObject.propA[");
    test_has_result(results, `"shift"`);

    results = propertyProvider("(this)['testObject']['propA'][");
    test_has_result(results, `"shift"`);

    results = propertyProvider("(this).testObject['propA'].");
    test_has_result(results, "shift");

    info("Test that no suggestions are given for 'this.this'");
    results = propertyProvider("this.this");
    test_has_no_results(results);
  }

  info("Test that suggestions are given for 'globalThis'");
  results = propertyProvider("g");
  test_has_result(results, "globalThis");

  info("Test that suggestions are given for 'globalThis.'");
  results = propertyProvider("globalThis.");
  test_has_result(results, "testObject");

  info("Test that suggestions are given for '(globalThis).'");
  results = propertyProvider("(globalThis).");
  test_has_result(results, "testObject");

  info(
    "Test that suggestions are given for deep 'globalThis' properties access"
  );
  results = propertyProvider("(globalThis).testObject.propA.");
  test_has_result(results, "shift");

  results = propertyProvider("(globalThis).testObject.propA[");
  test_has_result(results, `"shift"`);

  results = propertyProvider("(globalThis)['testObject']['propA'][");
  test_has_result(results, `"shift"`);

  results = propertyProvider("(globalThis).testObject['propA'].");
  test_has_result(results, "shift");

  info("Testing lexical scope issues (Bug 1207868)");
  results = propertyProvider("foobar");
  test_has_result(results, "foobar");

  results = propertyProvider("foobar.");
  test_has_result(results, "a");

  results = propertyProvider("blargh");
  test_has_result(results, "blargh");

  results = propertyProvider("blargh.");
  test_has_result(results, "a");

  info("Test that suggestions are given for 'foo[n]' where n is an integer.");
  results = propertyProvider("testArray[0].");
  test_has_result(results, "propA");

  info("Test that suggestions are given for multidimensional arrays.");
  results = propertyProvider("testArray[2][0].");
  test_has_result(results, "propE");

  info("Test that suggestions are given for nested arrays.");
  results = propertyProvider("testArray[1].propC[0].");
  test_has_result(results, "indexOf");

  info("Test that suggestions are given for literal arrays.");
  results = propertyProvider("[1,2,3].");
  test_has_result(results, "indexOf");

  results = propertyProvider("[1,2,3].h");
  test_has_result(results, "hello");

  results = propertyProvider("[1,2,3].hello.w");
  test_has_result(results, "world");

  info("Test that suggestions are given for literal arrays with newlines.");
  results = propertyProvider("[1,2,3,\n4\n].");
  test_has_result(results, "indexOf");

  info("Test that suggestions are given for literal strings.");
  results = propertyProvider("'foo'.");
  test_has_result(results, "charAt");
  results = propertyProvider('"foo".');
  test_has_result(results, "charAt");
  results = propertyProvider("`foo`.");
  test_has_result(results, "charAt");
  results = propertyProvider("`foo doc`.");
  test_has_result(results, "charAt");
  results = propertyProvider('`foo " doc`.');
  test_has_result(results, "charAt");
  results = propertyProvider("`foo ' doc`.");
  test_has_result(results, "charAt");
  results = propertyProvider("'[1,2,3]'.");
  test_has_result(results, "charAt");
  results = propertyProvider("'foo'.h");
  test_has_result(results, "hello");
  results = propertyProvider("'foo'.hello.w");
  test_has_result(results, "world");
  results = propertyProvider(`"\\n".`);
  test_has_result(results, "charAt");
  results = propertyProvider(`'\\r'.`);
  test_has_result(results, "charAt");
  results = propertyProvider("`\\\\`.");
  test_has_result(results, "charAt");

  info("Test that suggestions are not given for syntax errors.");
  results = propertyProvider("'foo\"");
  Assert.equal(null, results);
  results = propertyProvider("'foo d");
  Assert.equal(null, results);
  results = propertyProvider(`"foo d`);
  Assert.equal(null, results);
  results = propertyProvider("`foo d");
  Assert.equal(null, results);
  results = propertyProvider("[1,',2]");
  Assert.equal(null, results);
  results = propertyProvider("'[1,2].");
  Assert.equal(null, results);
  results = propertyProvider("'foo'..");
  Assert.equal(null, results);

  info("Test that suggestions are not given without a dot.");
  results = propertyProvider("'foo'");
  test_has_no_results(results);
  results = propertyProvider("`foo`");
  test_has_no_results(results);
  results = propertyProvider("[1,2,3]");
  test_has_no_results(results);
  results = propertyProvider("[1,2,3].\n'foo'");
  test_has_no_results(results);

  info("Test that suggestions are not given for index that's out of bounds.");
  results = propertyProvider("testArray[10].");
  Assert.equal(null, results);

  info("Test that invalid element access syntax does not return anything");
  results = propertyProvider("testArray[][1].");
  Assert.equal(null, results);

  info("Test that deep element access works.");
  results = propertyProvider("testObject['propA'][0].");
  test_has_result(results, "propB");

  results = propertyProvider("testArray[1]['propC'].");
  test_has_result(results, "shift");

  results = propertyProvider("testArray[1].propC[0][");
  test_has_result(results, `"trim"`);

  results = propertyProvider("testArray[1].propC[0].");
  test_has_result(results, "trim");

  info(
    "Test that suggestions are displayed when variable is wrapped in parens"
  );
  results = propertyProvider("(testObject)['propA'][0].");
  test_has_result(results, "propB");

  results = propertyProvider("(testArray)[1]['propC'].");
  test_has_result(results, "shift");

  results = propertyProvider("(testArray)[1].propC[0][");
  test_has_result(results, `"trim"`);

  results = propertyProvider("(testArray)[1].propC[0].");
  test_has_result(results, "trim");

  info("Test that suggestions are given if there is an hyphen in the chain.");
  results = propertyProvider("testHyphenated['prop-A'].");
  test_has_result(results, "trim");

  info("Test that we have suggestions for generators.");
  const gen1Result = Cu.evalInSandbox("gen1.next().value", sandbox);
  results = propertyProvider("gen1.");
  test_has_result(results, "next");
  info("Test that the generator next() was not executed");
  const gen1NextResult = Cu.evalInSandbox("gen1.next().value", sandbox);
  Assert.equal(gen1Result + 1, gen1NextResult);

  info("Test with an anonymous generator.");
  const gen2Result = Cu.evalInSandbox("gen2.next().value", sandbox);
  results = propertyProvider("gen2.");
  test_has_result(results, "next");
  const gen2NextResult = Cu.evalInSandbox("gen2.next().value", sandbox);
  Assert.equal(gen2Result + 1, gen2NextResult);

  info(
    "Test that getters are not executed if authorizedEvaluations is undefined"
  );
  results = propertyProvider("testGetters.x.");
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "x"],
  });

  results = propertyProvider("testGetters.x[");
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "x"],
  });

  results = propertyProvider("testGetters.x.hell");
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "x"],
  });

  results = propertyProvider("testGetters.x['hell");
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "x"],
  });

  info(
    "Test that getters are not executed if authorizedEvaluations does not match"
  );
  results = propertyProvider("testGetters.x.", { authorizedEvaluations: [] });
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "x"],
  });

  results = propertyProvider("testGetters.x.", {
    authorizedEvaluations: [["testGetters"]],
  });
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "x"],
  });

  results = propertyProvider("testGetters.x.", {
    authorizedEvaluations: [["testGtrs", "x"]],
  });
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "x"],
  });

  results = propertyProvider("testGetters.x.", {
    authorizedEvaluations: [["x"]],
  });
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "x"],
  });

  info("Test that deep getter property access returns intermediate getters");
  results = propertyProvider("testGetters.y.y.");
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "y"],
  });

  results = propertyProvider("testGetters['y'].y.");
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "y"],
  });

  results = propertyProvider("testGetters['y']['y'].");
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "y"],
  });

  results = propertyProvider("testGetters.y['y'].");
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "y"],
  });

  info("Test that deep getter property access invoke intermediate getters");
  results = propertyProvider("testGetters.y.y.", {
    authorizedEvaluations: [["testGetters", "y"]],
  });
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "y", "y"],
  });

  results = propertyProvider("testGetters['y'].y.", {
    authorizedEvaluations: [["testGetters", "y"]],
  });
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "y", "y"],
  });

  results = propertyProvider("testGetters['y']['y'].", {
    authorizedEvaluations: [["testGetters", "y"]],
  });
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "y", "y"],
  });

  results = propertyProvider("testGetters.y['y'].", {
    authorizedEvaluations: [["testGetters", "y"]],
  });
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "y", "y"],
  });

  info(
    "Test that getters are executed if matching an authorizedEvaluation element"
  );
  results = propertyProvider("testGetters.x.", {
    authorizedEvaluations: [["testGetters", "x"]],
  });
  test_has_exact_results(results, ["hello", "world"]);
  Assert.ok(Object.keys(results).includes("isUnsafeGetter") === false);
  Assert.ok(Object.keys(results).includes("getterPath") === false);

  results = propertyProvider("testGetters.x.", {
    authorizedEvaluations: [["testGetters", "x"], ["y"]],
  });
  test_has_exact_results(results, ["hello", "world"]);
  Assert.ok(Object.keys(results).includes("isUnsafeGetter") === false);
  Assert.ok(Object.keys(results).includes("getterPath") === false);

  info("Test that executing getters filters with provided string");
  results = propertyProvider("testGetters.x.hell", {
    authorizedEvaluations: [["testGetters", "x"]],
  });
  test_has_exact_results(results, ["hello"]);

  results = propertyProvider("testGetters.x['hell", {
    authorizedEvaluations: [["testGetters", "x"]],
  });
  test_has_exact_results(results, ["'hello'"]);

  info(
    "Test children getters are not executed if not included in authorizedEvaluation"
  );
  results = propertyProvider("testGetters.y.y.", {
    authorizedEvaluations: [["testGetters", "y", "y"]],
  });
  Assert.deepEqual(results, {
    isUnsafeGetter: true,
    getterPath: ["testGetters", "y"],
  });

  info(
    "Test children getters are executed if matching an authorizedEvaluation element"
  );
  results = propertyProvider("testGetters.y.y.", {
    authorizedEvaluations: [
      ["testGetters", "y"],
      ["testGetters", "y", "y"],
    ],
  });
  test_has_result(results, "trim");

  info("Test with number literals");
  results = propertyProvider("1.");
  Assert.ok(results === null, "Does not complete on possible floating number");

  results = propertyProvider("(1)..");
  Assert.ok(results === null, "Does not complete on invalid syntax");

  results = propertyProvider("(1.1.).");
  Assert.ok(results === null, "Does not complete on invalid syntax");

  results = propertyProvider("1..");
  test_has_result(results, "toFixed");

  results = propertyProvider("1 .");
  test_has_result(results, "toFixed");

  results = propertyProvider("1\n.");
  test_has_result(results, "toFixed");

  results = propertyProvider(".1.");
  test_has_result(results, "toFixed");

  results = propertyProvider("1[");
  test_has_result(results, `"toFixed"`);

  results = propertyProvider("1[toFixed");
  test_has_exact_results(results, [`"toFixed"`]);

  results = propertyProvider("1['toFixed");
  test_has_exact_results(results, ["'toFixed'"]);

  results = propertyProvider("1.1[");
  test_has_result(results, `"toFixed"`);

  results = propertyProvider("(1).");
  test_has_result(results, "toFixed");

  results = propertyProvider("(.1).");
  test_has_result(results, "toFixed");

  results = propertyProvider("(1.1).");
  test_has_result(results, "toFixed");

  results = propertyProvider("(1).toFixed");
  test_has_exact_results(results, ["toFixed"]);

  results = propertyProvider("(1)[");
  test_has_result(results, `"toFixed"`);

  results = propertyProvider("(1.1)[");
  test_has_result(results, `"toFixed"`);

  results = propertyProvider("(1)[toFixed");
  test_has_exact_results(results, [`"toFixed"`]);

  results = propertyProvider("(1)['toFixed");
  test_has_exact_results(results, ["'toFixed'"]);

  results = propertyProvider("(1).h");
  test_has_result(results, "hello");

  results = propertyProvider("(1).hello.w");
  test_has_result(results, "world");

  info("Test access on dot-notation invalid property name");
  results = propertyProvider("testHyphenated.prop");
  Assert.ok(
    !results.matches.has("prop-A"),
    "Does not return invalid property name on dot access"
  );

  results = propertyProvider("testHyphenated['prop");
  test_has_result(results, `'prop-A'`);

  results = propertyProvider(`//t`);
  Assert.ok(results === null, "Does not complete in inline comment");

  results = propertyProvider(`// t`);
  Assert.ok(
    results === null,
    "Does not complete in inline comment after space"
  );

  results = propertyProvider(`//I'm a comment\nt`);
  test_has_result(results, "testObject");

  results = propertyProvider(`1/t`);
  test_has_result(results, "testObject");

  results = propertyProvider(`/* t`);
  Assert.ok(results === null, "Does not complete in multiline comment");

  results = propertyProvider(`/*I'm\nt`);
  Assert.ok(
    results === null,
    "Does not complete in multiline comment after line break"
  );

  results = propertyProvider(`/*I'm a comment\n \t * /t`);
  Assert.ok(
    results === null,
    "Does not complete in multiline comment after line break and invalid comment end"
  );

  results = propertyProvider(`/*I'm a comment\n \t */t`);
  test_has_result(results, "testObject");

  results = propertyProvider(`/*I'm a comment\n \t */\n\nt`);
  test_has_result(results, "testObject");

  info("Test local expression variables");
  results = propertyProvider("b", { expressionVars: ["a", "b", "c"] });
  test_has_result(results, "b");
  Assert.equal(results.matches.has("a"), false);
  Assert.equal(results.matches.has("c"), false);

  info(
    "Test that local expression variables are not included when accessing an object properties"
  );
  results = propertyProvider("testObject.prop", {
    expressionVars: ["propLocal"],
  });
  Assert.equal(results.matches.has("propLocal"), false);
  test_has_result(results, "propA");

  results = propertyProvider("testObject['prop", {
    expressionVars: ["propLocal"],
  });
  test_has_result(results, "'propA'");
  Assert.equal(results.matches.has("propLocal"), false);

  info("Test that expression with optional chaining operator are completed");
  results = propertyProvider("testObject?.prop");
  test_has_result(results, "propA");

  results = propertyProvider("testObject?.propA[0]?.propB?.to");
  test_has_result(results, "toString");

  results = propertyProvider("testObject?.propA?.[0]?.propB?.to");
  test_has_result(results, "toString");

  results = propertyProvider(
    "testObject      ?.    propA[0]    ?.    propB  ?.   to"
  );
  test_has_result(results, "toString");

  results = propertyProvider("testObject?.[prop");
  test_has_result(results, '"propA"');

  results = propertyProvider(`testObject?.["prop`);
  test_has_result(results, '"propA"');

  results = propertyProvider(`testObject?.['prop`);
  test_has_result(results, `'propA'`);

  results = propertyProvider(`testObject?.["propA"]?.[0]?.["propB"]?.["to`);
  test_has_result(results, `"toString"`);

  results = propertyProvider(
    `testObject  ?.    ["propA"]  ?.   [0]  ?.   ["propB"]  ?.  ['to`
  );
  test_has_result(results, "'toString'");

  results = propertyProvider("[1,2,3]?.");
  test_has_result(results, "indexOf");

  results = propertyProvider("'foo'?.");
  test_has_result(results, "charAt");

  results = propertyProvider("1?.");
  test_has_result(results, "toFixed");

  // check this doesn't throw since `propC` is not defined.
  results = propertyProvider("testObject?.propC?.this?.does?.not?.exist?.d");

  // check that ternary operator isn't mistaken for optional chaining
  results = propertyProvider(`true?.3.to`);
  test_has_result(results, `toExponential`);

  results = propertyProvider(`true?.3?.to`);
  test_has_result(results, `toExponential`);

  // Test more ternary
  results = propertyProvider(`true?t`);
  test_has_result(results, `testObject`);

  results = propertyProvider(`true??t`);
  test_has_result(results, `testObject`);

  results = propertyProvider(`true?/* comment */t`);
  test_has_result(results, `testObject`);

  results = propertyProvider(`true?<t`);
  test_has_no_results(results);

  // Test autocompletion on debugger statement does not throw
  results = propertyProvider(`debugger.`);
  Assert.ok(results === null, "Does not complete a debugger keyword");

  // Test autocompletion on Proxies
  // proxy does not get autocompletion result from prototype defined in `getPrototypeOf`
  test_has_no_results(propertyProvider(`testArrayPrototypeProxy.filte`));
  results = propertyProvider(`testArrayPrototypeProxy.`);
  // it does get the own property
  test_has_result(results, `world`);
  // as well as method from the actual proxy target prototype
  test_has_result(results, `hasOwnProperty`);

  results = propertyProvider(`testSelfPrototypeProxy.`);
  test_has_result(results, `hello`);
  test_has_result(results, `hasOwnProperty`);
}

/**
 * A helper that ensures an empty array of results were found.
 * @param Object results
 *        The results returned by JSPropertyProvider.
 */
function test_has_no_results(results) {
  Assert.notEqual(results, null);
  Assert.equal(results.matches.size, 0);
}
/**
 * A helper that ensures (required) results were found.
 * @param Object results
 *        The results returned by JSPropertyProvider.
 * @param String requiredSuggestion
 *        A suggestion that must be found from the results.
 */
function test_has_result(results, requiredSuggestion) {
  Assert.notEqual(results, null);
  Assert.ok(results.matches.size > 0);
  Assert.ok(
    results.matches.has(requiredSuggestion),
    `<${requiredSuggestion}> found in ${[...results.matches.values()].join(
      " - "
    )}`
  );
}

/**
 * A helper that ensures results are the expected ones.
 * @param Object results
 *        The results returned by JSPropertyProvider.
 * @param Array expectedMatches
 *        An array of the properties that should be returned by JsPropertyProvider.
 */
function test_has_exact_results(results, expectedMatches) {
  Assert.deepEqual([...results.matches], expectedMatches);
}
