/* -*- 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);
  });
});
