/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
 http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

ChromeUtils.defineModuleGetter(this, "ExtensionParent",
                               "resource://gre/modules/ExtensionParent.jsm");

const {WebExtensionPolicy} = ExtensionParent;

const {
  WebExtensionInspectedWindowFront
} = require("devtools/shared/fronts/webextension-inspected-window");

const TEST_RELOAD_URL = `${MAIN_DOMAIN}/inspectedwindow-reload-target.sjs`;

async function setup(pageUrl) {
  const extension = ExtensionTestUtils.loadExtension({
    background() {
      // This is just an empty extension used to ensure that the caller extension uuid
      // actually exists.
    }
  });

  await extension.startup();

  const fakeExtCallerInfo = {
    url: WebExtensionPolicy.getByID(extension.id).getURL("fake-caller-script.js"),
    lineNumber: 1,
    addonId: extension.id,
  };

  await addTab(pageUrl);
  initDebuggerServer();

  const client = new DebuggerClient(DebuggerServer.connectPipe());
  const form = await connectDebuggerClient(client);

  const [, tabClient] = await client.attachTab(form.actor);

  const [, consoleClient] = await client.attachConsole(form.consoleActor, []);

  const inspectedWindowFront = new WebExtensionInspectedWindowFront(client, form);

  return {
    client, form,
    tabClient, consoleClient,
    inspectedWindowFront,
    extension, fakeExtCallerInfo,
  };
}

async function teardown({client, extension}) {
  await client.close();
  DebuggerServer.destroy();
  gBrowser.removeCurrentTab();
  await extension.unload();
}

function waitForNextTabNavigated(client) {
  return new Promise(resolve => {
    client.addListener("tabNavigated", function tabNavigatedListener(evt, pkt) {
      if (pkt.state == "stop" && !pkt.isFrameSwitching) {
        client.removeListener("tabNavigated", tabNavigatedListener);
        resolve();
      }
    });
  });
}

function consoleEvalJS(consoleClient, jsCode) {
  return new Promise(resolve => {
    consoleClient.evaluateJS(jsCode, resolve);
  });
}

// Script used as the injectedScript option in the inspectedWindow.reload tests.
function injectedScript() {
  if (!window.pageScriptExecutedFirst) {
    window.addEventListener("DOMContentLoaded", function () {
      if (document.querySelector("pre")) {
        document.querySelector("pre").textContent = "injected script executed first";
      }
    }, {once: true});
  }
}

// Script evaluated in the target tab, to collect the results of injectedScript
// evaluation in the inspectedWindow.reload tests.
function collectEvalResults() {
  let results = [];
  let iframeDoc = document;

  while (iframeDoc) {
    if (iframeDoc.querySelector("pre")) {
      results.push(iframeDoc.querySelector("pre").textContent);
    }
    const iframe = iframeDoc.querySelector("iframe");
    iframeDoc = iframe ? iframe.contentDocument : null;
  }
  return JSON.stringify(results);
}

add_task(async function test_successfull_inspectedWindowEval_result() {
  const {
    client, inspectedWindowFront,
    extension, fakeExtCallerInfo,
  } = await setup(MAIN_DOMAIN);

  const result = await inspectedWindowFront.eval(fakeExtCallerInfo,
                                                 "window.location", {});

  ok(result.value, "Got a result from inspectedWindow eval");
  is(result.value.href, MAIN_DOMAIN,
     "Got the expected window.location.href property value");
  is(result.value.protocol, "http:",
     "Got the expected window.location.protocol property value");

  await teardown({client, extension});
});

