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

"use strict";

// Check evaluating and expanding getters in the console.
const TEST_URI =
  "data:text/html;charset=utf8,<!DOCTYPE html><h1>Object Inspector on Getters</h1>";
const { ELLIPSIS } = require("resource://devtools/shared/l10n.js");

add_task(async function () {
  const hud = await openNewTabAndConsole(TEST_URI);

  const LONGSTRING = "ab ".repeat(1e5);

  await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [LONGSTRING],
    function (longString) {
      content.wrappedJSObject.console.log(
        "oi-test",
        Object.create(
          null,
          Object.getOwnPropertyDescriptors({
            get myStringGetter() {
              return "hello";
            },
            get myNumberGetter() {
              return 123;
            },
            get myUndefinedGetter() {
              return undefined;
            },
            get myNullGetter() {
              return null;
            },
            get myZeroGetter() {
              return 0;
            },
            get myEmptyStringGetter() {
              return "";
            },
            get myFalseGetter() {
              return false;
            },
            get myTrueGetter() {
              return true;
            },
            get myObjectGetter() {
              return { foo: "bar" };
            },
            get myArrayGetter() {
              return Array.from({ length: 1000 }, (_, i) => i);
            },
            get myMapGetter() {
              return new Map([["foo", { bar: "baz" }]]);
            },
            get myProxyGetter() {
              const handler = {
                get(target, name) {
                  return name in target ? target[name] : 37;
                },
              };
              return new Proxy({ a: 1 }, handler);
            },
            get myThrowingGetter() {
              throw new Error("myError");
            },
            get myLongStringGetter() {
              return longString;
            },
            get ["hyphen-getter"]() {
              return "---";
            },
            get [`"quoted-getter"`]() {
              return "quoted";
            },
            get [`"'\``]() {
              return "quoted2";
            },
          })
        )
      );
    }
  );

  const node = await waitFor(() => findConsoleAPIMessage(hud, "oi-test"));
  const oi = node.querySelector(".tree");

  await expandObjectInspectorNode(oi.querySelector(".tree-node"));

  await testStringGetter(oi);
  await testNumberGetter(oi);
  await testUndefinedGetter(oi);
  await testNullGetter(oi);
  await testZeroGetter(oi);
  await testEmptyStringGetter(oi);
  await testFalseGetter(oi);
  await testTrueGetter(oi);
  await testObjectGetter(oi);
  await testArrayGetter(oi);
  await testMapGetter(oi);
  await testProxyGetter(oi);
  await testThrowingGetter(oi);
  await testLongStringGetter(oi, LONGSTRING);
  await testHypgenGetter(oi);
  await testQuotedGetters(oi);
});

async function testStringGetter(oi) {
  let node = findObjectInspectorNode(oi, "myStringGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myStringGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myStringGetter");
  ok(
    node.textContent.includes(`myStringGetter: "hello"`),
    "String getter now has the expected text content"
  );
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
}

async function testNumberGetter(oi) {
  let node = findObjectInspectorNode(oi, "myNumberGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myNumberGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myNumberGetter");
  ok(
    node.textContent.includes(`myNumberGetter: 123`),
    "Number getter now has the expected text content"
  );
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
}

async function testUndefinedGetter(oi) {
  let node = findObjectInspectorNode(oi, "myUndefinedGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myUndefinedGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myUndefinedGetter");
  ok(
    node.textContent.includes(`myUndefinedGetter: undefined`),
    "undefined getter now has the expected text content"
  );
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
}

async function testNullGetter(oi) {
  let node = findObjectInspectorNode(oi, "myNullGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myNullGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myNullGetter");
  ok(
    node.textContent.includes(`myNullGetter: null`),
    "null getter now has the expected text content"
  );
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
}

async function testZeroGetter(oi) {
  let node = findObjectInspectorNode(oi, "myZeroGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myZeroGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myZeroGetter");
  ok(
    node.textContent.includes(`myZeroGetter: 0`),
    "0 getter now has the expected text content"
  );
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
}

