/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html>
<script>
let x = 3, y = 4;
function zzyzx() {
  x = 10;
}
function zzyzx2() {
  x = 10;
}
var obj = {propA: "A", propB: "B"};
var array = [1, 2, 3];
var $$ = 42;
</script>
<h1>title</h1>
`;

const EAGER_EVALUATION_PREF = "devtools.webconsole.input.eagerEvaluation";

// Basic testing of eager evaluation functionality. Expressions which can be
// eagerly evaluated should show their results, and expressions with side
// effects should not perform those side effects.
add_task(async function () {
  // Open the inspector first to select a node, so that we can later test "$0"
  const toolbox = await openNewTabAndToolbox(TEST_URI, "inspector");
  await selectNodeWithPicker(toolbox, "h1");

  info("Picker mode stopped, <h1> selected, now switching to the console");
  const hud = await openConsole();

  // Do an evaluation to populate $_
  await executeAndWaitForResultMessage(
    hud,
    "'result: ' + (x + y)",
    "result: 7"
  );

  setInputValue(hud, "x + y");
  await waitForEagerEvaluationResult(hud, "7");

  setInputValue(hud, "x + y + undefined");
  await waitForEagerEvaluationResult(hud, "NaN");

  setInputValue(hud, "1 - 1");
  await waitForEagerEvaluationResult(hud, "0");

  setInputValue(hud, "!true");
  await waitForEagerEvaluationResult(hud, "false");

  setInputValue(hud, `"ab".slice(0, 0)`);
  await waitForEagerEvaluationResult(hud, `""`);

  setInputValue(hud, `JSON.parse("null")`);
  await waitForEagerEvaluationResult(hud, "null");

  setInputValue(hud, "-x / 0");
  await waitForEagerEvaluationResult(hud, "-Infinity");

  setInputValue(hud, "x = 10");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "x + 1");
  await waitForEagerEvaluationResult(hud, "4");

  setInputValue(hud, "zzyzx()");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "x + 2");
  await waitForEagerEvaluationResult(hud, "5");

  setInputValue(hud, "x +");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "x + z");
  await waitForEagerEvaluationResult(hud, /ReferenceError/);

  setInputValue(hud, "var a = 5");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "x + a");
  await waitForEagerEvaluationResult(hud, /ReferenceError/);

  setInputValue(hud, '"foobar".slice(1, 5)');
  await waitForEagerEvaluationResult(hud, '"ooba"');

  setInputValue(hud, '"foobar".toString()');
  await waitForEagerEvaluationResult(hud, '"foobar"');

  setInputValue(hud, "(new Array()).push(3)");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "(new Uint32Array([1,2,3])).includes(2)");
  await waitForEagerEvaluationResult(hud, "true");

  setInputValue(hud, "Math.round(3.2)");
  await waitForEagerEvaluationResult(hud, "3");

  info("Check web console commands");
  setInputValue(hud, "help()");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "$0");
  await waitForEagerEvaluationResult(hud, `<h1>`);

  setInputValue(hud, "$('html')");
  await waitForEagerEvaluationResult(hud, `<html>`);

  setInputValue(hud, "$$");
  await waitForEagerEvaluationResult(hud, `42`);

  info("Check that $_ wasn't polluted by eager evaluations");
  setInputValue(hud, "$_");
  await waitForEagerEvaluationResult(hud, `"result: 7"`);

  setInputValue(hud, "'> ' + $_");
  await waitForEagerEvaluationResult(hud, `"> result: 7"`);

  info("Switch to editor mode");
  await toggleLayout(hud);
  await waitForEagerEvaluationResult(hud, `"> result: 7"`);
  ok(true, "eager evaluation is still displayed in editor mode");

  setInputValue(hud, "4 + 7");
  await waitForEagerEvaluationResult(hud, "11");

  // go back to inline layout.
  await toggleLayout(hud);

  setInputValue(hud, "typeof new Proxy({}, {})");
  await waitForEagerEvaluationResult(hud, `"object"`);

  setInputValue(hud, "typeof Proxy.revocable({}, {}).revoke");
  await waitForEagerEvaluationResult(hud, `"function"`);

  setInputValue(hud, "Reflect.apply(() => 1, null, [])");
  await waitForEagerEvaluationResult(hud, "1");
  setInputValue(
    hud,
    `Reflect.apply(() => {
      globalThis.sideEffect = true;
      return 2;
    }, null, [])`
  );
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.construct(Array, []).length");
  await waitForEagerEvaluationResult(hud, "0");
  setInputValue(
    hud,
    `Reflect.construct(function() {
      globalThis.sideEffect = true;
    }, [])`
  );
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.defineProperty({}, 'a', {value: 1})");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.deleteProperty({a: 1}, 'a')");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.get({a: 1}, 'a')");
  await waitForEagerEvaluationResult(hud, "1");
  setInputValue(hud, "Reflect.get({get a(){return 2}, 'a')");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.getOwnPropertyDescriptor({a: 1}, 'a').value");
  await waitForEagerEvaluationResult(hud, "1");
  setInputValue(
    hud,
    `Reflect.getOwnPropertyDescriptor(
      new Proxy({ a: 2 }, { getOwnPropertyDescriptor() {
        globalThis.sideEffect = true;
        return { value: 2 };
      }}),
      "a"
    )`
  );
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.getPrototypeOf({}) === Object.prototype");
  await waitForEagerEvaluationResult(hud, "true");
  setInputValue(
    hud,
    `Reflect.getPrototypeOf(
      new Proxy({}, { getPrototypeOf() {
        globalThis.sideEffect = true;
        return null;
      }})
    )`
  );
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.has({a: 1}, 'a')");
  await waitForEagerEvaluationResult(hud, "true");
  setInputValue(
    hud,
    `Reflect.has(
      new Proxy({ a: 2 }, { has() {
        globalThis.sideEffect = true;
        return true;
      }}), "a"
    )`
  );
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.isExtensible({})");
  await waitForEagerEvaluationResult(hud, "true");
  setInputValue(
    hud,
    `Reflect.isExtensible(
      new Proxy({}, { isExtensible() {
        globalThis.sideEffect = true;
        return true;
      }})
    )`
  );
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.ownKeys({a: 1})[0]");
  await waitForEagerEvaluationResult(hud, `"a"`);
  setInputValue(
    hud,
    `Reflect.ownKeys(
      new Proxy({}, { ownKeys() {
        globalThis.sideEffect = true;
        return ['a'];
      }})
    )`
  );
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.preventExtensions({})");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.set({}, 'a', 1)");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "Reflect.setPrototypeOf({}, null)");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "[] instanceof Array");
  await waitForEagerEvaluationResult(hud, "true");

  setInputValue(hud, "Int8Array.from({length: 1})[0]");
  await waitForEagerEvaluationResult(hud, "0");

  setInputValue(hud, "Float64Array.of(1)[0]");
  await waitForEagerEvaluationResult(hud, "1");

  setInputValue(hud, "array.fill()");
  await waitForNoEagerEvaluationResult(hud);

  setInputValue(hud, "array");
  await waitForEagerEvaluationResult(hud, "Array(3) [ 1, 2, 3 ]");

  info("Check that top-level await expression are not evaluated");
  setInputValue(hud, "await 1; 2 + 3;");
  await waitForNoEagerEvaluationResult(hud);
  ok(true, "instant evaluation is disabled for top-level await expressions");
});

// Test that the currently selected autocomplete result is eagerly evaluated.
add_task(async function () {
  const hud = await openNewTabAndConsole(TEST_URI);
  const { jsterm } = hud;

  const { autocompletePopup: popup } = jsterm;

  ok(!popup.isOpen, "popup is not open");
  let onPopupOpen = popup.once("popup-opened");
  EventUtils.sendString("zzy");
  await onPopupOpen;

  await waitForEagerEvaluationResult(hud, "function zzyzx()");
  EventUtils.synthesizeKey("KEY_ArrowDown");
  await waitForEagerEvaluationResult(hud, "function zzyzx2()");

  // works when the input isn't properly cased but matches an autocomplete item
  setInputValue(hud, "o");
  onPopupOpen = popup.once("popup-opened");
  EventUtils.sendString("B");
  await waitForEagerEvaluationResult(hud, `Object { propA: "A", propB: "B" }`);

  // works when doing element access without quotes
  setInputValue(hud, "obj[p");
  onPopupOpen = popup.once("popup-opened");
  EventUtils.sendString("RoP");
  await waitForEagerEvaluationResult(hud, `"A"`);

  EventUtils.synthesizeKey("KEY_ArrowDown");
  await waitForEagerEvaluationResult(hud, `"B"`);

  // closing the autocomplete popup updates the eager evaluation result
  let onPopupClose = popup.once("popup-closed");
  EventUtils.synthesizeKey("KEY_Escape");
  await onPopupClose;
  await waitForNoEagerEvaluationResult(hud);

  info(
    "Check that closing the popup by adding a space will update the instant eval result"
  );
  await setInputValueForAutocompletion(hud, "x");
  await waitForEagerEvaluationResult(hud, "3");

  EventUtils.synthesizeKey("KEY_ArrowDown");
  // Navigates to the XMLDocument item in the popup
  await waitForEagerEvaluationResult(hud, `function XMLDocument()`);

  onPopupClose = popup.once("popup-closed");
  EventUtils.sendString(" ");
  await waitForEagerEvaluationResult(hud, `3`);
});

// Test that the setting works as expected.
add_task(async function () {
  // start with the pref off.
  await pushPref(EAGER_EVALUATION_PREF, false);
  const hud = await openNewTabAndConsole(TEST_URI);

  info("Check that the setting is disabled");
  checkConsoleSettingState(
    hud,
    ".webconsole-console-settings-menu-item-eager-evaluation",
    false
  );

  // Wait for the autocomplete popup to be displayed so we know the eager evaluation could
  // have occured.
  const onPopupOpen = hud.jsterm.autocompletePopup.once("popup-opened");
  await setInputValueForAutocompletion(hud, "x + y");
  await onPopupOpen;

  is(
    getEagerEvaluationElement(hud),
    null,
    "There's no eager evaluation element"
  );
  hud.jsterm.autocompletePopup.hidePopup();

  info("Turn on the eager evaluation");
  toggleConsoleSetting(
    hud,
    ".webconsole-console-settings-menu-item-eager-evaluation"
  );
  await waitFor(() => getEagerEvaluationElement(hud));
  ok(true, "The eager evaluation element is now displayed");
  is(
    Services.prefs.getBoolPref(EAGER_EVALUATION_PREF),
    true,
    "Pref was changed"
  );

  setInputValue(hud, "1 + 2");
  await waitForEagerEvaluationResult(hud, "3");
  ok(true, "Eager evaluation result is displayed");

  info("Turn off the eager evaluation");
  toggleConsoleSetting(
    hud,
    ".webconsole-console-settings-menu-item-eager-evaluation"
  );
  await waitFor(() => !getEagerEvaluationElement(hud));
  is(
    Services.prefs.getBoolPref(EAGER_EVALUATION_PREF),
    false,
    "Pref was changed"
  );
  ok(true, "Eager evaluation element is no longer displayed");

  // reset the preference
  await pushPref(EAGER_EVALUATION_PREF, true);
});

// Test that the console instant evaluation is updated on page navigation
add_task(async function () {
  const start_uri = "data:text/html, Start uri";
  const new_uri = "data:text/html, Test console refresh instant value";
  const hud = await openNewTabAndConsole(start_uri);

  setInputValue(hud, "globalThis.location.href");
  await waitForEagerEvaluationResult(hud, `"${start_uri}"`);

  await navigateTo(new_uri);
  await waitForEagerEvaluationResult(hud, `"${new_uri}"`);
});
