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

const { TabUnloader } = ChromeUtils.importESModule(
  "moz-src:///browser/components/tabbrowser/TabUnloader.sys.mjs"
);

const BASE_URL = "https://example.com/browser/browser/modules/test/browser/";

async function play(tab) {
  let browser = tab.linkedBrowser;

  let waitForAudioPromise = BrowserTestUtils.waitForEvent(
    tab,
    "TabAttrModified",
    false,
    event => {
      return (
        event.detail.changed.includes("soundplaying") &&
        tab.hasAttribute("soundplaying")
      );
    }
  );

  await SpecialPowers.spawn(browser, [], async function () {
    let audio = content.document.querySelector("audio");
    await audio.play();
  });

  await waitForAudioPromise;
}

async function addTab(win = window) {
  return BrowserTestUtils.openNewForegroundTab({
    gBrowser: win.gBrowser,
    url: BASE_URL + "dummy_page.html",
    waitForLoad: true,
  });
}

async function addPrivTab(win = window) {
  const tab = BrowserTestUtils.addTab(
    win.gBrowser,
    BASE_URL + "dummy_page.html"
  );
  const browser = win.gBrowser.getBrowserForTab(tab);
  await BrowserTestUtils.browserLoaded(browser);
  return tab;
}

async function addAudioTab(win = window) {
  let tab = await BrowserTestUtils.openNewForegroundTab({
    gBrowser: win.gBrowser,
    url: BASE_URL + "file_mediaPlayback.html",
    waitForLoad: true,
    waitForStateStop: true,
  });

  await play(tab);
  return tab;
}

async function addWebRTCTab(win = window) {
  let popupPromise = new Promise(resolve => {
    win.PopupNotifications.panel.addEventListener(
      "popupshown",
      function () {
        executeSoon(resolve);
      },
      { once: true }
    );
  });

  let tab = await BrowserTestUtils.openNewForegroundTab({
    gBrowser: win.gBrowser,
    url: BASE_URL + "file_webrtc.html",
    waitForLoad: true,
    waitForStateStop: true,
  });

  await popupPromise;

  let recordingPromise = BrowserTestUtils.contentTopicObserved(
    tab.linkedBrowser.browsingContext,
    "recording-device-events"
  );
  win.PopupNotifications.panel.firstElementChild.button.click();
  await recordingPromise;

  return tab;
}

async function pressure() {
  let tabDiscarded = BrowserTestUtils.waitForEvent(
    document,
    "TabBrowserDiscarded",
    true
  );
  TabUnloader.unloadTabAsync(null);
  return tabDiscarded;
}

function pressureAndObserve(aExpectedTopic) {
  const promise = new Promise(resolve => {
    const observer = {
      QueryInterface: ChromeUtils.generateQI([
        "nsIObserver",
        "nsISupportsWeakReference",
      ]),
      observe(aSubject, aTopicInner, aData) {
        if (aTopicInner == aExpectedTopic) {
          Services.obs.removeObserver(observer, aTopicInner);
          resolve(aData);
        }
      },
    };
    Services.obs.addObserver(observer, aExpectedTopic);
  });
  TabUnloader.unloadTabAsync(null);
  return promise;
}

async function compareTabOrder(expectedOrder) {
  let tabInfo = await TabUnloader.getSortedTabs(null);

  is(
    tabInfo.length,
    expectedOrder.length,
    "right number of tabs in discard sort list"
  );
  for (let idx = 0; idx < expectedOrder.length; idx++) {
    is(tabInfo[idx].tab, expectedOrder[idx], "index " + idx + " is correct");
  }
}

const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
const PREF_AUDIO_LOOPBACK = "media.audio_loopback_dev";
const PREF_VIDEO_LOOPBACK = "media.video_loopback_dev";
const PREF_FAKE_STREAMS = "media.navigator.streams.fake";
const PREF_ENABLE_UNLOADER = "browser.tabs.unloadOnLowMemory";
const PREF_MAC_LOW_MEM_RESPONSE = "browser.lowMemoryResponseMask";

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["test.wait300msAfterTabSwitch", true]],
  });
});

