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

function assertNoLeaksInTabTracker() {
  // Check that no tabs have been leaked by the internal tabTracker helper class.
  const { ExtensionParent } = ChromeUtils.importESModule(
    "resource://gre/modules/ExtensionParent.sys.mjs"
  );
  const { tabTracker } = ExtensionParent.apiManager.global;

  for (const [tabId, nativeTab] of tabTracker._tabIds) {
    if (!nativeTab.ownerGlobal) {
      ok(
        false,
        `A tab with tabId ${tabId} has been leaked in the tabTracker ("${nativeTab.title}")`
      );
    }
  }
}

add_task(async function testWindowCreate() {
  async function background() {
    let promiseTabAttached = () => {
      return new Promise(resolve => {
        browser.tabs.onAttached.addListener(function listener() {
          browser.tabs.onAttached.removeListener(listener);
          resolve();
        });
      });
    };

    let promiseTabUpdated = expected => {
      return new Promise(resolve => {
        browser.tabs.onUpdated.addListener(function listener(
          tabId,
          changeInfo,
          tab
        ) {
          if (changeInfo.url === expected) {
            browser.tabs.onUpdated.removeListener(listener);
            resolve();
          }
        });
      });
    };

    try {
      let window = await browser.windows.getCurrent();
      let windowId = window.id;

      browser.test.log("Create additional tab in window 1");
      let tab = await browser.tabs.create({ windowId, url: "about:blank" });
      let tabId = tab.id;

      browser.test.log("Create a new window, adopting the new tab");

      // Note that we want to check against actual boolean values for
      // all of the `incognito` property tests.
      browser.test.assertEq(false, tab.incognito, "Tab is not private");

      {
        let [, window] = await Promise.all([
          promiseTabAttached(),
          browser.windows.create({ tabId: tabId }),
        ]);
        browser.test.assertEq(
          false,
          window.incognito,
          "New window is not private"
        );
        browser.test.assertEq(
          tabId,
          window.tabs[0].id,
          "tabs property populated correctly"
        );

        browser.test.log("Close the new window");
        await browser.windows.remove(window.id);
      }

      {
        browser.test.log("Create a new private window");
        let privateWindow = await browser.windows.create({ incognito: true });
        browser.test.assertEq(
          true,
          privateWindow.incognito,
          "Private window is private"
        );

        browser.test.log("Create additional tab in private window");
        let privateTab = await browser.tabs.create({
          windowId: privateWindow.id,
        });
        browser.test.assertEq(
          true,
          privateTab.incognito,
          "Private tab is private"
        );

        browser.test.log("Create a new window, adopting the new private tab");
        let [, newWindow] = await Promise.all([
          promiseTabAttached(),
          browser.windows.create({ tabId: privateTab.id }),
        ]);
        browser.test.assertEq(
          true,
          newWindow.incognito,
          "New private window is private"
        );

        browser.test.log("Close the new private window");
        await browser.windows.remove(newWindow.id);

        browser.test.log("Close the private window");
        await browser.windows.remove(privateWindow.id);
      }

      browser.test.log("Try to create a window with both a tab and a URL");
      [tab] = await browser.tabs.query({ windowId, active: true });
      await browser.test.assertRejects(
        browser.windows.create({ tabId: tab.id, url: "http://example.com/" }),
        /`tabId` may not be used in conjunction with `url`/,
        "Create call failed as expected"
      );

      browser.test.log(
        "Try to create a window with both a tab and an invalid incognito setting"
      );
      await browser.test.assertRejects(
        browser.windows.create({ tabId: tab.id, incognito: true }),
        /`incognito` property must match the incognito state of tab/,
        "Create call failed as expected"
      );

      browser.test.log("Try to create a window with an invalid tabId");
      await browser.test.assertRejects(
        browser.windows.create({ tabId: 0 }),
        /Invalid tab ID: 0/,
        "Create call failed as expected"
      );

      browser.test.log("Try to create a window with two URLs");
      let readyPromise = Promise.all([
        // tabs.onUpdated can be invoked between the call of windows.create and
        // the invocation of its callback/promise, so set up the listeners
        // before creating the window.
        promiseTabUpdated("http://example.com/"),
        promiseTabUpdated("http://example.org/"),
      ]);

      window = await browser.windows.create({
        url: ["http://example.com/", "http://example.org/"],
      });
      await readyPromise;

      browser.test.assertEq(
        2,
        window.tabs.length,
        "2 tabs were opened in new window"
      );
      browser.test.assertEq(
        "about:blank",
        window.tabs[0].url,
        "about:blank, page not loaded yet"
      );
      browser.test.assertEq(
        "about:blank",
        window.tabs[1].url,
        "about:blank, page not loaded yet"
      );

      window = await browser.windows.get(window.id, { populate: true });

      browser.test.assertEq(
        2,
        window.tabs.length,
        "2 tabs were opened in new window"
      );
      browser.test.assertEq(
        "http://example.com/",
        window.tabs[0].url,
        "Correct URL was loaded in tab 1"
      );
      browser.test.assertEq(
        "http://example.org/",
        window.tabs[1].url,
        "Correct URL was loaded in tab 2"
      );

      await browser.windows.remove(window.id);

      browser.test.notifyPass("window-create");
    } catch (e) {
      browser.test.fail(`${e} :: ${e.stack}`);
      browser.test.notifyFail("window-create");
    }
  }

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

    background,
  });

  await extension.startup();
  await extension.awaitFinish("window-create");
  await extension.unload();

  assertNoLeaksInTabTracker();
});