async function testEmptyStringGetter(oi) {
  let node = findObjectInspectorNode(oi, "myEmptyStringGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myEmptyStringGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myEmptyStringGetter");
  ok(
    node.textContent.includes(`myEmptyStringGetter: ""`),
    "empty string getter now has the expected text content"
  );
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
}

async function testFalseGetter(oi) {
  let node = findObjectInspectorNode(oi, "myFalseGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myFalseGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myFalseGetter");
  ok(
    node.textContent.includes(`myFalseGetter: false`),
    "false getter now has the expected text content"
  );
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
}

async function testTrueGetter(oi) {
  let node = findObjectInspectorNode(oi, "myTrueGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myTrueGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myTrueGetter");
  ok(
    node.textContent.includes(`myTrueGetter: true`),
    "false getter now has the expected text content"
  );
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
}

async function testObjectGetter(oi) {
  let node = findObjectInspectorNode(oi, "myObjectGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myObjectGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myObjectGetter");
  ok(
    node.textContent.includes(`myObjectGetter: Object { foo: "bar" }`),
    "object getter now has the expected text content"
  );
  is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded");

  await expandObjectInspectorNode(node);
  checkChildren(node, [`foo: "bar"`, `<prototype>`]);
}

async function testArrayGetter(oi) {
  let node = findObjectInspectorNode(oi, "myArrayGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myArrayGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myArrayGetter");
  ok(
    node.textContent.includes(
      `myArrayGetter: Array(1000) [ 0, 1, 2, ${ELLIPSIS} ]`
    ),
    "Array getter now has the expected text content - "
  );
  is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded");

  await expandObjectInspectorNode(node);
  const children = getObjectInspectorChildrenNodes(node);

  const firstBucket = children[0];
  ok(firstBucket.textContent.includes(`[0${ELLIPSIS}99]`), "Array has buckets");

  is(
    isObjectInspectorNodeExpandable(firstBucket),
    true,
    "The bucket can be expanded"
  );
  await expandObjectInspectorNode(firstBucket);
  checkChildren(
    firstBucket,
    Array.from({ length: 100 }, (_, i) => `${i}: ${i}`)
  );
}

async function testMapGetter(oi) {
  let node = findObjectInspectorNode(oi, "myMapGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myMapGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myMapGetter");
  ok(
    node.textContent.includes(`myMapGetter: Map`),
    "map getter now has the expected text content"
  );
  is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded");

  await expandObjectInspectorNode(node);
  checkChildren(node, [`size`, `<entries>`, `<prototype>`]);

  const entriesNode = findObjectInspectorNode(oi, "<entries>");
  await expandObjectInspectorNode(entriesNode);
  checkChildren(entriesNode, [`foo → Object { bar: "baz" }`]);

  const entryNode = getObjectInspectorChildrenNodes(entriesNode)[0];
  await expandObjectInspectorNode(entryNode);
  checkChildren(entryNode, [`<key>: "foo"`, `<value>: Object { bar: "baz" }`]);
}

async function testProxyGetter(oi) {
  let node = findObjectInspectorNode(oi, "myProxyGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myProxyGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myProxyGetter");
  ok(
    node.textContent.includes(`myProxyGetter: Proxy`),
    "proxy getter now has the expected text content"
  );
  is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded");

  await expandObjectInspectorNode(node);
  checkChildren(node, [`<target>`, `<handler>`]);

  const targetNode = findObjectInspectorNode(oi, "<target>");
  await expandObjectInspectorNode(targetNode);
  checkChildren(targetNode, [`a: 1`, `<prototype>`]);

  const handlerNode = findObjectInspectorNode(oi, "<handler>");
  await expandObjectInspectorNode(handlerNode);
  checkChildren(handlerNode, [`get:`, `<prototype>`]);
}