add_task(async function test() {
  registerCleanupFunction(() => {
    Services.prefs.clearUserPref(PREF_ENABLE_UNLOADER);
    if (AppConstants.platform == "macosx") {
      Services.prefs.clearUserPref(PREF_MAC_LOW_MEM_RESPONSE);
    }
  });
  Services.prefs.setBoolPref(PREF_ENABLE_UNLOADER, true);

  // On Mac, tab unloading and memory pressure notifications are limited
  // to Nightly so force them on for this test for non-Nightly builds. i.e.,
  // tests on Release and Beta builds. Mac tab unloading and memory pressure
  // notifications require this pref to be set.
  if (AppConstants.platform == "macosx") {
    Services.prefs.setIntPref(PREF_MAC_LOW_MEM_RESPONSE, 3);
  }

  TabUnloader.init();

  // Set some WebRTC simulation preferences.
  let prefs = [
    [PREF_PERMISSION_FAKE, true],
    [PREF_AUDIO_LOOPBACK, ""],
    [PREF_VIDEO_LOOPBACK, ""],
    [PREF_FAKE_STREAMS, true],
  ];
  await SpecialPowers.pushPrefEnv({ set: prefs });

  // Set up 6 tabs, three normal ones, one pinned, one playing sound and one
  // pinned playing sound
  let tab0 = gBrowser.tabs[0];
  let tab1 = await addTab();
  let tab2 = await addTab();
  let pinnedTab = await addTab();
  gBrowser.pinTab(pinnedTab);
  let soundTab = await addAudioTab();
  let pinnedSoundTab = await addAudioTab();
  gBrowser.pinTab(pinnedSoundTab);

  // Open a new private window and add a tab
  const windowPriv = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });
  const tabPriv0 = windowPriv.gBrowser.tabs[0];
  const tabPriv1 = await addPrivTab(windowPriv);

  // Move the original window to the foreground to pass the tests
  gBrowser.selectedTab = tab0;
  tab0.ownerGlobal.focus();

  // Pretend we've visited the tabs
  await BrowserTestUtils.switchTab(windowPriv.gBrowser, tabPriv1);
  await BrowserTestUtils.switchTab(windowPriv.gBrowser, tabPriv0);
  await BrowserTestUtils.switchTab(gBrowser, tab1);
  await BrowserTestUtils.switchTab(gBrowser, tab2);
  await BrowserTestUtils.switchTab(gBrowser, pinnedTab);
  await BrowserTestUtils.switchTab(gBrowser, soundTab);
  await BrowserTestUtils.switchTab(gBrowser, pinnedSoundTab);
  await BrowserTestUtils.switchTab(gBrowser, tab0);

  // Checks the tabs are in the state we expect them to be
  ok(pinnedTab.pinned, "tab is pinned");
  ok(pinnedSoundTab.soundPlaying, "tab is playing sound");
  ok(
    pinnedSoundTab.pinned && pinnedSoundTab.soundPlaying,
    "tab is pinned and playing sound"
  );

  await compareTabOrder([
    tab1,
    tab2,
    pinnedTab,
    tabPriv1,
    soundTab,
    tab0,
    pinnedSoundTab,
    tabPriv0,
  ]);

  // Check that the tabs are present
  ok(
    tab1.linkedPanel &&
      tab2.linkedPanel &&
      pinnedTab.linkedPanel &&
      soundTab.linkedPanel &&
      pinnedSoundTab.linkedPanel &&
      tabPriv0.linkedPanel &&
      tabPriv1.linkedPanel,
    "tabs are present"
  );

  // Check that low-memory memory-pressure events unload tabs
  await pressure();
  ok(
    !tab1.linkedPanel,
    "low-memory memory-pressure notification unloaded the LRU tab"
  );

  await compareTabOrder([
    tab2,
    pinnedTab,
    tabPriv1,
    soundTab,
    tab0,
    pinnedSoundTab,
    tabPriv0,
  ]);

  // If no normal tab is available unload pinned tabs
  await pressure();
  ok(!tab2.linkedPanel, "unloaded a second tab in LRU order");
  await compareTabOrder([
    pinnedTab,
    tabPriv1,
    soundTab,
    tab0,
    pinnedSoundTab,
    tabPriv0,
  ]);

  ok(soundTab.soundPlaying, "tab is still playing sound");

  await pressure();
  ok(!pinnedTab.linkedPanel, "unloaded a pinned tab");
  await compareTabOrder([tabPriv1, soundTab, tab0, pinnedSoundTab, tabPriv0]);

  ok(pinnedSoundTab.soundPlaying, "tab is still playing sound");

  // There are no unloadable tabs.
  TabUnloader.unloadTabAsync(null);
  ok(tabPriv1.linkedPanel, "a tab in a private window is never unloaded");

  const histogram = TelemetryTestUtils.getAndClearHistogram(
    "TAB_UNLOAD_TO_RELOAD"
  );

  // It's possible that we're already in the memory-pressure state
  // and we may receive the "ongoing" message.
  const message = await pressureAndObserve("memory-pressure");
  Assert.ok(
    message == "low-memory" || message == "low-memory-ongoing",
    "observed the memory-pressure notification because of no discardable tab"
  );

  // Add a WebRTC tab and another sound tab.
  let webrtcTab = await addWebRTCTab();
  let anotherSoundTab = await addAudioTab();

  await BrowserTestUtils.switchTab(gBrowser, tab1);
  await BrowserTestUtils.switchTab(gBrowser, pinnedTab);

  const hist = histogram.snapshot();
  const numEvents = Object.values(hist.values).reduce((a, b) => a + b);
  Assert.equal(numEvents, 2, "two tabs have been reloaded.");

  // tab0 has never been unloaded.  No data is added to the histogram.
  await BrowserTestUtils.switchTab(gBrowser, tab0);

  await compareTabOrder([
    tab1,
    pinnedTab,
    tabPriv1,
    soundTab,
    webrtcTab,
    anotherSoundTab,
    tab0,
    pinnedSoundTab,
    tabPriv0,
  ]);

  await BrowserTestUtils.closeWindow(windowPriv);

  let window2 = await BrowserTestUtils.openNewBrowserWindow();
  let win2tab1 = window2.gBrowser.selectedTab;
  let win2tab2 = await addTab(window2);
  let win2winrtcTab = await addWebRTCTab(window2);
  let win2tab3 = await addTab(window2);

  await compareTabOrder([
    tab1,
    win2tab1,
    win2tab2,
    pinnedTab,
    soundTab,
    webrtcTab,
    anotherSoundTab,
    win2winrtcTab,
    tab0,
    win2tab3,
    pinnedSoundTab,
  ]);

  await BrowserTestUtils.closeWindow(window2);

  await compareTabOrder([
    tab1,
    pinnedTab,
    soundTab,
    webrtcTab,
    anotherSoundTab,
    tab0,
    pinnedSoundTab,
  ]);

  // Cleanup
  BrowserTestUtils.removeTab(tab1);
  BrowserTestUtils.removeTab(tab2);
  BrowserTestUtils.removeTab(pinnedTab);
  BrowserTestUtils.removeTab(soundTab);
  BrowserTestUtils.removeTab(pinnedSoundTab);
  BrowserTestUtils.removeTab(webrtcTab);
  BrowserTestUtils.removeTab(anotherSoundTab);

  await awaitWebRTCClose();
});

// Wait for the WebRTC indicator window to close.
function awaitWebRTCClose() {
  if (
    Services.prefs.getBoolPref("privacy.webrtc.legacyGlobalIndicator", false) ||
    AppConstants.platform == "macosx"
  ) {
    return null;
  }

  let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator");
  if (!win) {
    return null;
  }

  return new Promise(resolve => {
    win.addEventListener("unload", function listener(e) {
      if (e.target == win.document) {
        win.removeEventListener("unload", listener);
        executeSoon(resolve);
      }
    });
  });
}
