/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
const {gDevTools} = require("devtools/client/framework/devtools");

/**
 * Helper that returns the id of the last additional/extension tool for a provided
 * toolbox.
 *
 * @param {Object} toolbox
 *        The DevTools toolbox object.
 * @param {string} label
 *        The expected label for the additional tool.
 * @returns {string} the id of the last additional panel.
 */
function getAdditionalPanelId(toolbox, label) {
  // Copy the tools array and pop the last element from it.
  const panelDef = toolbox.getAdditionalTools().slice().pop();
  is(panelDef.label, label, "Additional panel label is the expected label");
  return panelDef.id;
}

/**
 * this test file ensures that:
 *
 * - the devtools page gets only a subset of the runtime API namespace.
 * - devtools.inspectedWindow.tabId is the same tabId that we can retrieve
 *   in the background page using the tabs API namespace.
 * - devtools API is available in the devtools page sub-frames when a valid
 *   extension URL has been loaded.
 * - devtools.inspectedWindow.eval:
 *   - returns a serialized version of the evaluation result.
 *   - returns the expected error object when the return value serialization raises a
 *     "TypeError: cyclic object value" exception.
 *   - returns the expected exception when an exception has been raised from the evaluated
 *     javascript code.
 */
add_task(async function test_devtools_inspectedWindow_tabId() {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");

  async function background() {
    browser.test.assertEq(undefined, browser.devtools,
                          "No devtools APIs should be available in the background page");

    const tabs = await browser.tabs.query({active: true, lastFocusedWindow: true});
    browser.test.sendMessage("current-tab-id", tabs[0].id);
  }

  function devtools_page() {
    browser.test.assertEq(
      undefined, browser.runtime.getBackgroundPage,
      "The `runtime.getBackgroundPage` API method should be missing in a devtools_page context");

    try {
      let tabId = browser.devtools.inspectedWindow.tabId;
      browser.test.sendMessage("inspectedWindow-tab-id", tabId);
    } catch (err) {
      browser.test.sendMessage("inspectedWindow-tab-id", undefined);
      throw err;
    }
  }

  function devtools_page_iframe() {
    try {
      let tabId = browser.devtools.inspectedWindow.tabId;
      browser.test.sendMessage("devtools_page_iframe.inspectedWindow-tab-id", tabId);
    } catch (err) {
      browser.test.fail(`Error: ${err} :: ${err.stack}`);
      browser.test.sendMessage("devtools_page_iframe.inspectedWindow-tab-id", undefined);
    }
  }

  let extension = ExtensionTestUtils.loadExtension({
    background,
    manifest: {
      devtools_page: "devtools_page.html",
    },
    files: {
      "devtools_page.html": `<!DOCTYPE html>
      <html>
       <head>
         <meta charset="utf-8">
       </head>
       <body>
         <iframe src="/devtools_page_iframe.html"></iframe>
         <script src="devtools_page.js"></script>
       </body>
      </html>`,
      "devtools_page.js": devtools_page,
      "devtools_page_iframe.html": `<!DOCTYPE html>
      <html>
       <head>
         <meta charset="utf-8">
       </head>
       <body>
         <script src="devtools_page_iframe.js"></script>
       </body>
      </html>`,
      "devtools_page_iframe.js": devtools_page_iframe,
    },
  });

  await extension.startup();

  let backgroundPageCurrentTabId = await extension.awaitMessage("current-tab-id");

  let target = gDevTools.getTargetForTab(tab);

  await gDevTools.showToolbox(target, "webconsole");
  info("developer toolbox opened");

  let devtoolsInspectedWindowTabId = await extension.awaitMessage("inspectedWindow-tab-id");

  is(devtoolsInspectedWindowTabId, backgroundPageCurrentTabId,
     "Got the expected tabId from devtool.inspectedWindow.tabId");

  let devtoolsPageIframeTabId = await extension.awaitMessage("devtools_page_iframe.inspectedWindow-tab-id");

  is(devtoolsPageIframeTabId, backgroundPageCurrentTabId,
     "Got the expected tabId from devtool.inspectedWindow.tabId called in a devtool_page iframe");

  await gDevTools.closeToolbox(target);

  await target.destroy();

  await extension.unload();

  BrowserTestUtils.removeTab(tab);
});