add_task(async function test_successfull_inspectedWindowEval_resultAsGrip() {
  const {
    client, inspectedWindowFront, form,
    extension, fakeExtCallerInfo,
  } = await setup(MAIN_DOMAIN);

  let result = await inspectedWindowFront.eval(fakeExtCallerInfo, "window", {
    evalResultAsGrip: true,
    toolboxConsoleActorID: form.consoleActor
  });

  ok(result.valueGrip, "Got a result from inspectedWindow eval");
  ok(result.valueGrip.actor, "Got a object actor as expected");
  is(result.valueGrip.type, "object", "Got a value grip of type object");
  is(result.valueGrip.class, "Window", "Got a value grip which is instanceof Location");

  // Test invalid evalResultAsGrip request.
  result = await inspectedWindowFront.eval(
    fakeExtCallerInfo, "window", {evalResultAsGrip: true}
  );

  ok(!result.value && !result.valueGrip,
     "Got a null result from the invalid inspectedWindow eval call");
  ok(result.exceptionInfo.isError, "Got an API Error result from inspectedWindow eval");
  ok(!result.exceptionInfo.isException, "An error isException is false as expected");
  is(result.exceptionInfo.code, "E_PROTOCOLERROR",
     "Got the expected 'code' property in the error result");
  is(result.exceptionInfo.description, "Inspector protocol error: %s - %s",
     "Got the expected 'description' property in the error result");
  is(result.exceptionInfo.details.length, 2,
     "The 'details' array property should contains 1 element");
  is(result.exceptionInfo.details[0],
     "Unexpected invalid sidebar panel expression request",
     "Got the expected content in the error results's details");
  is(result.exceptionInfo.details[1],
     "missing toolboxConsoleActorID",
     "Got the expected content in the error results's details");

  await teardown({client, extension});
});

add_task(async function test_error_inspectedWindowEval_result() {
  const {
    client, inspectedWindowFront,
    extension, fakeExtCallerInfo,
  } = await setup(MAIN_DOMAIN);

  const result = await inspectedWindowFront.eval(fakeExtCallerInfo, "window", {});

  ok(!result.value, "Got a null result from inspectedWindow eval");
  ok(result.exceptionInfo.isError, "Got an API Error result from inspectedWindow eval");
  ok(!result.exceptionInfo.isException, "An error isException is false as expected");
  is(result.exceptionInfo.code, "E_PROTOCOLERROR",
     "Got the expected 'code' property in the error result");
  is(result.exceptionInfo.description, "Inspector protocol error: %s",
     "Got the expected 'description' property in the error result");
  is(result.exceptionInfo.details.length, 1,
     "The 'details' array property should contains 1 element");
  ok(result.exceptionInfo.details[0].includes("cyclic object value"),
     "Got the expected content in the error results's details");

  await teardown({client, extension});
});

add_task(async function test_system_principal_denied_error_inspectedWindowEval_result() {
  const {
    client, inspectedWindowFront,
    extension, fakeExtCallerInfo,
  } = await setup("about:addons");

  const result = await inspectedWindowFront.eval(fakeExtCallerInfo, "window", {});

  ok(!result.value, "Got a null result from inspectedWindow eval");
  ok(result.exceptionInfo.isError,
     "Got an API Error result from inspectedWindow eval on a system principal page");
  is(result.exceptionInfo.code, "E_PROTOCOLERROR",
     "Got the expected 'code' property in the error result");
  is(result.exceptionInfo.description, "Inspector protocol error: %s",
     "Got the expected 'description' property in the error result");
  is(result.exceptionInfo.details.length, 1,
     "The 'details' array property should contains 1 element");
  is(result.exceptionInfo.details[0],
     "This target has a system principal. inspectedWindow.eval denied.",
     "Got the expected content in the error results's details");

  await teardown({client, extension});
});

add_task(async function test_exception_inspectedWindowEval_result() {
  const {
    client, inspectedWindowFront,
    extension, fakeExtCallerInfo,
  } = await setup(MAIN_DOMAIN);

  const result = await inspectedWindowFront.eval(
    fakeExtCallerInfo, "throw Error('fake eval error');", {});

  ok(result.exceptionInfo.isException, "Got an exception as expected");
  ok(!result.value, "Got an undefined eval value");
  ok(!result.exceptionInfo.isError, "An exception should not be isError=true");
  ok(result.exceptionInfo.value.includes("Error: fake eval error"),
     "Got the expected exception message");

  const expectedCallerInfo =
    `called from ${fakeExtCallerInfo.url}:${fakeExtCallerInfo.lineNumber}`;
  ok(result.exceptionInfo.value.includes(expectedCallerInfo),
     "Got the expected caller info in the exception message");

  const expectedStack = `eval code:1:7`;
  ok(result.exceptionInfo.value.includes(expectedStack),
     "Got the expected stack trace in the exception message");

  await teardown({client, extension});
});

