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

"use strict";

/* import-globals-from helper-telemetry.js */
Services.scriptloader.loadSubScript(
  CHROME_URL_ROOT + "helper-telemetry.js",
  this
);

const USB_RUNTIME = {
  id: "runtime-id-1",
  deviceName: "Device A",
  name: "Runtime 1",
  shortName: "R1",
};

/**
 * Check that telemetry events for connection attempts are correctly recorded in various
 * scenarios:
 * - successful connection
 * - successful connection after showing the timeout warning
 * - failed connection
 * - connection timeout
 */
add_task(async function testSuccessfulConnectionAttempt() {
  const { doc, mocks, runtimeId, sessionId, tab } =
    await setupConnectionAttemptTest();

  await connectToRuntime(USB_RUNTIME.deviceName, doc);

  const connectionEvents = checkTelemetryEvents(
    [
      { method: "runtime_connected", extras: { runtime_id: runtimeId } },
      {
        method: "connection_attempt",
        extras: getEventExtras("start", runtimeId),
      },
      {
        method: "connection_attempt",
        extras: getEventExtras("success", runtimeId),
      },
    ],
    sessionId
  ).filter(({ method }) => method === "connection_attempt");

  checkConnectionId(connectionEvents);

  await removeUsbRuntime(USB_RUNTIME, mocks, doc);
  await removeTab(tab);
});

add_task(async function testFailedConnectionAttempt() {
  const { doc, mocks, runtimeId, sessionId, tab } =
    await setupConnectionAttemptTest();
  mocks.runtimeClientFactoryMock.createClientForRuntime = async runtime => {
    throw new Error("failed");
  };

  info(
    "Try to connect to the runtime and wait for the connection error message"
  );
  const usbRuntimeSidebarItem = findSidebarItemByText(
    USB_RUNTIME.deviceName,
    doc
  );
  const connectButton =
    usbRuntimeSidebarItem.querySelector(".qa-connect-button");
  connectButton.click();
  await waitUntil(() =>
    usbRuntimeSidebarItem.querySelector(".qa-connection-error")
  );

  const connectionEvents = checkTelemetryEvents(
    [
      {
        method: "connection_attempt",
        extras: getEventExtras("start", runtimeId),
      },
      {
        method: "connection_attempt",
        extras: getEventExtras("failed", runtimeId),
      },
    ],
    sessionId
  ).filter(({ method }) => method === "connection_attempt");

  checkConnectionId(connectionEvents);

  await removeUsbRuntime(USB_RUNTIME, mocks, doc);
  await removeTab(tab);
});

add_task(async function testPendingConnectionAttempt() {
  info("Set timeout preferences to avoid cancelling the connection");
  await pushPref(
    "devtools.aboutdebugging.test-connection-timing-out-delay",
    100
  );
  await pushPref(
    "devtools.aboutdebugging.test-connection-cancel-delay",
    100000
  );

  const { doc, mocks, runtimeId, sessionId, tab } =
    await setupConnectionAttemptTest();

  info("Simulate a pending connection");
  let resumeConnection;
  const resumeConnectionPromise = new Promise(r => {
    resumeConnection = r;
  });
  mocks.runtimeClientFactoryMock.createClientForRuntime = async runtime => {
    await resumeConnectionPromise;
    return mocks._clients[runtime.type][runtime.id];
  };

  info("Click on the connect button and wait for the warning message");
  const usbRuntimeSidebarItem = findSidebarItemByText(
    USB_RUNTIME.deviceName,
    doc
  );
  const connectButton =
    usbRuntimeSidebarItem.querySelector(".qa-connect-button");
  connectButton.click();
  await waitUntil(() => doc.querySelector(".qa-connection-not-responding"));

  info("Resume the connection and wait for the connection to succeed");
  resumeConnection();
  await waitUntil(
    () => !usbRuntimeSidebarItem.querySelector(".qa-connect-button")
  );

  const connectionEvents = checkTelemetryEvents(
    [
      { method: "runtime_connected", extras: { runtime_id: runtimeId } },
      {
        method: "connection_attempt",
        extras: getEventExtras("start", runtimeId),
      },
      {
        method: "connection_attempt",
        extras: getEventExtras("not responding", runtimeId),
      },
      {
        method: "connection_attempt",
        extras: getEventExtras("success", runtimeId),
      },
    ],
    sessionId
  ).filter(({ method }) => method === "connection_attempt");
  checkConnectionId(connectionEvents);

  await removeUsbRuntime(USB_RUNTIME, mocks, doc);
  await removeTab(tab);
});