async function testThrowingGetter(oi) {
  let node = findObjectInspectorNode(oi, "myThrowingGetter");
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () =>
      !getObjectInspectorInvokeGetterButton(
        findObjectInspectorNode(oi, "myThrowingGetter")
      )
  );

  node = findObjectInspectorNode(oi, "myThrowingGetter");
  ok(
    node.textContent.includes(`myThrowingGetter: Error`),
    "throwing getter does show the error"
  );
  is(isObjectInspectorNodeExpandable(node), true, "The node can be expanded");

  await expandObjectInspectorNode(node);
  await waitFor(() => !!getObjectInspectorChildrenNodes(node).length);
  checkChildren(node, [
    `columnNumber`,
    `fileName`,
    `lineNumber`,
    `message`,
    `stack`,
    `<prototype>`,
  ]);
}

async function testLongStringGetter(oi, longString) {
  const getLongStringNode = () =>
    findObjectInspectorNode(oi, "myLongStringGetter");
  const node = getLongStringNode();
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(() =>
    getLongStringNode().textContent.includes(`myLongStringGetter: "ab ab`)
  );
  ok(true, "longstring getter shows the initial text");
  is(
    isObjectInspectorNodeExpandable(getLongStringNode()),
    true,
    "The node can be expanded"
  );

  await expandObjectInspectorNode(getLongStringNode());
  await waitFor(() =>
    getLongStringNode().textContent.includes(
      `myLongStringGetter: "${longString}"`
    )
  );
  ok(true, "the longstring was expanded");
}

async function testHypgenGetter(oi) {
  const findHyphenGetterNode = () =>
    findObjectInspectorNode(oi, `"hyphen-getter"`);
  let node = findHyphenGetterNode();

  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
  const invokeButton = getObjectInspectorInvokeGetterButton(node);
  ok(invokeButton, "There is an invoke button as expected");

  invokeButton.click();
  await waitFor(
    () => !getObjectInspectorInvokeGetterButton(findHyphenGetterNode())
  );

  node = findHyphenGetterNode();
  ok(
    node.textContent.includes(`"hyphen-getter": "---"`),
    "Node now has the expected text content"
  );
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    "The node can't be expanded"
  );
}

async function testQuotedGetters(oi) {
  const nodes = [
    {
      name: `'"quoted-getter"'`,
      expected: `"quoted"`,
      expandable: false,
    },
    {
      name: `"\\"'\`"`,
      expected: `"quoted2"`,
      expandable: false,
    },
  ];

  for (const { name, expected, expandable } of nodes) {
    await testGetter(oi, name, expected, expandable);
  }
}

async function testGetter(oi, propertyName, expectedResult, resultExpandable) {
  info(`Check «${propertyName}» getter`);
  const findNode = () => findObjectInspectorNode(oi, propertyName);

  let node = findNode();
  is(
    isObjectInspectorNodeExpandable(node),
    false,
    `«${propertyName}» can't be expanded`
  );
  getObjectInspectorInvokeGetterButton(node).click();
  await waitFor(() => !getObjectInspectorInvokeGetterButton(findNode()));

  node = findNode();
  ok(
    node.textContent.includes(`${propertyName}: ${expectedResult}`),
    `«${propertyName}» now has the expected text content («${expectedResult}»)`
  );
  is(
    isObjectInspectorNodeExpandable(node),
    resultExpandable,
    `«${propertyName}» ${resultExpandable ? "now can" : "can't"} be expanded`
  );
}

function checkChildren(node, expectedChildren) {
  const children = getObjectInspectorChildrenNodes(node);
  is(
    children.length,
    expectedChildren.length,
    "There is the expected number of children"
  );
  children.forEach((child, index) => {
    ok(
      child.textContent.includes(expectedChildren[index]),
      `Expected "${child.textContent}" to include "${expectedChildren[index]}"`
    );
  });
}
