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

"use strict";

const {
  createCommandsForTab,
  STUBS_UPDATE_ENV,
  getCleanedPacket,
  getSerializedPacket,
  getStubFile,
  writeStubsToFile,
} = require(`${CHROME_URL_ROOT}stub-generator-helpers`);

const TEST_URI =
  "https://example.com/browser/devtools/client/webconsole/test/browser/test-network-event.html";
const STUB_FILE = "networkEvent.js";

add_task(async function () {
  const isStubsUpdate = Services.env.get(STUBS_UPDATE_ENV) == "true";
  info(`${isStubsUpdate ? "Update" : "Check"} ${STUB_FILE}`);

  const generatedStubs = await generateNetworkEventStubs();

  if (isStubsUpdate) {
    await writeStubsToFile(STUB_FILE, generatedStubs, true);
    ok(true, `${STUB_FILE} was updated`);
    return;
  }

  const existingStubs = getStubFile(STUB_FILE);
  const FAILURE_MSG =
    "The network event stubs file needs to be updated by running `" +
    `mach test ${getCurrentTestFilePath()} --headless --setenv WEBCONSOLE_STUBS_UPDATE=true` +
    "`";

  if (generatedStubs.size !== existingStubs.stubPackets.size) {
    ok(false, FAILURE_MSG);
    return;
  }

  let failed = false;
  for (const [key, packet] of generatedStubs) {
    // const existingPacket = existingStubs.stubPackets.get(key);
    const packetStr = getSerializedPacket(packet, {
      sortKeys: true,
      replaceActorIds: true,
    });
    const existingPacketStr = getSerializedPacket(
      existingStubs.stubPackets.get(key),
      { sortKeys: true, replaceActorIds: true }
    );
    is(packetStr, existingPacketStr, `"${key}" packet has expected value`);
    failed = failed || packetStr !== existingPacketStr;
  }

  if (failed) {
    ok(false, FAILURE_MSG);
  } else {
    ok(true, "Stubs are up to date");
  }
});

async function generateNetworkEventStubs() {
  const stubs = new Map();
  const tab = await addTab(TEST_URI);
  const commands = await createCommandsForTab(tab);
  await commands.targetCommand.startListening();
  const resourceCommand = commands.resourceCommand;

  const stacktraces = new Map();
  let addNetworkStub = function () {};
  let addNetworkUpdateStub = function () {};

  const onAvailable = resources => {
    for (const resource of resources) {
      if (resource.resourceType == resourceCommand.TYPES.NETWORK_EVENT) {
        if (stacktraces.has(resource.channelId)) {
          const { stacktraceAvailable, lastFrame } = stacktraces.get(
            resource.channelId
          );
          resource.cause.stacktraceAvailable = stacktraceAvailable;
          resource.cause.lastFrame = lastFrame;
          stacktraces.delete(resource.channelId);
        }
        addNetworkStub(resource);
        continue;
      }
      if (
        resource.resourceType == resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE
      ) {
        stacktraces.set(resource.channelId, resource);
      }
    }
  };
  const onUpdated = updates => {
    for (const { resource, update } of updates) {
      addNetworkUpdateStub(resource, update);
    }
  };

  await resourceCommand.watchResources(
    [
      resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
      resourceCommand.TYPES.NETWORK_EVENT,
    ],
    {
      onAvailable,
      onUpdated,
    }
  );

  for (const [key, code] of getCommands()) {
    const networkEventDone = new Promise(resolve => {
      addNetworkStub = resource => {
        stubs.set(key, getCleanedPacket(key, getOrderedResource(resource)));
        resolve();
      };
    });

    const networkEventUpdateDone = new Promise(resolve => {
      addNetworkUpdateStub = (resource, update) => {
        const updateKey = `${key} update`;
        stubs.set(key, getCleanedPacket(key, getOrderedResource(resource)));
        stubs.set(
          updateKey,
          // We cannot ensure the form of the resource, some properties
          // might be in another order than in the original resource.
          // Hand-picking only what we need should prevent this.
          getCleanedPacket(updateKey, getOrderedResource(resource))
        );

        if (update.resourceUpdates.responseEndAvailable) {
          resolve();
        }
      };
    });

    await SpecialPowers.spawn(
      gBrowser.selectedBrowser,
      [code],
      function (subCode) {
        const script = content.document.createElement("script");
        script.append(
          content.document.createTextNode(
            `function triggerPacket() {${subCode}}`
          )
        );
        content.document.body.append(script);
        content.wrappedJSObject.triggerPacket();
        script.remove();
      }
    );
    await Promise.all([networkEventDone, networkEventUpdateDone]);
  }
  resourceCommand.unwatchResources(
    [
      resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
      resourceCommand.TYPES.NETWORK_EVENT,
    ],
    {
      onAvailable,
      onUpdated,
    }
  );

  await commands.destroy();

  return stubs;
}
// Ensures the order of the resource properties
function getOrderedResource(resource) {
  return {
    resourceType: resource.resourceType,
    timeStamp: resource.timeStamp,
    actor: resource.actor,
    startedDateTime: resource.startedDateTime,
    method: resource.method,
    url: resource.url,
    isXHR: resource.isXHR,
    cause: resource.cause,
    httpVersion: resource.httpVersion,
    status: resource.status,
    statusText: resource.statusText,
    headersSize: resource.headersSize,
    remoteAddress: resource.remoteAddress,
    remotePort: resource.remotePort,
    mimeType: resource.mimeType,
    waitingTime: resource.waitingTime,
    contentSize: resource.contentSize,
    transferredSize: resource.transferredSize,
    timings: resource.timings,
    private: resource.private,
    fromCache: resource.fromCache,
    fromServiceWorker: resource.fromServiceWorker,
    isThirdPartyTrackingResource: resource.isThirdPartyTrackingResource,
    referrerPolicy: resource.referrerPolicy,
    blockedReason: resource.blockedReason,
    extension: resource.extension,
    channelId: resource.channelId,
    totalTime: resource.totalTime,
    securityState: resource.securityState,
    responseCache: resource.responseCache,
    isRacing: resource.isRacing,
  };
}

function getCommands() {
  const networkEvent = new Map();

  networkEvent.set(
    "GET request",
    `
let i = document.createElement("img");
i.src = "/inexistent.html";
`
  );

  networkEvent.set(
    "XHR GET request",
    `
const xhr = new XMLHttpRequest();
xhr.open("GET", "/inexistent.html");
xhr.send();
`
  );

  networkEvent.set(
    "XHR POST request",
    `
const xhr = new XMLHttpRequest();
xhr.open("POST", "/inexistent.html");
xhr.send();
`
  );
  return networkEvent;
}
