/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/**
 * Tests the warning that is displayed when switching to background
 * tabs when sharing the browser window or screen
 */

// The number of tabs to have in the background for testing.
const NEW_BACKGROUND_TABS_TO_OPEN = 5;
const WARNING_PANEL_ID = "sharing-tabs-warning-panel";
const ALLOW_BUTTON_ID = "sharing-warning-proceed-to-tab";
const DISABLE_WARNING_FOR_SESSION_CHECKBOX_ID =
  "sharing-warning-disable-for-session";
const WINDOW_SHARING_HEADER_ID = "sharing-warning-window-panel-header";
const SCREEN_SHARING_HEADER_ID = "sharing-warning-screen-panel-header";
// The number of milliseconds we're willing to wait for the
// warning panel before we decide that it's not coming.
const WARNING_PANEL_TIMEOUT_MS = 1000;
const CTRL_TAB_RUO_PREF = "browser.ctrlTab.sortByRecentlyUsed";

/**
 * Common helper function that pretendToShareWindow and pretendToShareScreen
 * call into. Ensures that the first tab is selected, and then (optionally)
 * does the first "freebie" tab switch to the second tab.
 *
 * @param {boolean} doFirstTabSwitch - True if this function should take
 *   care of doing the "freebie" tab switch for you.
 * @return {Promise}
 * @resolves {undefined} - Once the simulation is set up.
 */
async function pretendToShareDisplay(doFirstTabSwitch) {
  Assert.equal(
    gBrowser.selectedTab,
    gBrowser.tabs[0],
    "Should start on the first tab."
  );

  webrtcUI.sharingDisplay = true;
  if (doFirstTabSwitch) {
    await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[1]);
  }
}

/**
 * Simulates the sharing of a particular browser window. The
 * simulation doesn't actually share the window over WebRTC, but
 * does enough to convince webrtcUI that the window is in the shared
 * window list.
 *
 * It is assumed that the first tab is the selected tab when calling
 * this function.
 *
 * This helper function can also automatically perform the first
 * "freebie" tab switch that never warns. This is its default behaviour.
 *
 * @param {DOM Window} aWindow - The window that we're simulating sharing.
 * @param {boolean} doFirstTabSwitch - True if this function should take
 *   care of doing the "freebie" tab switch for you. Defaults to true.
 * @return {Promise}
 * @resolves {undefined} - Once the simulation is set up.
 */
async function pretendToShareWindow(aWindow, doFirstTabSwitch = true) {
  // Poke into webrtcUI so that it thinks that the current browser
  // window is being shared.
  webrtcUI.sharedBrowserWindows.add(aWindow);
  await pretendToShareDisplay(doFirstTabSwitch);
}

/**
 * Simulates the sharing of the screen. The simulation doesn't actually share
 * the screen over WebRTC, but does enough to convince webrtcUI that the screen
 * is being shared.
 *
 * It is assumed that the first tab is the selected tab when calling
 * this function.
 *
 * This helper function can also automatically perform the first
 * "freebie" tab switch that never warns. This is its default behaviour.
 *
 * @param {boolean} doFirstTabSwitch - True if this function should take
 *   care of doing the "freebie" tab switch for you. Defaults to true.
 * @return {Promise}
 * @resolves {undefined} - Once the simulation is set up.
 */
async function pretendToShareScreen(doFirstTabSwitch = true) {
  // Poke into webrtcUI so that it thinks that the current screen is being
  // shared.
  webrtcUI.sharingScreen = true;
  await pretendToShareDisplay(doFirstTabSwitch);
}

/**
 * Resets webrtcUI's notion of what is being shared. This also clears
 * out any simulated shared windows, and resets any state that only
 * persists for a sharing session.
 *
 * This helper function will also:
 * 1. Switch back to the first tab if it's not already selected.
 * 2. Check if the tab switch warning panel is open, and if so, close it.
 *
 * @return {Promise}
 * @resolves {undefined} - Once the state is reset.
 */
async function resetDisplaySharingState() {
  let firstTabBC = gBrowser.browsers[0].browsingContext;
  webrtcUI.streamAddedOrRemoved(firstTabBC, { remove: true });

  if (gBrowser.selectedTab !== gBrowser.tabs[0]) {
    await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
  }

  let panel = document.getElementById(WARNING_PANEL_ID);
  if (panel && (panel.state == "open" || panel.state == "showing")) {
    info("Closing the warning panel.");
    let panelHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
    panel.hidePopup();
    await panelHidden;
  }
}

