/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * Checks that restoring the last browser window in session is actually
 * working.
 *
 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=354894
 * @note It is implicitly tested that restoring the last window works when
 * non-browser windows are around. The "Run Tests" window as well as the main
 * browser window (wherein the test code gets executed) won't be considered
 * browser windows. To achiveve this said main browser window has it's windowtype
 * attribute modified so that it's not considered a browser window any longer.
 * This is crucial, because otherwise there would be two browser windows around,
 * said main test window and the one opened by the tests, and hence the new
 * logic wouldn't be executed at all.
 * @note Mac only tests the new notifications, as restoring the last window is
 * not enabled on that platform (platform shim; the application is kept running
 * although there are no windows left)
 * @note There is a difference when closing a browser window with
 * BrowserTryToCloseWindow() as opposed to close(). The former will make
 * nsSessionStore restore a window next time it gets a chance and will post
 * notifications. The latter won't.
 */

// The rejection "RecentWindow.getMostRecentBrowserWindow(...) is null" is left
// unhandled in some cases. This bug should be fixed, but for the moment this
// file is whitelisted.
//
// NOTE: Whitelisting a class of rejections should be limited. Normally you
//       should use "expectUncaughtRejection" to flag individual failures.
ChromeUtils.import("resource://testing-common/PromiseTestUtils.jsm", this);
PromiseTestUtils.whitelistRejectionsGlobally(/getMostRecentBrowserWindow/);

// Some urls that might be opened in tabs and/or popups
// Do not use about:blank:
// That one is reserved for special purposes in the tests
const TEST_URLS = ["about:mozilla", "about:buildconfig"];

// Number of -request notifications to except
// remember to adjust when adding new tests
const NOTIFICATIONS_EXPECTED = 6;

// Window features of popup windows
const POPUP_FEATURES = "toolbar=no,resizable=no,status=no";

// Window features of browser windows
const CHROME_FEATURES = "chrome,all,dialog=no";

const IS_MAC = navigator.platform.match(/Mac/);

/**
 * Returns an Object with two properties:
 *   open (int):
 *     A count of how many non-closed navigator:browser windows there are.
 *   winstates (int):
 *     A count of how many windows there are in the SessionStore state.
 */
function getBrowserWindowsCount() {
  let open = 0;
  let e = Services.wm.getEnumerator("navigator:browser");
  while (e.hasMoreElements()) {
    if (!e.getNext().closed)
      ++open;
  }

  let winstates = JSON.parse(ss.getBrowserState()).windows.length;

  return { open, winstates };
}

add_task(async function setup() {
  // Make sure we've only got one browser window to start with
  let { open, winstates } = getBrowserWindowsCount();
  is(open, 1, "Should only be one open window");
  is(winstates, 1, "Should only be one window state in SessionStore");

  // This test takes some time to run, and it could timeout randomly.
  // So we require a longer timeout. See bug 528219.
  requestLongerTimeout(3);

  // Make the main test window not count as a browser window any longer
  let oldWinType = document.documentElement.getAttribute("windowtype");
  document.documentElement.setAttribute("windowtype", "navigator:testrunner");

  registerCleanupFunction(() => {
    document.documentElement.setAttribute("windowtype", oldWinType);
  });
});

/**
 * Sets up one of our tests by setting the right preferences, and
 * then opening up a browser window preloaded with some tabs.
 *
 * @param options (Object)
 *        An object that can contain the following properties:
 *
 *        private:
 *          Whether or not the opened window should be private.
 *
 *        denyFirst:
 *          Whether or not the first window that attempts to close
 *          via closeWindowForRestoration should be denied.
 *
 * @param testFunction (Function*)
 *        A generator function that yields Promises to be run
 *        once the test has been set up.
 *
 * @returns Promise
 *        Resolves once the test has been cleaned up.
 */
