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

// Test the "Copy object" menu item of the webconsole is enabled only when
// clicking on messages that are associated with an object actor.

"use strict";

const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html><script>
  window.bar = { baz: 1 };
  console.log("foo");
  console.log("foo", window.bar);
  console.log(["foo", window.bar, 2]);
  console.group("group");
  console.groupCollapsed("collapsed");
  console.groupEnd();
  console.log(532);
  console.log(true);
  console.log(false);
  console.log(undefined);
  console.log(null);
  /* Verify that the conflicting binding on user code doesn't break the
   * functionality. */
  function copy() { alert("user-defined function is called"); }
  /* Check that trying to copy an object that can't be serialized displays an error in the UI */
  var cyclical = {}; cyclical.cycle = cyclical;
  console.log(cyclical);
  
  /* Verify that custom formatters don't break copying. */
  window.devtoolsFormatters = [
    {
      header: (obj, config) => {
        if (!obj?.useCustomFormatter)
          return null;
        return [
          "span",
          { "style": "color: red" },
          "Hello, ",
          [
            "span",
            { "style": "color: green" },
            "world!"
          ]
        ];
      },
      hasBody: (obj) => {
        return false;
      },
    }
  ];
  console.log({ useCustomFormatter: true, a: 5 });
</script>`;
const copyObjectMenuItemId = "#console-menu-copy-object";

add_task(async function () {
  await pushPref("devtools.custom-formatters.enabled", true);

  const hud = await openNewTabAndConsole(TEST_URI);

  // Reload the browser to ensure the custom formatters are picked up
  await reloadBrowser();

  const [msgWithText, msgWithObj, msgNested] = await waitFor(() =>
    findConsoleAPIMessages(hud, "foo")
  );
  ok(
    msgWithText && msgWithObj && msgNested,
    "Three messages should have appeared"
  );

  const [groupMsgObj] = await waitFor(() =>
    findMessagePartsByType(hud, {
      text: "group",
      typeSelector: ".console-api",
      partSelector: ".message-body",
    })
  );
  const [collapsedGroupMsgObj] = await waitFor(() =>
    findMessagePartsByType(hud, {
      text: "collapsed",
      typeSelector: ".console-api",
      partSelector: ".message-body",
    })
  );
  const [numberMsgObj] = await waitFor(() =>
    findMessagePartsByType(hud, {
      text: `532`,
      typeSelector: ".console-api",
      partSelector: ".message-body",
    })
  );
  const [trueMsgObj] = await waitFor(() =>
    findMessagePartsByType(hud, {
      text: `true`,
      typeSelector: ".console-api",
      partSelector: ".message-body",
    })
  );
  const [falseMsgObj] = await waitFor(() =>
    findMessagePartsByType(hud, {
      text: `false`,
      typeSelector: ".console-api",
      partSelector: ".message-body",
    })
  );
  const [undefinedMsgObj] = await waitFor(() =>
    findMessagePartsByType(hud, {
      text: `undefined`,
      typeSelector: ".console-api",
      partSelector: ".message-body",
    })
  );
  const [nullMsgObj] = await waitFor(() =>
    findMessagePartsByType(hud, {
      text: `null`,
      typeSelector: ".console-api",
      partSelector: ".message-body",
    })
  );
  const [customMsgObj] = await waitFor(() =>
    findMessagePartsByType(hud, {
      text: `Hello, world!`,
      typeSelector: ".console-api",
      partSelector: ".message-body",
    })
  );
  ok(nullMsgObj, "One message with null value should have appeared");

  const text = msgWithText.querySelector(".objectBox-string");
  const objInMsgWithObj = msgWithObj.querySelector(".objectBox-object");
  const textInMsgWithObj = msgWithObj.querySelector(".objectBox-string");

  // The third message has an object nested in an array, the array is therefore the top
  // object, the object is the nested object.
  const topObjInMsg = msgNested.querySelector(".objectBox-array");
  const nestedObjInMsg = msgNested.querySelector(".objectBox-object");

  const consoleMessages = await waitFor(() =>
    findMessagePartsByType(hud, {
      text: 'console.log("foo");',
      typeSelector: ".console-api",
      partSelector: ".message-location",
    })
  );
  await testCopyObjectMenuItemDisabled(hud, consoleMessages[0]);

  info(`Check "Copy object" is enabled for text only messages
    thus copying the text`);
  await testCopyObject(hud, text, `foo`, false);

  info(`Check "Copy object" is enabled for text in complex messages
   thus copying the text`);
  await testCopyObject(hud, textInMsgWithObj, `foo`, false);

  info("Check `Copy object` is enabled for objects in complex messages");
  await testCopyObject(hud, objInMsgWithObj, `{"baz":1}`, true);

  info("Check `Copy object` is enabled for top object in nested messages");
  await testCopyObject(hud, topObjInMsg, `["foo",{"baz":1},2]`, true);

  info("Check `Copy object` is enabled for nested object in nested messages");
  await testCopyObject(hud, nestedObjInMsg, `{"baz":1}`, true);

  info("Check `Copy object` is disabled on `console.group('group')` messages");
  await testCopyObjectMenuItemDisabled(hud, groupMsgObj);

  info(`Check "Copy object" is disabled in "console.groupCollapsed('collapsed')"
    messages`);
  await testCopyObjectMenuItemDisabled(hud, collapsedGroupMsgObj);

  // Check for primitive objects
  info("Check `Copy object` is enabled for numbers");
  await testCopyObject(hud, numberMsgObj, `532`, false);

  info("Check `Copy object` is enabled for booleans");
  await testCopyObject(hud, trueMsgObj, `true`, false);
  await testCopyObject(hud, falseMsgObj, `false`, false);

  info("Check `Copy object` is enabled for undefined and null");
  await testCopyObject(hud, undefinedMsgObj, `undefined`, false);
  await testCopyObject(hud, nullMsgObj, `null`, false);

  info("Check `Copy object` is enabled for custom-formatted objects");
  await testCopyObject(
    hud,
    customMsgObj,
    `{"useCustomFormatter":true,"a":5}`,
    true
  );

  info(
    "Check `Copy object` for an object with cyclical reference displays an error in the UI"
  );
  const clipboardContent = SpecialPowers.getClipboardData("text/plain");
  const [cyclicalMsgObj] = await waitFor(() =>
    findMessagePartsByType(hud, {
      text: `cycle`,
      typeSelector: ".console-api",
      partSelector: ".message-body",
    })
  );
  const menuPopup = await openContextMenu(
    hud,
    cyclicalMsgObj.querySelector(".objectBox-object")
  );
  menuPopup.activateItem(menuPopup.querySelector(copyObjectMenuItemId));

  info("Wait until the notification box displays the error");
  const notificationBox = await waitFor(() =>
    hud.ui.document.getElementById("webconsole-notificationbox")
  );
  is(
    notificationBox.querySelector(".notification").textContent,
    "`copy` command failed, object can’t be stringified: TypeError: cyclic object value",
    "Notification is displayed with expected message"
  );

  // Wait for a bit to check the clipboard isn't overridden
  await wait(500);
  is(
    SpecialPowers.getClipboardData("text/plain"),
    clipboardContent,
    "clipboard wasn't overridden"
  );
});

async function testCopyObject(hud, element, expectedMessage, objectInput) {
  info("Check `Copy object` is enabled");
  const menuPopup = await openContextMenu(hud, element);
  const copyObjectMenuItem = menuPopup.querySelector(copyObjectMenuItemId);
  ok(
    !copyObjectMenuItem.disabled,
    "`Copy object` is enabled for object in complex message"
  );
  is(
    copyObjectMenuItem.getAttribute("accesskey"),
    "o",
    "`Copy object` has the right accesskey"
  );

  const validatorFn = data => {
    const prettifiedMessage = prettyPrintMessage(expectedMessage, objectInput);
    return data === prettifiedMessage;
  };

  info("Activate item `Copy object`");
  await waitForClipboardPromise(
    () => menuPopup.activateItem(copyObjectMenuItem),
    validatorFn
  );
}

async function testCopyObjectMenuItemDisabled(hud, element) {
  const menuPopup = await openContextMenu(hud, element);
  const copyObjectMenuItem = menuPopup.querySelector(copyObjectMenuItemId);
  ok(
    copyObjectMenuItem.disabled,
    `"Copy object" is disabled for messages
    with no variables/objects`
  );
  await hideContextMenu(hud);
}

function prettyPrintMessage(message, isObject) {
  return isObject ? JSON.stringify(JSON.parse(message), null, 2) : message;
}
