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

"use strict";

/**
 * Tests for all expected requests when an iframe is loading a subdocument.
 */

const TOP_FILE_NAME = "html_frame-test-page.html";
const SUB_FILE_NAME = "html_frame-subdocument.html";
const TOP_URL = EXAMPLE_URL + TOP_FILE_NAME;
const SUB_URL = EXAMPLE_URL + SUB_FILE_NAME;

const EXPECTED_REQUESTS_TOP = [
  {
    method: "GET",
    url: TOP_URL,
    causeType: "document",
    causeUri: null,
    stack: true,
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "stylesheet_request",
    causeType: "stylesheet",
    causeUri: TOP_URL,
    stack: false,
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "img_request",
    causeType: "img",
    causeUri: TOP_URL,
    stack: false,
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "xhr_request",
    causeType: "xhr",
    causeUri: TOP_URL,
    stack: [{ fn: "performXhrRequest", file: TOP_FILE_NAME, line: 25 }],
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "fetch_request",
    causeType: "fetch",
    causeUri: TOP_URL,
    stack: [{ fn: "performFetchRequest", file: TOP_FILE_NAME, line: 29 }],
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "promise_fetch_request",
    causeType: "fetch",
    causeUri: TOP_URL,
    stack: [
      { fn: "performPromiseFetchRequest", file: TOP_FILE_NAME, line: 41 },
      {
        fn: null,
        file: TOP_FILE_NAME,
        line: 40,
        asyncCause: "promise callback",
      },
    ],
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "timeout_fetch_request",
    causeType: "fetch",
    causeUri: TOP_URL,
    stack: [
      { fn: "performTimeoutFetchRequest", file: TOP_FILE_NAME, line: 43 },
      {
        fn: "performPromiseFetchRequest",
        file: TOP_FILE_NAME,
        line: 42,
        asyncCause: "setTimeout handler",
      },
    ],
  },
  {
    method: "POST",
    url: EXAMPLE_URL + "beacon_request",
    causeType: "beacon",
    causeUri: TOP_URL,
    stack: [{ fn: "performBeaconRequest", file: TOP_FILE_NAME, line: 33 }],
  },
];

const EXPECTED_REQUESTS_SUB = [
  {
    method: "GET",
    url: SUB_URL,
    causeType: "subdocument",
    causeUri: TOP_URL,
    stack: false,
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "stylesheet_request",
    causeType: "stylesheet",
    causeUri: SUB_URL,
    stack: false,
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "img_request",
    causeType: "img",
    causeUri: SUB_URL,
    stack: false,
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "xhr_request",
    causeType: "xhr",
    causeUri: SUB_URL,
    stack: [{ fn: "performXhrRequest", file: SUB_FILE_NAME, line: 24 }],
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "fetch_request",
    causeType: "fetch",
    causeUri: SUB_URL,
    stack: [{ fn: "performFetchRequest", file: SUB_FILE_NAME, line: 28 }],
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "promise_fetch_request",
    causeType: "fetch",
    causeUri: SUB_URL,
    stack: [
      { fn: "performPromiseFetchRequest", file: SUB_FILE_NAME, line: 40 },
      {
        fn: null,
        file: SUB_FILE_NAME,
        line: 39,
        asyncCause: "promise callback",
      },
    ],
  },
  {
    method: "GET",
    url: EXAMPLE_URL + "timeout_fetch_request",
    causeType: "fetch",
    causeUri: SUB_URL,
    stack: [
      { fn: "performTimeoutFetchRequest", file: SUB_FILE_NAME, line: 42 },
      {
        fn: "performPromiseFetchRequest",
        file: SUB_FILE_NAME,
        line: 41,
        asyncCause: "setTimeout handler",
      },
    ],
  },
  {
    method: "POST",
    url: EXAMPLE_URL + "beacon_request",
    causeType: "beacon",
    causeUri: SUB_URL,
    stack: [{ fn: "performBeaconRequest", file: SUB_FILE_NAME, line: 32 }],
  },
];

const REQUEST_COUNT =
  EXPECTED_REQUESTS_TOP.length + EXPECTED_REQUESTS_SUB.length;

add_task(async function() {
  // the initNetMonitor function clears the network request list after the
  // page is loaded. That's why we first load a bogus page from SIMPLE_URL,
  // and only then load the real thing from TOP_URL - we want to catch
  // all the requests the page is making, not only the XHRs.
  // We can't use about:blank here, because initNetMonitor checks that the
  // page has actually made at least one request.
  const { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 });

  const { document, store, windowRequire, connector } = monitor.panelWin;
  const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
  const { getDisplayedRequests, getSortedRequests } = windowRequire(
    "devtools/client/netmonitor/src/selectors/index"
  );

  store.dispatch(Actions.batchEnable(false));

  await navigateTo(TOP_URL);

  await waitForNetworkEvents(monitor, REQUEST_COUNT);

  is(
    store.getState().requests.requests.length,
    REQUEST_COUNT,
    "All the page events should be recorded."
  );

  // Fetch stack-trace data from the backend and wait till
  // all packets are received.
  const requests = getSortedRequests(store.getState());
  await Promise.all(
    requests.map(requestItem =>
      connector.requestData(requestItem.id, "stackTrace")
    )
  );

  // While there is a defined order for requests in each document separately, the requests
  // from different documents may interleave in various ways that change per test run, so
  // there is not a single order when considering all the requests together.
  let currentTop = 0;
  let currentSub = 0;
  for (let i = 0; i < REQUEST_COUNT; i++) {
    const requestItem = getSortedRequests(store.getState())[i];

    const itemUrl = requestItem.url;
    const itemCauseUri = requestItem.cause.loadingDocumentUri;
    let spec;
    if (itemUrl == SUB_URL || itemCauseUri == SUB_URL) {
      spec = EXPECTED_REQUESTS_SUB[currentSub++];
    } else {
      spec = EXPECTED_REQUESTS_TOP[currentTop++];
    }
    const { method, url, causeType, causeUri, stack } = spec;

    verifyRequestItemTarget(
      document,
      getDisplayedRequests(store.getState()),
      requestItem,
      method,
      url,
      { cause: { type: causeType, loadingDocumentUri: causeUri } }
    );

    const { stacktrace } = requestItem;
    const stackLen = stacktrace ? stacktrace.length : 0;

    if (stack) {
      ok(stacktrace, `Request #${i} has a stacktrace`);
      ok(
        stackLen > 0,
        `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`
      );

      // if "stack" is array, check the details about the top stack frames
      if (Array.isArray(stack)) {
        stack.forEach((frame, j) => {
          is(
            stacktrace[j].functionName,
            frame.fn,
            `Request #${i} has the correct function on JS stack frame #${j}`
          );
          is(
            stacktrace[j].filename.split("/").pop(),
            frame.file,
            `Request #${i} has the correct file on JS stack frame #${j}`
          );
          is(
            stacktrace[j].lineNumber,
            frame.line,
            `Request #${i} has the correct line number on JS stack frame #${j}`
          );
          is(
            stacktrace[j].asyncCause,
            frame.asyncCause,
            `Request #${i} has the correct async cause on JS stack frame #${j}`
          );
        });
      }
    } else {
      is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
    }
  }

  await teardown(monitor);
});
