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

async function background(tabCount, testFn) {
  try {
    const { TAB_ID_NONE } = browser.tabs;
    const tabIds = await Promise.all(
      Array.from({ length: tabCount }, () =>
        browser.tabs.create({ url: "about:blank" }).then(t => t.id)
      )
    );

    const toTabIds = i => tabIds[i];

    const setSuccessors = mapping =>
      Promise.all(
        mapping.map((succ, i) =>
          browser.tabs.update(tabIds[i], { successorTabId: tabIds[succ] })
        )
      );

    const verifySuccessors = async function(mapping, name) {
      const promises = [],
        expected = [];
      for (let i = 0; i < mapping.length; i++) {
        if (mapping[i] !== undefined) {
          promises.push(
            browser.tabs.get(tabIds[i]).then(t => t.successorTabId)
          );
          expected.push(
            mapping[i] === TAB_ID_NONE ? TAB_ID_NONE : tabIds[mapping[i]]
          );
        }
      }
      const results = await Promise.all(promises);
      for (let i = 0; i < results.length; i++) {
        browser.test.assertEq(
          expected[i],
          results[i],
          `${name}: successorTabId of tab ${i} in mapping should be ${expected[i]}`
        );
      }
    };

    await testFn({
      TAB_ID_NONE,
      tabIds,
      toTabIds,
      setSuccessors,
      verifySuccessors,
    });

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

async function runTabTest(tabCount, testFn) {
  const extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["tabs"],
    },
    background: `(${background})(${tabCount}, ${testFn});`,
  });

  await extension.startup();
  await extension.awaitFinish("background-script");
  await extension.unload();
}

add_task(function testTabSuccessors() {
  return runTabTest(3, async function({ TAB_ID_NONE, tabIds }) {
    const anotherWindow = await browser.windows.create({ url: "about:blank" });

    browser.test.assertEq(
      TAB_ID_NONE,
      (await browser.tabs.get(tabIds[0])).successorTabId,
      "Tabs default to an undefined successor"
    );

    // Basic getting and setting

    await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
    browser.test.assertEq(
      tabIds[1],
      (await browser.tabs.get(tabIds[0])).successorTabId,
      "tabs.update assigned the correct successor"
    );

    await browser.tabs.update(tabIds[0], {
      successorTabId: browser.tabs.TAB_ID_NONE,
    });
    browser.test.assertEq(
      TAB_ID_NONE,
      (await browser.tabs.get(tabIds[0])).successorTabId,
      "tabs.update cleared successor"
    );

    await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
    await browser.tabs.update(tabIds[0], { successorTabId: tabIds[0] });
    browser.test.assertEq(
      TAB_ID_NONE,
      (await browser.tabs.get(tabIds[0])).successorTabId,
      "Setting a tab as its own successor clears the successor instead"
    );

    // Validation tests

    await browser.test.assertRejects(
      browser.tabs.update(tabIds[0], { successorTabId: 1e8 }),
      /Invalid successorTabId/,
      "tabs.update should throw with an invalid successor tab ID"
    );

    await browser.test.assertRejects(
      browser.tabs.update(tabIds[0], {
        successorTabId: anotherWindow.tabs[0].id,
      }),
      /Successor tab must be in the same window as the tab being updated/,
      "tabs.update should throw with a successor tab ID from another window"
    );

    // Make sure the successor is truly being assigned

    await browser.tabs.update(tabIds[0], {
      successorTabId: tabIds[2],
      active: true,
    });
    await browser.tabs.remove(tabIds[0]);
    browser.test.assertEq(
      tabIds[2],
      (await browser.tabs.query({ active: true }))[0].id
    );

    return browser.tabs.remove([
      tabIds[1],
      tabIds[2],
      anotherWindow.tabs[0].id,
    ]);
  });
});

add_task(function testMoveInSuccession_appendFalse() {
  return runTabTest(8, async function({
    TAB_ID_NONE,
    tabIds,
    toTabIds,
    setSuccessors,
    verifySuccessors,
  }) {
    await browser.tabs.moveInSuccession([1, 0].map(toTabIds), tabIds[0]);
    await verifySuccessors([TAB_ID_NONE, 0], "scenario 1");

    await browser.tabs.moveInSuccession([0, 1, 2, 3].map(toTabIds), tabIds[0]);
    await verifySuccessors([1, 2, 3, 0], "scenario 2");

    await browser.tabs.moveInSuccession([1, 0].map(toTabIds), tabIds[0]);
    await verifySuccessors(
      [TAB_ID_NONE, 0],
      "scenario 1 after tab 0 has a successor"
    );

    await browser.tabs.update(tabIds[7], { successorTabId: tabIds[0] });
    await browser.tabs.moveInSuccession([4, 5, 6, 7].map(toTabIds));
    await verifySuccessors(
      new Array(4).concat([5, 6, 7, TAB_ID_NONE]),
      "scenario 4"
    );

    await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
    await browser.tabs.moveInSuccession([4, 6, 3, 2].map(toTabIds), tabIds[7]);
    await verifySuccessors([7, TAB_ID_NONE, 7, 2, 6, 7, 3, 5], "scenario 5");

    await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
    await browser.tabs.moveInSuccession([4, 6, 3, 2].map(toTabIds), tabIds[7], {
      insert: true,
    });
    await verifySuccessors([4, TAB_ID_NONE, 7, 2, 6, 4, 3, 5], "insert = true");

    await setSuccessors([1, 2, 3, 4, 0]);
    await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[0], {
      insert: true,
    });
    await verifySuccessors([4, 2, 0, 1, 3], "insert = true, part 2");

    await browser.tabs.moveInSuccession([tabIds[0], tabIds[1], 1e8, tabIds[2]]);
    await verifySuccessors([1, 2, TAB_ID_NONE], "unknown tab ID");

    browser.test.assertTrue(
      await browser.tabs.moveInSuccession([1e8]).then(
        () => true,
        () => false
      ),
      "When all tab IDs are unknown, tabs.moveInSuccession should not throw"
    );

    // Validation tests

    await browser.test.assertRejects(
      browser.tabs.moveInSuccession([tabIds[0], tabIds[1], tabIds[0]]),
      /IDs must not occur more than once in tabIds/,
      "tabs.moveInSuccession should throw when a tab is referenced more than once in tabIds"
    );

    await browser.test.assertRejects(
      browser.tabs.moveInSuccession([tabIds[0], tabIds[1]], tabIds[0], {
        insert: true,
      }),
      /Value of tabId must not occur in tabIds if append or insert is true/,
      "tabs.moveInSuccession should throw when tabId occurs in tabIds and insert is true"
    );

    return browser.tabs.remove(tabIds);
  });
});