let setupTest = async function(options, testFunction) {
  await pushPrefs(["browser.startup.page", 3],
                  ["browser.tabs.warnOnClose", false]);

  // Observe these, and also use to count the number of hits
  let observing = {
    "browser-lastwindow-close-requested": 0,
    "browser-lastwindow-close-granted": 0
  };

  /**
   * Helper: Will observe and handle the notifications for us
   */
  let hitCount = 0;
  function observer(aCancel, aTopic, aData) {
    // count so that we later may compare
    observing[aTopic]++;

    // handle some tests
    if (options.denyFirst && ++hitCount == 1) {
      aCancel.QueryInterface(Ci.nsISupportsPRBool).data = true;
    }
  }

  for (let o in observing) {
    Services.obs.addObserver(observer, o);
  }

  let private = options.private || false;
  let newWin = await promiseNewWindowLoaded({ private });

  injectTestTabs(newWin);

  await testFunction(newWin, observing);

  let count = getBrowserWindowsCount();
  is(count.open, 0, "Got right number of open windows");
  is(count.winstates, 1, "Got right number of stored window states");

  for (let o in observing) {
    Services.obs.removeObserver(observer, o);
  }

  await popPrefs();
};

/**
 * Loads a TEST_URLS into a browser window.
 *
 * @param win (Window)
 *        The browser window to load the tabs in
 */
function injectTestTabs(win) {
  TEST_URLS.forEach(function(url) {
    win.gBrowser.addTab(url);
  });
}

/**
 * Attempts to close a window via BrowserTryToCloseWindow so that
 * we get the browser-lastwindow-close-requested and
 * browser-lastwindow-close-granted observer notifications.
 *
 * @param win (Window)
 *        The window to try to close
 * @returns Promise
 *        Resolves to true if the window closed, or false if the window
 *        was denied the ability to close.
 */
function closeWindowForRestoration(win) {
  return new Promise((resolve) => {
    let closePromise = BrowserTestUtils.windowClosed(win);
    win.BrowserTryToCloseWindow();
    if (!win.closed) {
      resolve(false);
      return;
    }

    closePromise.then(() => {
      resolve(true);
    });
  });
}

/**
 * Normal in-session restore
 *
 * @note: Non-Mac only
 *
 * Should do the following:
 *  1. Open a new browser window
 *  2. Add some tabs
 *  3. Close that window
 *  4. Opening another window
 *  5. Checks that state is restored
 */
add_task(async function test_open_close_normal() {
  if (IS_MAC) {
    return;
  }

  await setupTest({ denyFirst: true }, async function(newWin, obs) {
    let closed = await closeWindowForRestoration(newWin);
    ok(!closed, "First close request should have been denied");

    closed = await closeWindowForRestoration(newWin);
    ok(closed, "Second close request should be accepted");

    newWin = await promiseNewWindowLoaded();
    is(newWin.gBrowser.browsers.length, TEST_URLS.length + 2,
       "Restored window in-session with otherpopup windows around");

    // Note that this will not result in the the browser-lastwindow-close
    // notifications firing for this other newWin.
    await BrowserTestUtils.closeWindow(newWin);

    // setupTest gave us a window which was denied for closing once, and then
    // closed.
    is(obs["browser-lastwindow-close-requested"], 2,
       "Got expected browser-lastwindow-close-requested notifications");
    is(obs["browser-lastwindow-close-granted"], 1,
       "Got expected browser-lastwindow-close-granted notifications");
  });
});

/**
 * PrivateBrowsing in-session restore
 *
 * @note: Non-Mac only
 *
 * Should do the following:
 *  1. Open a new browser window A
 *  2. Add some tabs
 *  3. Close the window A as the last window
 *  4. Open a private browsing window B
 *  5. Make sure that B didn't restore the tabs from A
 *  6. Close private browsing window B
 *  7. Open a new window C
 *  8. Make sure that new window C has restored tabs from A
 */