add_task(async function testCancelledConnectionAttempt() {
  info("Set timeout preferences to quickly cancel the connection");
  await pushPref(
    "devtools.aboutdebugging.test-connection-timing-out-delay",
    100
  );
  await pushPref("devtools.aboutdebugging.test-connection-cancel-delay", 1000);

  const { doc, mocks, runtimeId, sessionId, tab } =
    await setupConnectionAttemptTest();

  info("Simulate a connection timeout");
  mocks.runtimeClientFactoryMock.createClientForRuntime = async runtime => {
    await new Promise(r => {});
  };

  info("Click on the connect button and wait for the error message");
  const usbRuntimeSidebarItem = findSidebarItemByText(
    USB_RUNTIME.deviceName,
    doc
  );
  const connectButton =
    usbRuntimeSidebarItem.querySelector(".qa-connect-button");
  connectButton.click();
  await waitUntil(() =>
    usbRuntimeSidebarItem.querySelector(".qa-connection-timeout")
  );

  const connectionEvents = checkTelemetryEvents(
    [
      {
        method: "connection_attempt",
        extras: getEventExtras("start", runtimeId),
      },
      {
        method: "connection_attempt",
        extras: getEventExtras("not responding", runtimeId),
      },
      {
        method: "connection_attempt",
        extras: getEventExtras("cancelled", runtimeId),
      },
    ],
    sessionId
  ).filter(({ method }) => method === "connection_attempt");
  checkConnectionId(connectionEvents);

  await removeUsbRuntime(USB_RUNTIME, mocks, doc);
  await removeTab(tab);
});

function checkConnectionId(connectionEvents) {
  const connectionId = connectionEvents[0].extras.connection_id;
  ok(
    !!connectionId,
    "Found a valid connection id in the first connection_attempt event"
  );
  for (const evt of connectionEvents) {
    is(
      evt.extras.connection_id,
      connectionId,
      "All connection_attempt events share the same connection id"
    );
  }
}

// Small helper to create the expected event extras object for connection_attempt events
function getEventExtras(status, runtimeId) {
  return {
    connection_type: "usb",
    runtime_id: runtimeId,
    status,
  };
}

// Open about:debugging, setup telemetry, mocks and create a mocked USB runtime.
async function setupConnectionAttemptTest() {
  const mocks = new Mocks();
  setupTelemetryTest();

  const { tab, document } = await openAboutDebugging();

  const sessionId = getOpenEventSessionId();
  ok(!isNaN(sessionId), "Open event has a valid session id");

  mocks.createUSBRuntime(USB_RUNTIME.id, {
    deviceName: USB_RUNTIME.deviceName,
    name: USB_RUNTIME.name,
    shortName: USB_RUNTIME.shortName,
  });
  mocks.emitUSBUpdate();

  info("Wait for the runtime to appear in the sidebar");
  await waitUntil(() => findSidebarItemByText(USB_RUNTIME.shortName, document));
  const evts = checkTelemetryEvents(
    [
      { method: "device_added", extras: {} },
      { method: "runtime_added", extras: {} },
    ],
    sessionId
  );

  const runtimeId = evts.filter(e => e.method === "runtime_added")[0].extras
    .runtime_id;
  return { doc: document, mocks, runtimeId, sessionId, tab };
}

async function removeUsbRuntime(runtime, mocks, doc) {
  mocks.removeRuntime(runtime.id);
  mocks.emitUSBUpdate();
  await waitUntil(
    () =>
      !findSidebarItemByText(runtime.name, doc) &&
      !findSidebarItemByText(runtime.shortName, doc)
  );
}