add_task(function testMoveInSuccession_appendTrue() {
  return runTabTest(8, async function({
    TAB_ID_NONE,
    tabIds,
    toTabIds,
    setSuccessors,
    verifySuccessors,
  }) {
    await browser.tabs.moveInSuccession([1].map(toTabIds), tabIds[0], {
      append: true,
    });
    await verifySuccessors([1, TAB_ID_NONE], "scenario 1");

    await browser.tabs.update(tabIds[3], { successorTabId: tabIds[4] });
    await browser.tabs.moveInSuccession([1, 2, 3].map(toTabIds), tabIds[0], {
      append: true,
    });
    await verifySuccessors([1, 2, 3, TAB_ID_NONE], "scenario 2");

    await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
    await browser.tabs.moveInSuccession([1e8], tabIds[0], { append: true });
    browser.test.assertEq(
      TAB_ID_NONE,
      (await browser.tabs.get(tabIds[0])).successorTabId,
      "If no tabs get appended after the reference tab, it should lose its successor"
    );

    await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
    await browser.tabs.moveInSuccession([4, 6, 3, 2].map(toTabIds), tabIds[7], {
      append: true,
    });
    await verifySuccessors(
      [7, TAB_ID_NONE, TAB_ID_NONE, 2, 6, 7, 3, 4],
      "scenario 3"
    );

    await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
    await browser.tabs.moveInSuccession([4, 6, 3, 2].map(toTabIds), tabIds[7], {
      append: true,
      insert: true,
    });
    await verifySuccessors([7, TAB_ID_NONE, 5, 2, 6, 7, 3, 4], "insert = true");

    await browser.tabs.moveInSuccession([0, 4].map(toTabIds), tabIds[7], {
      append: true,
      insert: true,
    });
    await verifySuccessors(
      [4, undefined, undefined, undefined, 6, undefined, undefined, 0],
      "insert = true, part 2"
    );

    await setSuccessors([1, 2, 3, 4, 0]);
    await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[0], {
      append: true,
      insert: true,
    });
    await verifySuccessors([3, 2, 4, 1, 0], "insert = true, part 3");

    await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
    await browser.tabs.moveInSuccession([1e8], tabIds[0], {
      append: true,
      insert: true,
    });
    browser.test.assertEq(
      tabIds[1],
      (await browser.tabs.get(tabIds[0])).successorTabId,
      "If no tabs get inserted after the reference tab, it should keep its successor"
    );

    // Validation tests

    await browser.test.assertRejects(
      browser.tabs.moveInSuccession([tabIds[0], tabIds[1]], tabIds[0], {
        append: true,
      }),
      /Value of tabId must not occur in tabIds if append or insert is true/,
      "tabs.moveInSuccession should throw when tabId occurs in tabIds and insert is true"
    );

    return browser.tabs.remove(tabIds);
  });
});

add_task(function testMoveInSuccession_ignoreTabsInOtherWindows() {
  return runTabTest(2, async function({
    TAB_ID_NONE,
    tabIds,
    toTabIds,
    setSuccessors,
    verifySuccessors,
  }) {
    const anotherWindow = await browser.windows.create({
      url: Array.from({ length: 3 }, () => "about:blank"),
    });
    tabIds.push(...anotherWindow.tabs.map(t => t.id));

    await setSuccessors([1, 0, 3, 4, 2]);
    await browser.tabs.moveInSuccession([1, 3, 2].map(toTabIds), tabIds[4]);
    await verifySuccessors(
      [1, 0, 4, 2, TAB_ID_NONE],
      "first tab in another window"
    );

    await setSuccessors([1, 0, 3, 4, 2]);
    await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[4]);
    await verifySuccessors(
      [1, 0, 4, 2, TAB_ID_NONE],
      "middle tab in another window"
    );

    await setSuccessors([1, 0, 3, 4, 2]);
    await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds));
    await verifySuccessors(
      [1, 0, TAB_ID_NONE, 2, TAB_ID_NONE],
      "using the first tab to determine the window"
    );

    await setSuccessors([1, 0, 3, 4, 2]);
    await browser.tabs.moveInSuccession([1, 3, 2].map(toTabIds), tabIds[4], {
      append: true,
    });
    await verifySuccessors(
      [1, 0, TAB_ID_NONE, 2, 3],
      "first tab in another window, appending"
    );

    await setSuccessors([1, 0, 3, 4, 2]);
    await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[4], {
      append: true,
    });
    await verifySuccessors(
      [1, 0, TAB_ID_NONE, 2, 3],
      "middle tab in another window, appending"
    );

    return browser.tabs.remove(tabIds);
  });
});