add_task(async function test_open_close_private_browsing() {
  if (IS_MAC) {
    return;
  }

  await setupTest({}, async function(newWin, obs) {
    let closed = await closeWindowForRestoration(newWin);
    ok(closed, "Should be able to close the window");

    newWin = await promiseNewWindowLoaded({private: true});
    is(newWin.gBrowser.browsers.length, 1,
       "Did not restore in private browing mode");

    closed = await closeWindowForRestoration(newWin);
    ok(closed, "Should be able to close the window");

    newWin = await promiseNewWindowLoaded();
    is(newWin.gBrowser.browsers.length, TEST_URLS.length + 2,
       "Restored tabs in a new non-private window");

    // Note that this will not result in the the browser-lastwindow-close
    // notifications firing for this other newWin.
    await BrowserTestUtils.closeWindow(newWin);

    // We closed two windows with closeWindowForRestoration, and both
    // should have been successful.
    is(obs["browser-lastwindow-close-requested"], 2,
       "Got expected browser-lastwindow-close-requested notifications");
    is(obs["browser-lastwindow-close-granted"], 2,
       "Got expected browser-lastwindow-close-granted notifications");
  });
});

/**
 * Open some popup windows to check those aren't restored, but the browser
 * window is.
 *
 * @note: Non-Mac only
 *
 * Should do the following:
 *  1. Open a new browser window
 *  2. Add some tabs
 *  3. Open some popups
 *  4. Add another tab to one popup (so that it gets stored) and close it again
 *  5. Close the browser window
 *  6. Open another browser window
 *  7. Make sure that the tabs of the closed browser window, but not the popup,
 *     are restored
 */
add_task(async function test_open_close_window_and_popup() {
  if (IS_MAC) {
    return;
  }

  await setupTest({}, async function(newWin, obs) {
    let popupPromise = BrowserTestUtils.waitForNewWindow();
    openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[0]);
    let popup = await popupPromise;

    let popup2Promise = BrowserTestUtils.waitForNewWindow();
    openDialog(location, "popup2", POPUP_FEATURES, TEST_URLS[1]);
    let popup2 = await popup2Promise;

    popup2.gBrowser.addTab(TEST_URLS[0]);

    let closed = await closeWindowForRestoration(newWin);
    ok(closed, "Should be able to close the window");

    await BrowserTestUtils.closeWindow(popup2);

    newWin = await promiseNewWindowLoaded();

    is(newWin.gBrowser.browsers.length, TEST_URLS.length + 2,
       "Restored window and associated tabs in session");

    await BrowserTestUtils.closeWindow(popup);
    await BrowserTestUtils.closeWindow(newWin);

    // We closed one window with closeWindowForRestoration, and it should
    // have been successful.
    is(obs["browser-lastwindow-close-requested"], 1,
       "Got expected browser-lastwindow-close-requested notifications");
    is(obs["browser-lastwindow-close-granted"], 1,
       "Got expected browser-lastwindow-close-granted notifications");
  });
});

/**
 * Open some popup window to check it isn't restored. Instead nothing at all
 * should be restored
 *
 * @note: Non-Mac only
 *
 * Should do the following:
 *  1. Open a popup
 *  2. Add another tab to the popup (so that it gets stored) and close it again
 *  3. Open a window
 *  4. Check that nothing at all is restored
 *  5. Open two browser windows and close them again
 *  6. undoCloseWindow() one
 *  7. Open another browser window
 *  8. Check that nothing at all is restored
 */