add_task(async function testWebNavigationOnWindowCreateTabId() {
  async function background() {
    const webNavEvents = [];
    const onceTabsAttached = [];

    let promiseTabAttached = tab => {
      return new Promise(resolve => {
        browser.tabs.onAttached.addListener(function listener(tabId) {
          if (tabId !== tab.id) {
            return;
          }
          browser.tabs.onAttached.removeListener(listener);
          resolve();
        });
      });
    };

    // Listen to webNavigation.onCompleted events to ensure that
    // it is not going to be fired when we move the existent tabs
    // to new windows.
    browser.webNavigation.onCompleted.addListener(data => {
      webNavEvents.push(data);
    });

    // Wait for the list of urls needed to select the test tabs,
    // and then move these tabs to a new window and assert that
    // no webNavigation.onCompleted events should be received
    // while the tabs are being adopted into the new windows.
    browser.test.onMessage.addListener(async (msg, testTabURLs) => {
      if (msg !== "testTabURLs") {
        return;
      }

      // Retrieve the tabs list and filter out the tabs that should
      // not be moved into a new window.
      let allTabs = await browser.tabs.query({});
      let testTabs = allTabs.filter(tab => {
        return testTabURLs.includes(tab.url);
      });

      browser.test.assertEq(
        2,
        testTabs.length,
        "Got the expected number of test tabs"
      );

      for (let tab of testTabs) {
        onceTabsAttached.push(promiseTabAttached(tab));
        await browser.windows.create({ tabId: tab.id });
      }

      // Wait the tabs to have been attached to the new window and then assert that no
      // webNavigation.onCompleted event has been received.
      browser.test.log("Waiting tabs move to new window to be attached");
      await Promise.all(onceTabsAttached);

      browser.test.assertEq(
        "[]",
        JSON.stringify(webNavEvents),
        "No webNavigation.onCompleted event should have been received"
      );

      // Remove all the test tabs before exiting the test successfully.
      for (let tab of testTabs) {
        await browser.tabs.remove(tab.id);
      }

      browser.test.notifyPass("webNavigation-on-window-create-tabId");
    });
  }

  const testURLs = ["http://example.com/", "http://example.org/"];

  for (let url of testURLs) {
    await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  }

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

  await extension.startup();

  await extension.sendMessage("testTabURLs", testURLs);

  await extension.awaitFinish("webNavigation-on-window-create-tabId");
  await extension.unload();

  assertNoLeaksInTabTracker();
});

add_task(async function testGetLastFocusedDoesNotLeakDuringTabAdoption() {
  async function background() {
    const allTabs = await browser.tabs.query({});

    browser.test.onMessage.addListener(async (msg, testTabURL) => {
      if (msg !== "testTabURL") {
        return;
      }

      let tab = allTabs.filter(tab => tab.url === testTabURL).pop();

      // Keep calling getLastFocused while browser.windows.create is creating
      // a new window to adopt the test tab, so that the test recreates
      // conditions similar to the extension that has been triggered this leak
      // (See Bug 1458918 for a rationale).
      // The while loop is explicited exited right before the notifyPass
      // (but unloading the extension will stop it in any case).
      let stopGetLastFocusedLoop = false;
      Promise.resolve().then(async () => {
        while (!stopGetLastFocusedLoop) {
          browser.windows.getLastFocused({ populate: true });
          // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
          await new Promise(resolve => setTimeout(resolve, 50));
        }
      });

      // Create a new window which adopt an existent tab and wait the tab to
      // be fully attached to the new window.
      await Promise.all([
        new Promise(resolve => {
          const listener = () => {
            browser.tabs.onAttached.removeListener(listener);
            resolve();
          };
          browser.tabs.onAttached.addListener(listener);
        }),
        browser.windows.create({ tabId: tab.id }),
      ]);

      // Check that getLastFocused populate the tabs property once the tab adoption
      // has been completed.
      const lastFocusedPopulate = await browser.windows.getLastFocused({
        populate: true,
      });
      browser.test.assertEq(
        1,
        lastFocusedPopulate.tabs.length,
        "Got the expected number of tabs from windows.getLastFocused"
      );

      // Remove the test tab.
      await browser.tabs.remove(tab.id);

      stopGetLastFocusedLoop = true;

      browser.test.notifyPass("tab-adopted");
    });
  }

  await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");

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

  await extension.startup();

  extension.sendMessage("testTabURL", "http://example.com/");

  await extension.awaitFinish("tab-adopted");

  await extension.unload();

  assertNoLeaksInTabTracker();
});