/**
 * Checks to make sure that a tab switch warning doesn't show
 * within WARNING_PANEL_TIMEOUT_MS milliseconds.
 *
 * @return {Promise}
 * @resolves {undefined} - Once the check is complete.
 */
async function ensureNoWarning() {
  let timerExpired = false;
  let sawWarning = false;

  let resolver;
  let timeoutOrPopupShowingPromise = new Promise(resolve => {
    resolver = resolve;
  });

  let onPopupShowing = event => {
    if (event.target.id == WARNING_PANEL_ID) {
      sawWarning = true;
      resolver();
    }
  };
  // The panel might not have been lazily-inserted yet, so we
  // attach the popupshowing handler to the window instead.
  window.addEventListener("popupshowing", onPopupShowing);

  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  let timer = setTimeout(() => {
    timerExpired = true;
    resolver();
  }, WARNING_PANEL_TIMEOUT_MS);

  await timeoutOrPopupShowingPromise;

  clearTimeout(timer);
  window.removeEventListener("popupshowing", onPopupShowing);

  Assert.ok(timerExpired, "Timer should have expired.");
  Assert.ok(!sawWarning, "Should not have shown the tab switch warning.");
}

/**
 * Checks to make sure that a tab switch warning appears for
 * a particular tab.
 *
 * @param {<xul:tab>} tab - The tab that the warning should be anchored to.
 * @return {Promise}
 * @resolves {undefined} - Once the check is complete.
 */
async function ensureWarning(tab) {
  let popupShowingEvent = await BrowserTestUtils.waitForEvent(
    window,
    "popupshowing",
    false,
    event => {
      return event.target.id == WARNING_PANEL_ID;
    }
  );
  let panel = popupShowingEvent.target;

  Assert.equal(
    panel.anchorNode,
    tab,
    "Expected the warning to be anchored to the right tab."
  );
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["privacy.webrtc.sharedTabWarning", true]],
  });

  // Loads up NEW_BACKGROUND_TABS_TO_OPEN background tabs at about:blank,
  // and waits until they're fully open.
  let uris = new Array(NEW_BACKGROUND_TABS_TO_OPEN).fill("about:blank");

  let loadPromises = Promise.all(
    uris.map(uri => BrowserTestUtils.waitForNewTab(gBrowser, uri, false, true))
  );

  gBrowser.loadTabs(uris, {
    inBackground: true,
    triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
  });

  await loadPromises;

  // Switches to the first tab and closes all of the rest.
  registerCleanupFunction(async () => {
    await resetDisplaySharingState();
    gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
  });
});

/**
 * Tests that when sharing the window that the first tab switch does _not_ show
 * the warning. This is because we presume that the first tab switch since
 * starting display sharing is for a tab that is intentionally being shared.
 */
add_task(async function testFirstTabSwitchAllowed() {
  pretendToShareWindow(window, false);

  let targetTab = gBrowser.tabs[1];

  let noWarningPromise = ensureNoWarning();
  await BrowserTestUtils.switchTab(gBrowser, targetTab);
  await noWarningPromise;

  await resetDisplaySharingState();
});

/**
 * Tests that the second tab switch after sharing is not allowed
 * without a warning. Also tests that the warning can "allow"
 * the tab switch to proceed, and that no warning is subsequently
 * shown for the "allowed" tab. Finally, ensures that if the sharing
 * session ends and a new session begins, that warnings are shown
 * again for the allowed tabs.
 */