add_task(async function test_devtools_inspectedWindow_eval() {
  const TEST_TARGET_URL = "http://mochi.test:8888/";
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_TARGET_URL);

  function devtools_page() {
    browser.test.onMessage.addListener(async (msg, ...args) => {
      if (msg !== "inspectedWindow-eval-request") {
        browser.test.fail(`Unexpected test message received: ${msg}`);
        return;
      }

      try {
        const [evalResult, errorResult] = await browser.devtools.inspectedWindow.eval(...args);
        browser.test.sendMessage("inspectedWindow-eval-result", {
          evalResult,
          errorResult,
        });
      } catch (err) {
        browser.test.sendMessage("inspectedWindow-eval-result");
        browser.test.fail(`Error: ${err} :: ${err.stack}`);
      }
    });
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      devtools_page: "devtools_page.html",
    },
    files: {
      "devtools_page.html": `<!DOCTYPE html>
      <html>
       <head>
         <meta charset="utf-8">
         <script text="text/javascript" src="devtools_page.js"></script>
       </head>
       <body>
       </body>
      </html>`,
      "devtools_page.js": devtools_page,
    },
  });

  await extension.startup();

  let target = gDevTools.getTargetForTab(tab);

  await gDevTools.showToolbox(target, "webconsole");
  info("developer toolbox opened");

  const evalTestCases = [
    // Successful evaluation results.
    {
      args: ["window.location.href"],
      expectedResults: {evalResult: TEST_TARGET_URL, errorResult: undefined},
    },

    // Error evaluation results.
    {
      args: ["window"],
      expectedResults: {
        evalResult: undefined,
        errorResult: {
          isError: true,
          code: "E_PROTOCOLERROR",
          description: "Inspector protocol error: %s",
          details: [
            "TypeError: cyclic object value",
          ],
        },
      },
    },

    // Exception evaluation results.
    {
      args: ["throw new Error('fake eval exception');"],
      expectedResults: {
        evalResult: undefined,
        errorResult: {
          isException: true,
          value: /Error: fake eval exception\n.*moz-extension:\/\//,
        },
      },

    },
  ];

  for (let testCase of evalTestCases) {
    info(`test inspectedWindow.eval with ${JSON.stringify(testCase)}`);

    const {args, expectedResults} = testCase;

    extension.sendMessage(`inspectedWindow-eval-request`, ...args);

    const {evalResult, errorResult} = await extension.awaitMessage(`inspectedWindow-eval-result`);

    Assert.deepEqual(evalResult, expectedResults.evalResult, "Got the expected eval result");

    if (errorResult) {
      for (const errorPropName of Object.keys(expectedResults.errorResult)) {
        const expected = expectedResults.errorResult[errorPropName];
        const actual = errorResult[errorPropName];

        if (expected instanceof RegExp) {
          ok(expected.test(actual),
             `Got exceptionInfo.${errorPropName} value ${actual} matches ${expected}`);
        } else {
          Assert.deepEqual(actual, expected,
                           `Got the expected exceptionInfo.${errorPropName} value`);
        }
      }
    }
  }

  await gDevTools.closeToolbox(target);

  await target.destroy();

  await extension.unload();

  BrowserTestUtils.removeTab(tab);
});

/**
 * This test asserts that both the page and the panel can use devtools.inspectedWindow.
 * See regression in Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1392531
 */
add_task(async function test_devtools_inspectedWindow_eval_in_page_and_panel() {
  const TEST_TARGET_URL = "http://mochi.test:8888/";
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_TARGET_URL);

  async function devtools_page() {
    await browser.devtools.panels.create("test-eval", "fake-icon.png", "devtools_panel.html");

    browser.test.onMessage.addListener(async (msg, ...args) => {
      switch (msg) {
        case "inspectedWindow-page-eval-request": {
          const [evalResult, errorResult] = await browser.devtools.inspectedWindow.eval(...args);
          browser.test.sendMessage("inspectedWindow-page-eval-result", {
            evalResult,
            errorResult,
          });
          break;
        }
        case "inspectedWindow-panel-eval-request":
          // Ignore the test message expected by the devtools panel.
          break;
        default:
          browser.test.fail(`Unexpected test message received: ${msg}`);
      }
    });

    browser.test.sendMessage("devtools_panel_created");
  }

  function devtools_panel() {
    browser.test.onMessage.addListener(async (msg, ...args) => {
      switch (msg) {
        case "inspectedWindow-panel-eval-request": {
          const [evalResult, errorResult] = await browser.devtools.inspectedWindow.eval(...args);
          browser.test.sendMessage("inspectedWindow-panel-eval-result", {
            evalResult,
            errorResult,
          });
          break;
        }
        case "inspectedWindow-page-eval-request":
          // Ignore the test message expected by the devtools page.
          break;
        default:
          browser.test.fail(`Unexpected test message received: ${msg}`);
      }
    });
    browser.test.sendMessage("devtools_panel_initialized");
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      devtools_page: "devtools_page.html",
    },
    files: {
      "devtools_page.html": `<!DOCTYPE html>
      <html>
       <head>
         <meta charset="utf-8">
         <script text="text/javascript" src="devtools_page.js"></script>
       </head>
       <body>
       </body>
      </html>`,
      "devtools_page.js": devtools_page,
      "devtools_panel.html":  `<!DOCTYPE html>
      <html>
       <head>
         <meta charset="utf-8">
       </head>
       <body>
         DEVTOOLS PANEL
         <script src="devtools_panel.js"></script>
       </body>
      </html>`,
      "devtools_panel.js": devtools_panel,
    },
  });

  await extension.startup();

  const target = gDevTools.getTargetForTab(tab);
  const toolbox = await gDevTools.showToolbox(target, "webconsole");
  info("developer toolbox opened");

  info("Wait for devtools_panel_created event");
  await extension.awaitMessage("devtools_panel_created");

  info("Switch to the extension test panel");
  await gDevTools.showToolbox(target, getAdditionalPanelId(toolbox, "test-eval"));

  info("Wait for devtools_panel_initialized event");
  await extension.awaitMessage("devtools_panel_initialized");

  info(`test inspectedWindow.eval with eval(window.location.href) from the devtools page`);
  extension.sendMessage(`inspectedWindow-page-eval-request`, "window.location.href");

  info("Wait for response from the page");
  let {evalResult} = await extension.awaitMessage(`inspectedWindow-page-eval-result`);
  Assert.deepEqual(evalResult, TEST_TARGET_URL, "Got the expected eval result in the page");

  info(`test inspectedWindow.eval with eval(window.location.href) from the devtools panel`);
  extension.sendMessage(`inspectedWindow-panel-eval-request`, "window.location.href");

  info("Wait for response from the panel");
  ({evalResult} = await extension.awaitMessage(`inspectedWindow-panel-eval-result`));
  Assert.deepEqual(evalResult, TEST_TARGET_URL, "Got the expected eval result in the panel");

  // Cleanup
  await gDevTools.closeToolbox(target);
  await target.destroy();
  await extension.unload();
  BrowserTestUtils.removeTab(tab);
});
