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

"use strict";

const { JSONHandler } = ChromeUtils.importESModule(
  "chrome://remote/content/cdp/JSONHandler.sys.mjs"
);

// Get list of supported routes from JSONHandler
const routes = new JSONHandler().routes;

add_task(async function json_version() {
  const { userAgent } = Cc[
    "@mozilla.org/network/protocol;1?name=http"
  ].getService(Ci.nsIHttpProtocolHandler);

  const json = await requestJSON("/json/version");
  is(
    json.Browser,
    `${Services.appinfo.name}/${Services.appinfo.version}`,
    "Browser name and version found"
  );
  is(json["Protocol-Version"], "1.3", "Protocol version found");
  is(json["User-Agent"], userAgent, "User agent found");
  is(json["V8-Version"], "1.0", "V8 version found");
  is(json["WebKit-Version"], "1.0", "Webkit version found");
  is(
    json.webSocketDebuggerUrl,
    RemoteAgent.cdp.targetList.getMainProcessTarget().wsDebuggerURL,
    "Websocket URL for main process target found"
  );
});

add_task(async function check_routes() {
  for (const route in routes) {
    const { parameter, method } = routes[route];
    // Skip routes expecting parameter
    if (parameter) {
      continue;
    }

    // Check request succeeded (200) and responded with valid JSON
    info(`Checking ${route}`);
    await requestJSON(route, { method });

    // Check with trailing slash
    info(`Checking ${route + "/"}`);
    await requestJSON(route + "/", { method });

    // Test routes expecting a certain method
    if (method) {
      const responseText = await requestJSON(route, {
        method: "DELETE",
        status: 405,
        json: false,
      });
      is(
        responseText,
        `Using unsafe HTTP verb DELETE to invoke ${route}. This action supports only ${method} verb.`,
        "/json/new fails with 405 when using GET"
      );
    }
  }
});

add_task(async function json_list({ client }) {
  const { Target } = client;
  const { targetInfos } = await Target.getTargets();

  const json = await requestJSON("/json/list");
  const jsonAlias = await requestJSON("/json");

  Assert.deepEqual(json, jsonAlias, "/json/list and /json return the same");

  ok(Array.isArray(json), "Target list is an array");

  is(
    json.length,
    targetInfos.length,
    "Targets as listed on /json/list are equal to Target.getTargets"
  );

  for (let i = 0; i < json.length; i++) {
    const jsonTarget = json[i];
    const wsTarget = targetInfos[i];

    is(
      jsonTarget.id,
      wsTarget.targetId,
      "Target id matches between HTTP and Target.getTargets"
    );
    is(
      jsonTarget.type,
      wsTarget.type,
      "Target type matches between HTTP and Target.getTargets"
    );
    is(
      jsonTarget.url,
      wsTarget.url,
      "Target url matches between HTTP and Target.getTargets"
    );

    // Ensure expected values specifically for JSON endpoint
    // and that type is always "page" as main process target should not be included
    is(
      jsonTarget.type,
      "page",
      `Target (${jsonTarget.id}) from list has expected type (page)`
    );
    is(
      jsonTarget.webSocketDebuggerUrl,
      `ws://${RemoteAgent.debuggerAddress}/devtools/page/${wsTarget.targetId}`,
      `Target (${jsonTarget.id}) from list has expected webSocketDebuggerUrl value`
    );
  }
});

add_task(async function json_new_target({ client }) {
  const newUrl = "https://example.com";

  let getError = await requestJSON("/json/new?" + newUrl, {
    status: 405,
    json: false,
  });
  is(
    getError,
    "Using unsafe HTTP verb GET to invoke /json/new. This action supports only PUT verb.",
    "/json/new fails with 405 when using GET"
  );

  const newTarget = await requestJSON("/json/new?" + newUrl, { method: "PUT" });

  is(newTarget.type, "page", "Returned target type is 'page'");
  is(newTarget.url, newUrl, "Returned target URL matches");
  ok(!!newTarget.id, "Returned target has id");
  ok(
    !!newTarget.webSocketDebuggerUrl,
    "Returned target has webSocketDebuggerUrl"
  );

  const { Target } = client;
  const targets = await getDiscoveredTargets(Target);
  const foundTarget = targets.find(target => target.targetId === newTarget.id);

  ok(!!foundTarget, "Returned target id was found");
});