add_task(async function testWarningOnSecondTabSwitch() {
  pretendToShareWindow(window);
  let originalTab = gBrowser.selectedTab;

  // pretendToShareWindow will have switched us to the second
  // tab automatically as the first "freebie" tab switch

  let targetTab = gBrowser.tabs[2];

  // Ensure that we show the warning on the second tab switch
  let warningPromise = ensureWarning(targetTab);
  await BrowserTestUtils.switchTab(gBrowser, targetTab);
  await warningPromise;

  // Not only should we have warned, but we should have prevented
  // the tab switch from occurring.
  Assert.equal(
    gBrowser.selectedTab,
    originalTab,
    "Should still be on the original tab."
  );

  // Now test the "Allow" button in the warning to make sure the tab
  // switch goes through.
  let tabSwitchPromise = BrowserTestUtils.waitForEvent(
    gBrowser,
    "TabSwitchDone"
  );
  let allowButton = document.getElementById(ALLOW_BUTTON_ID);
  allowButton.click();
  await tabSwitchPromise;

  Assert.equal(
    gBrowser.selectedTab,
    targetTab,
    "Should have switched tabs to the target."
  );

  // We shouldn't see a warning when switching back to that first
  // "freebie" tab.
  let noWarningPromise = ensureNoWarning();
  await BrowserTestUtils.switchTab(gBrowser, originalTab);
  await noWarningPromise;

  Assert.equal(
    gBrowser.selectedTab,
    originalTab,
    "Should have switched tabs back to the original tab."
  );

  // We shouldn't see a warning when switching back to the tab that
  // we had just allowed.
  noWarningPromise = ensureNoWarning();
  await BrowserTestUtils.switchTab(gBrowser, targetTab);
  await noWarningPromise;

  Assert.equal(
    gBrowser.selectedTab,
    targetTab,
    "Should have switched tabs back to the target tab."
  );

  // Reset the sharing state, and make sure that warnings can
  // be displayed again.
  await resetDisplaySharingState();
  pretendToShareWindow(window);

  // pretendToShareWindow will have switched us to the second
  // tab automatically as the first "freebie" tab switch
  //
  // Make sure we get the warning again when switching to the
  // target tab.
  warningPromise = ensureWarning(targetTab);
  await BrowserTestUtils.switchTab(gBrowser, targetTab);
  await warningPromise;

  await resetDisplaySharingState();
});

/**
 * Tests that warnings can be skipped for a session via the
 * checkbox in the warning panel. Also checks that once the
 * session ends and a new one begins that warnings are displayed
 * again.
 */
add_task(async function testDisableWarningForSession() {
  pretendToShareWindow(window);

  // pretendToShareWindow will have switched us to the second
  // tab automatically as the first "freebie" tab switch
  let targetTab = gBrowser.tabs[2];

  // Ensure that we show the warning on the second tab switch
  let warningPromise = ensureWarning(targetTab);
  await BrowserTestUtils.switchTab(gBrowser, targetTab);
  await warningPromise;

  // Check the checkbox to suppress warnings for the rest of this session.
  let checkbox = document.getElementById(
    DISABLE_WARNING_FOR_SESSION_CHECKBOX_ID
  );
  checkbox.checked = true;

  // Now test the "Allow" button in the warning to make sure the tab
  // switch goes through.
  let tabSwitchPromise = BrowserTestUtils.waitForEvent(
    gBrowser,
    "TabSwitchDone"
  );
  let allowButton = document.getElementById(ALLOW_BUTTON_ID);
  allowButton.click();
  await tabSwitchPromise;

  Assert.equal(
    gBrowser.selectedTab,
    targetTab,
    "Should have switched tabs to the target tab."
  );

  // Switching to the 4th and 5th tabs should now not show warnings.
  let noWarningPromise = ensureNoWarning();
  await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[3]);
  await noWarningPromise;

  noWarningPromise = ensureNoWarning();
  await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[4]);
  await noWarningPromise;

  // Reset the sharing state, and make sure that warnings can
  // be displayed again.
  await resetDisplaySharingState();
  pretendToShareWindow(window);

  // pretendToShareWindow will have switched us to the second
  // tab automatically as the first "freebie" tab switch
  //
  // Make sure we get the warning again when switching to the
  // target tab.
  warningPromise = ensureWarning(targetTab);
  await BrowserTestUtils.switchTab(gBrowser, targetTab);
  await warningPromise;

  await resetDisplaySharingState();
});

/**
 * Tests that we don't show a warning when sharing a different
 * window than the one we're switching tabs in.
 */
add_task(async function testOtherWindow() {
  let otherWin = await BrowserTestUtils.openNewBrowserWindow();
  await SimpleTest.promiseFocus(window);
  pretendToShareWindow(otherWin);

  // Switching to the 4th and 5th tabs should now not show warnings.
  let noWarningPromise = ensureNoWarning();
  await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[3]);
  await noWarningPromise;

  noWarningPromise = ensureNoWarning();
  await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[4]);
  await noWarningPromise;

  await BrowserTestUtils.closeWindow(otherWin);

  await resetDisplaySharingState();
});