add_task(async function test_open_close_only_popup() {
  if (IS_MAC) {
    return;
  }

  await setupTest({}, async function(newWin, obs) {
    // We actually don't care about the initial window in this test.
    await BrowserTestUtils.closeWindow(newWin);

    // This will cause nsSessionStore to restore a window the next time it
    // gets a chance.
    let popupPromise = BrowserTestUtils.waitForNewWindow();
    openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[1]);
    let popup = await popupPromise;

    is(popup.gBrowser.browsers.length, 1,
       "Did not restore the popup window (1)");

    let closed = await closeWindowForRestoration(popup);
    ok(closed, "Should be able to close the window");

    popupPromise = BrowserTestUtils.waitForNewWindow();
    openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[1]);
    popup = await popupPromise;

    popup.gBrowser.addTab(TEST_URLS[0]);
    is(popup.gBrowser.browsers.length, 2,
       "Did not restore to the popup window (2)");

    await BrowserTestUtils.closeWindow(popup);

    newWin = await promiseNewWindowLoaded();
    isnot(newWin.gBrowser.browsers.length, 2,
          "Did not restore the popup window");
    is(TEST_URLS.indexOf(newWin.gBrowser.browsers[0].currentURI.spec), -1,
        "Did not restore the popup window (2)");
    await BrowserTestUtils.closeWindow(newWin);

    // We closed one popup window with closeWindowForRestoration, and popup
    // windows should never fire the browser-lastwindow notifications.
    is(obs["browser-lastwindow-close-requested"], 0,
       "Got expected browser-lastwindow-close-requested notifications");
    is(obs["browser-lastwindow-close-granted"], 0,
       "Got expected browser-lastwindow-close-granted notifications");
  });
});

/**
 * Open some windows and do undoCloseWindow. This should prevent any
 * restoring later in the test
 *
 * @note: Non-Mac only
 *
 * Should do the following:
 *  1. Open two browser windows and close them again
 *  2. undoCloseWindow() one
 *  3. Open another browser window
 *  4. Make sure nothing at all is restored
 */
add_task(async function test_open_close_restore_from_popup() {
  if (IS_MAC) {
    return;
  }

  await setupTest({}, async function(newWin, obs) {
    let newWin2 = await promiseNewWindowLoaded();
    await injectTestTabs(newWin2);

    let closed = await closeWindowForRestoration(newWin);
    ok(closed, "Should be able to close the window");
    closed = await closeWindowForRestoration(newWin2);
    ok(closed, "Should be able to close the window");

    let counts = getBrowserWindowsCount();
    is(counts.open, 0, "Got right number of open windows");
    is(counts.winstates, 1, "Got right number of window states");

    newWin = undoCloseWindow(0);
    await BrowserTestUtils.waitForEvent(newWin, "load");

    // Make sure we wait until this window is restored.
    await BrowserTestUtils.waitForEvent(newWin.gBrowser.tabContainer,
                                        "SSTabRestored");

    newWin2 = await promiseNewWindowLoaded();

    is(newWin2.gBrowser.browsers.length, 1,
       "Did not restore, as undoCloseWindow() was last called");
    is(TEST_URLS.indexOf(newWin2.gBrowser.browsers[0].currentURI.spec), -1,
       "Did not restore, as undoCloseWindow() was last called (2)");

    counts = getBrowserWindowsCount();
    is(counts.open, 2, "Got right number of open windows");
    is(counts.winstates, 3, "Got right number of window states");

    await BrowserTestUtils.closeWindow(newWin);
    await BrowserTestUtils.closeWindow(newWin2);

    counts = getBrowserWindowsCount();
    is(counts.open, 0, "Got right number of open windows");
    is(counts.winstates, 1, "Got right number of window states");
  });
});

/**
 * Test if closing can be denied on Mac.
 * @note: Mac only
 */
add_task(async function test_mac_notifications() {
  if (!IS_MAC) {
    return;
  }

  await setupTest({ denyFirst: true }, async function(newWin, obs) {
    let closed = await closeWindowForRestoration(newWin);
    ok(!closed, "First close attempt should be denied");
    closed = await closeWindowForRestoration(newWin);
    ok(closed, "Second close attempt should be granted");

    // We tried closing once, and got denied. Then we tried again and
    // succeeded. That means 2 close requests, and 1 close granted.
    is(obs["browser-lastwindow-close-requested"], 2,
       "Got expected browser-lastwindow-close-requested notifications");
    is(obs["browser-lastwindow-close-granted"], 1,
       "Got expected browser-lastwindow-close-granted notifications");
  });
});
