/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
"use strict";

add_task(async function testTabEvents() {
  async function background() {
    /** The list of active tab ID's */
    let tabIds = [];

    /**
     * Stores the events that fire for each tab.
     *
     * events {
     *   tabId1: [event1, event2, ...],
     *   tabId2: [event1, event2, ...],
     * }
     */
    let events = {};

    browser.tabs.onActivated.addListener(info => {
      if (info.tabId in events) {
        events[info.tabId].push("onActivated");
      } else {
        events[info.tabId] = ["onActivated"];
      }
    });

    browser.tabs.onCreated.addListener(info => {
      if (info.id in events) {
        events[info.id].push("onCreated");
      } else {
        events[info.id] = ["onCreated"];
      }
    });

    browser.tabs.onHighlighted.addListener(info => {
      if (info.tabIds[0] in events) {
        events[info.tabIds[0]].push("onHighlighted");
      } else {
        events[info.tabIds[0]] = ["onHighlighted"];
      }
    });

    /**
     * Asserts that the expected events are fired for the tab with id = tabId.
     * The events associated to the specified tab are removed after this check is made.
     *
     * @param {number} tabId
     * @param {Array<string>} expectedEvents
     */
    async function expectEvents(tabId, expectedEvents) {
      browser.test.log(`Expecting events: ${expectedEvents.join(", ")}`);

      // Wait up to 5000 ms for the expected number of events.
      for (
        let i = 0;
        i < 50 &&
        (!events[tabId] || events[tabId].length < expectedEvents.length);
        i++
      ) {
        await new Promise(resolve => setTimeout(resolve, 100));
      }

      browser.test.assertEq(
        expectedEvents.length,
        events[tabId].length,
        `Got expected number of events for ${tabId}`
      );

      for (let name of expectedEvents) {
        browser.test.assertTrue(
          events[tabId].includes(name),
          `Got expected ${name} event`
        );
      }

      if (expectedEvents.includes("onCreated")) {
        browser.test.assertEq(
          events[tabId].indexOf("onCreated"),
          0,
          "onCreated happened first"
        );
      }

      delete events[tabId];
    }

    /**
     * Opens a new tab and asserts that the correct events are fired.
     *
     * @param {number} windowId
     */
    async function openTab(windowId) {
      browser.test.assertEq(
        0,
        Object.keys(events).length,
        "No events remaining before testing openTab."
      );

      let tab = await browser.tabs.create({ windowId });

      tabIds.push(tab.id);
      browser.test.log(`Opened tab ${tab.id}`);

      await expectEvents(tab.id, ["onCreated", "onActivated", "onHighlighted"]);
    }

    /**
     * Opens a new window and asserts that the correct events are fired.
     *
     * @param {Array} urls A list of urls for which to open tabs in the new window.
     */
    async function openWindow(urls) {
      browser.test.assertEq(
        0,
        Object.keys(events).length,
        "No events remaining before testing openWindow."
      );

      let window = await browser.windows.create({ url: urls });
      browser.test.log(`Opened new window ${window.id}`);

      for (let [i] of urls.entries()) {
        let tab = window.tabs[i];
        tabIds.push(tab.id);

        let expectedEvents = ["onCreated", "onActivated", "onHighlighted"];
        if (i !== 0) {
          expectedEvents.splice(1);
        }
        await expectEvents(window.tabs[i].id, expectedEvents);
      }
    }

    /**
     * Highlights an existing tab and asserts that the correct events are fired.
     *
     * @param {number} tabId
     */
    async function highlightTab(tabId) {
      browser.test.assertEq(
        0,
        Object.keys(events).length,
        "No events remaining before testing highlightTab."
      );

      browser.test.log(`Highlighting tab ${tabId}`);
      let tab = await browser.tabs.update(tabId, { active: true });

      browser.test.assertEq(tab.id, tabId, `Tab ${tab.id} highlighted`);

      await expectEvents(tab.id, ["onActivated", "onHighlighted"]);
    }

    /**
     * The main entry point to the tests.
     */
    let tabs = await browser.tabs.query({ active: true, currentWindow: true });

    let activeWindow = tabs[0].windowId;
    await Promise.all([
      openTab(activeWindow),
      openTab(activeWindow),
      openTab(activeWindow),
    ]);

    await Promise.all([
      highlightTab(tabIds[0]),
      highlightTab(tabIds[1]),
      highlightTab(tabIds[2]),
    ]);

    // If we open these windows in parallel, there is a risk
    // that a window will be occluded by the next one before
    // it can finish first paint, which will prevent
    // the firing of browser-delayed-startup-finished
    await openWindow(["http://example.com"]);
    await openWindow(["http://example.com", "http://example.org"]);
    await openWindow([
      "http://example.com",
      "http://example.org",
      "http://example.net",
    ]);

    browser.test.assertEq(
      0,
      Object.keys(events).length,
      "No events remaining after tests."
    );

    await Promise.all(tabIds.map(id => browser.tabs.remove(id)));

    browser.test.notifyPass("tabs.highlight");
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["tabs"],
    },

    background,
  });

  await extension.startup();
  await extension.awaitFinish("tabs.highlight");
  await extension.unload();
});