/**
 * Tests that we show a different label when sharing the screen
 * vs when sharing a window.
 */
add_task(async function testWindowVsScreenLabel() {
  pretendToShareWindow(window);

  // pretendToShareWindow will have switched us to the second
  // tab automatically as the first "freebie" tab switch.
  // Let's now switch to the third tab.
  let targetTab = gBrowser.tabs[2];

  // Ensure that we show the warning on this second tab switch
  let warningPromise = ensureWarning(targetTab);
  await BrowserTestUtils.switchTab(gBrowser, targetTab);
  await warningPromise;

  let windowHeader = document.getElementById(WINDOW_SHARING_HEADER_ID);
  let screenHeader = document.getElementById(SCREEN_SHARING_HEADER_ID);
  Assert.ok(
    !BrowserTestUtils.is_hidden(windowHeader),
    "Should be showing window sharing header"
  );
  Assert.ok(
    BrowserTestUtils.is_hidden(screenHeader),
    "Should not be showing screen sharing header"
  );

  // Reset the sharing state, and then pretend to share the screen.
  await resetDisplaySharingState();
  pretendToShareScreen();

  // Ensure that we show the warning on this second tab switch
  warningPromise = ensureWarning(targetTab);
  await BrowserTestUtils.switchTab(gBrowser, targetTab);
  await warningPromise;

  Assert.ok(
    BrowserTestUtils.is_hidden(windowHeader),
    "Should not be showing window sharing header"
  );
  Assert.ok(
    !BrowserTestUtils.is_hidden(screenHeader),
    "Should be showing screen sharing header"
  );
  await resetDisplaySharingState();
});

/**
 * Tests that tab switching via the keyboard can also trigger the
 * tab switch warnings.
 */
add_task(async function testKeyboardTabSwitching() {
  let pressCtrlTab = async (expectPanel = false) => {
    let promise;
    if (expectPanel) {
      promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popupshown");
    } else {
      promise = BrowserTestUtils.waitForEvent(document, "keyup");
    }
    EventUtils.synthesizeKey("VK_TAB", {
      ctrlKey: true,
      shiftKey: false,
    });
    await promise;
  };

  let releaseCtrl = async () => {
    let promise;
    if (ctrlTab.isOpen) {
      promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popuphidden");
    } else {
      promise = BrowserTestUtils.waitForEvent(document, "keyup");
    }
    EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
    return promise;
  };

  // Ensure that the (on by default) ctrl-tab switch panel is enabled.
  await SpecialPowers.pushPrefEnv({
    set: [[CTRL_TAB_RUO_PREF, true]],
  });

  pretendToShareWindow(window);
  let originalTab = gBrowser.selectedTab;
  await pressCtrlTab(true);

  // The Ctrl-Tab MRU list should be:
  // 0: Second tab (currently selected)
  // 1: First tab
  // 2: Last tab
  //
  // Having pressed Ctrl-Tab once, 1 (First tab) is selected in the
  // panel. We want a tab that will warn, so let's hit Ctrl-Tab again
  // to choose 2 (Last tab).
  let targetTab = ctrlTab.tabList[2];
  await pressCtrlTab();

  let warningPromise = ensureWarning(targetTab);
  await releaseCtrl();
  await warningPromise;

  // Hide the warning without allowing the tab switch.
  let panel = document.getElementById(WARNING_PANEL_ID);
  panel.hidePopup();

  Assert.equal(
    gBrowser.selectedTab,
    originalTab,
    "Should not have changed from the original tab."
  );

  // Now switch to the in-order tab switching keyboard shortcut mode.
  await SpecialPowers.popPrefEnv();
  await SpecialPowers.pushPrefEnv({
    set: [[CTRL_TAB_RUO_PREF, false]],
  });

  // Hitting Ctrl-Tab should choose the _next_ tab over from
  // the originalTab, which should be the third tab.
  targetTab = gBrowser.tabs[2];

  warningPromise = ensureWarning(targetTab);
  await pressCtrlTab();
  await warningPromise;

  await resetDisplaySharingState();
});