add_task(async function json_activate_target({ client, tab }) {
  const { Target, target } = client;

  const currentTargetId = target.id;
  const targets = await getDiscoveredTargets(Target);
  const initialTarget = targets.find(
    target => target.targetId === currentTargetId
  );
  ok(!!initialTarget, "The current target has been found");

  // open some more tabs in the initial window
  await openTab(Target);
  await openTab(Target);

  const lastTabFirstWindow = await openTab(Target);
  is(
    gBrowser.selectedTab,
    lastTabFirstWindow.newTab,
    "Selected tab has changed to a new tab"
  );

  const activateResponse = await requestJSON(
    "/json/activate/" + initialTarget.targetId,
    { json: false }
  );

  is(
    activateResponse,
    "Target activated",
    "Activate endpoint returned expected string"
  );

  is(gBrowser.selectedTab, tab, "Selected tab is the initial tab again");

  const invalidResponse = await requestJSON("/json/activate/does-not-exist", {
    status: 404,
    json: false,
  });

  is(invalidResponse, "No such target id: does-not-exist");
});

add_task(async function json_close_target({ CDP, client, tab }) {
  const { Target } = client;

  const { targetInfo, newTab } = await openTab(Target);

  const targetListBefore = await CDP.List();
  const beforeTarget = targetListBefore.find(
    target => target.id === targetInfo.targetId
  );

  ok(!!beforeTarget, "New target has been found");

  const tabClosed = BrowserTestUtils.waitForEvent(newTab, "TabClose");
  const targetDestroyed = Target.targetDestroyed();

  const activateResponse = await requestJSON(
    "/json/close/" + targetInfo.targetId,
    { json: false }
  );
  is(
    activateResponse,
    "Target is closing",
    "Close endpoint returned expected string"
  );

  await tabClosed;
  info("Tab was closed");

  await targetDestroyed;
  info("Received the Target.targetDestroyed event");

  const targetListAfter = await CDP.List();
  const afterTarget = targetListAfter.find(
    target => target.id === targetInfo.targetId
  );

  ok(afterTarget == null, "New target is gone");

  const invalidResponse = await requestJSON("/json/close/does-not-exist", {
    status: 404,
    json: false,
  });

  is(invalidResponse, "No such target id: does-not-exist");
});

add_task(async function json_prevent_load_in_iframe({ client }) {
  const { Page } = client;

  const PAGE = `https://example.com/document-builder.sjs?html=${encodeURIComponent(
    '<iframe src="http://localhost:9222/json/version"></iframe>`'
  )}`;

  await Page.enable();

  const NAVIGATED = "Page.frameNavigated";

  const history = new RecordEvents(2);
  history.addRecorder({
    event: Page.frameNavigated,
    eventName: NAVIGATED,
    messageFn: payload => {
      return `Received ${NAVIGATED} for frame id ${payload.frame.id}`;
    },
  });

  await loadURL(PAGE);

  const frameNavigatedEvents = await history.record();

  const frames = frameNavigatedEvents
    .map(({ payload }) => payload.frame)
    .filter(frame => frame.parentId !== undefined);

  const windowGlobal = BrowsingContext.get(frames[0].id).currentWindowGlobal;
  ok(
    windowGlobal.documentURI.spec.startsWith("about:neterror?e=cspBlocked"),
    "Expected page not be loaded within an iframe"
  );
});

async function requestJSON(path, options = {}) {
  const { method = "GET", status = 200, json = true } = options;

  info(`${method} http://${RemoteAgent.debuggerAddress}${path}`);

  const response = await fetch(`http://${RemoteAgent.debuggerAddress}${path}`, {
    method,
  });

  is(response.status, status, `JSON response is ${status}`);

  if (json) {
    return response.json();
  }

  return response.text();
}