add_task(async function test_exception_inspectedWindowReload() {
  const {
    client, consoleClient, inspectedWindowFront,
    extension, fakeExtCallerInfo,
  } = await setup(`${TEST_RELOAD_URL}?test=cache`);

  // Test reload with bypassCache=false.

  const waitForNoBypassCacheReload = waitForNextTabNavigated(client);
  const reloadResult = await inspectedWindowFront.reload(fakeExtCallerInfo,
                                                         {ignoreCache: false});

  ok(!reloadResult, "Got the expected undefined result from inspectedWindow reload");

  await waitForNoBypassCacheReload;

  const noBypassCacheEval = await consoleEvalJS(consoleClient,
                                                "document.body.textContent");

  is(noBypassCacheEval.result, "empty cache headers",
     "Got the expected result with reload forceBypassCache=false");

  // Test reload with bypassCache=true.

  const waitForForceBypassCacheReload = waitForNextTabNavigated(client);
  await inspectedWindowFront.reload(fakeExtCallerInfo, {ignoreCache: true});

  await waitForForceBypassCacheReload;

  const forceBypassCacheEval = await consoleEvalJS(consoleClient,
                                                   "document.body.textContent");

  is(forceBypassCacheEval.result, "no-cache:no-cache",
     "Got the expected result with reload forceBypassCache=true");

  await teardown({client, extension});
});

add_task(async function test_exception_inspectedWindowReload_customUserAgent() {
  const {
    client, consoleClient, inspectedWindowFront,
    extension, fakeExtCallerInfo,
  } = await setup(`${TEST_RELOAD_URL}?test=user-agent`);

  // Test reload with custom userAgent.

  const waitForCustomUserAgentReload = waitForNextTabNavigated(client);
  await inspectedWindowFront.reload(fakeExtCallerInfo,
                                    {userAgent: "Customized User Agent"});

  await waitForCustomUserAgentReload;

  const customUserAgentEval = await consoleEvalJS(consoleClient,
                                                  "document.body.textContent");

  is(customUserAgentEval.result, "Customized User Agent",
     "Got the expected result on reload with a customized userAgent");

  // Test reload with no custom userAgent.

  const waitForNoCustomUserAgentReload = waitForNextTabNavigated(client);
  await inspectedWindowFront.reload(fakeExtCallerInfo, {});

  await waitForNoCustomUserAgentReload;

  const noCustomUserAgentEval = await consoleEvalJS(consoleClient,
                                                    "document.body.textContent");

  is(noCustomUserAgentEval.result, window.navigator.userAgent,
     "Got the expected result with reload without a customized userAgent");

  await teardown({client, extension});
});

add_task(async function test_exception_inspectedWindowReload_injectedScript() {
  const {
    client, consoleClient, inspectedWindowFront,
    extension, fakeExtCallerInfo,
  } = await setup(`${TEST_RELOAD_URL}?test=injected-script&frames=3`);

  // Test reload with an injectedScript.

  const waitForInjectedScriptReload = waitForNextTabNavigated(client);
  await inspectedWindowFront.reload(fakeExtCallerInfo,
                                    {injectedScript: `new ${injectedScript}`});
  await waitForInjectedScriptReload;

  const injectedScriptEval = await consoleEvalJS(consoleClient,
                                                 `(${collectEvalResults})()`);

  const expectedResult = (new Array(5)).fill("injected script executed first");

  SimpleTest.isDeeply(JSON.parse(injectedScriptEval.result), expectedResult,
     "Got the expected result on reload with an injected script");

  // Test reload without an injectedScript.

  const waitForNoInjectedScriptReload = waitForNextTabNavigated(client);
  await inspectedWindowFront.reload(fakeExtCallerInfo, {});
  await waitForNoInjectedScriptReload;

  const noInjectedScriptEval = await consoleEvalJS(consoleClient,
                                                   `(${collectEvalResults})()`);

  const newExpectedResult = (new Array(5)).fill("injected script NOT executed");

  SimpleTest.isDeeply(JSON.parse(noInjectedScriptEval.result), newExpectedResult,
                      "Got the expected result on reload with no injected script");

  await teardown({client, extension});
});

add_task(async function test_exception_inspectedWindowReload_multiple_calls() {
  const {
    client, consoleClient, inspectedWindowFront,
    extension, fakeExtCallerInfo,
  } = await setup(`${TEST_RELOAD_URL}?test=user-agent`);

  // Test reload with custom userAgent three times (and then
  // check that only the first one has affected the page reload.

  const waitForCustomUserAgentReload = waitForNextTabNavigated(client);

  inspectedWindowFront.reload(fakeExtCallerInfo, {userAgent: "Customized User Agent 1"});
  inspectedWindowFront.reload(fakeExtCallerInfo, {userAgent: "Customized User Agent 2"});

  await waitForCustomUserAgentReload;

  const customUserAgentEval = await consoleEvalJS(consoleClient,
                                                  "document.body.textContent");

  is(customUserAgentEval.result, "Customized User Agent 1",
     "Got the expected result on reload with a customized userAgent");

  // Test reload with no custom userAgent.

  const waitForNoCustomUserAgentReload = waitForNextTabNavigated(client);
  await inspectedWindowFront.reload(fakeExtCallerInfo, {});

  await waitForNoCustomUserAgentReload;

  const noCustomUserAgentEval = await consoleEvalJS(consoleClient,
                                                    "document.body.textContent");

  is(noCustomUserAgentEval.result, window.navigator.userAgent,
     "Got the expected result with reload without a customized userAgent");

  await teardown({client, extension});
});

add_task(async function test_exception_inspectedWindowReload_stopped() {
  const {
    client, consoleClient, inspectedWindowFront,
    extension, fakeExtCallerInfo,
  } = await setup(`${TEST_RELOAD_URL}?test=injected-script&frames=3`);

  // Test reload on a page that calls window.stop() immediately during the page loading

  const waitForPageLoad = waitForNextTabNavigated(client);
  await inspectedWindowFront.eval(fakeExtCallerInfo,
                                  "window.location += '&stop=windowStop'");

  info("Load a webpage that calls 'window.stop()' while is still loading");
  await waitForPageLoad;

  info("Starting a reload with an injectedScript");
  const waitForInjectedScriptReload = waitForNextTabNavigated(client);
  await inspectedWindowFront.reload(fakeExtCallerInfo,
                                    {injectedScript: `new ${injectedScript}`});
  await waitForInjectedScriptReload;

  const injectedScriptEval = await consoleEvalJS(consoleClient,
                                                 `(${collectEvalResults})()`);

  // The page should have stopped during the reload and only one injected script
  // is expected.
  const expectedResult = (new Array(1)).fill("injected script executed first");

  SimpleTest.isDeeply(JSON.parse(injectedScriptEval.result), expectedResult,
     "The injected script has been executed on the 'stopped' page reload");

  // Reload again with no options.

  info("Reload the tab again without any reload options");
  const waitForNoInjectedScriptReload = waitForNextTabNavigated(client);
  await inspectedWindowFront.reload(fakeExtCallerInfo, {});
  await waitForNoInjectedScriptReload;

  const noInjectedScriptEval = await consoleEvalJS(consoleClient,
                                                   `(${collectEvalResults})()`);

  // The page should have stopped during the reload and no injected script should
  // have been executed during this second reload (or it would mean that the previous
  // customized reload was still pending and has wrongly affected the second reload)
  const newExpectedResult = (new Array(1)).fill("injected script NOT executed");

  SimpleTest.isDeeply(
    JSON.parse(noInjectedScriptEval.result), newExpectedResult,
    "No injectedScript should have been evaluated during the second reload"
  );

  await teardown({client, extension});
});

// TODO: check eval with $0 binding once implemented (Bug 1300590)
