/* eslint-disable mozilla/no-arbitrary-setTimeout */
const PAGE =
  "https://example.com/browser/browser/base/content/test/tabs/file_mediaPlayback.html";
const TABATTR_REMOVAL_PREFNAME = "browser.tabs.delayHidingAudioPlayingIconMS";
const INITIAL_TABATTR_REMOVAL_DELAY_MS = Services.prefs.getIntPref(
  TABATTR_REMOVAL_PREFNAME
);

async function pause(tab, options) {
  let extendedDelay = options && options.extendedDelay;
  if (extendedDelay) {
    // Use 10s to remove possibility of race condition with attr removal.
    Services.prefs.setIntPref(TABATTR_REMOVAL_PREFNAME, 10000);
  }

  try {
    let browser = tab.linkedBrowser;
    let awaitDOMAudioPlaybackStopped;
    if (!browser.audioMuted) {
      awaitDOMAudioPlaybackStopped = BrowserTestUtils.waitForEvent(
        browser,
        "DOMAudioPlaybackStopped",
        "DOMAudioPlaybackStopped event should get fired after pause"
      );
    }
    await SpecialPowers.spawn(browser, [], async function () {
      let audio = content.document.querySelector("audio");
      audio.pause();
    });

    // If the tab has already be muted, it means the tab won't have soundplaying,
    // so we don't need to check this attribute.
    if (browser.audioMuted) {
      return;
    }

    if (extendedDelay) {
      ok(
        tab.hasAttribute("soundplaying"),
        "The tab should still have the soundplaying attribute immediately after pausing"
      );

      await awaitDOMAudioPlaybackStopped;
      ok(
        tab.hasAttribute("soundplaying"),
        "The tab should still have the soundplaying attribute immediately after DOMAudioPlaybackStopped"
      );
    }

    await wait_for_tab_playing_event(tab, false);
    ok(
      !tab.hasAttribute("soundplaying"),
      "The tab should not have the soundplaying attribute after the timeout has resolved"
    );
  } finally {
    // Make sure other tests don't timeout if an exception gets thrown above.
    // Need to use setIntPref instead of clearUserPref because
    // testing/profiles/common/user.js overrides the default value to help this and
    // other tests run faster.
    Services.prefs.setIntPref(
      TABATTR_REMOVAL_PREFNAME,
      INITIAL_TABATTR_REMOVAL_DELAY_MS
    );
  }
}

async function hide_tab(tab) {
  let tabHidden = BrowserTestUtils.waitForEvent(tab, "TabHide");
  gBrowser.hideTab(tab);
  return tabHidden;
}

async function show_tab(tab) {
  let tabShown = BrowserTestUtils.waitForEvent(tab, "TabShow");
  gBrowser.showTab(tab);
  return tabShown;
}

async function test_tooltip(icon, expectedTooltip, isActiveTab, tab) {
  let tooltip = document.getElementById("tabbrowser-tab-tooltip");

  let tabContent = tab.querySelector(".tab-content");
  await hover_icon(tabContent, tooltip);

  await hover_icon(icon, tooltip);
  if (isActiveTab) {
    // The active tab should have the keybinding shortcut in the tooltip.
    // We check this by ensuring that the strings are not equal but the expected
    // message appears in the beginning.
    isnot(
      tooltip.getAttribute("label"),
      expectedTooltip,
      "Tooltips should not be equal"
    );
    is(
      tooltip.getAttribute("label").indexOf(expectedTooltip),
      0,
      "Correct tooltip expected"
    );
  } else {
    is(
      tooltip.getAttribute("label"),
      expectedTooltip,
      "Tooltips should not be equal"
    );
  }
  leave_icon(icon);
}

function get_tab_state(tab) {
  return JSON.parse(SessionStore.getTabState(tab));
}

async function test_muting_using_menu(tab, expectMuted) {
  // Show the popup menu
  let contextMenu = document.getElementById("tabContextMenu");
  let popupShownPromise = BrowserTestUtils.waitForEvent(
    contextMenu,
    "popupshown"
  );
  EventUtils.synthesizeMouseAtCenter(tab, { type: "contextmenu", button: 2 });
  await popupShownPromise;

  // Check the menu
  let expectedLabel = expectMuted ? "Unmute Tab" : "Mute Tab";
  let expectedAccessKey = expectMuted ? "m" : "M";
  let toggleMute = document.getElementById("context_toggleMuteTab");
  is(toggleMute.label, expectedLabel, "Correct label expected");
  is(toggleMute.accessKey, expectedAccessKey, "Correct accessKey expected");

  is(
    toggleMute.hasAttribute("muted"),
    expectMuted,
    "Should have the correct state for the muted attribute"
  );
  ok(
    !toggleMute.hasAttribute("soundplaying"),
    "Should not have the soundplaying attribute"
  );

  await play(tab);

  is(
    toggleMute.hasAttribute("muted"),
    expectMuted,
    "Should have the correct state for the muted attribute"
  );
  is(
    !toggleMute.hasAttribute("soundplaying"),
    expectMuted,
    "The value of soundplaying attribute is incorrect"
  );

  await pause(tab);

  is(
    toggleMute.hasAttribute("muted"),
    expectMuted,
    "Should have the correct state for the muted attribute"
  );
  ok(
    !toggleMute.hasAttribute("soundplaying"),
    "Should not have the soundplaying attribute"
  );

  // Click on the menu and wait for the tab to be muted.
  let mutedPromise = get_wait_for_mute_promise(tab, !expectMuted);
  let popupHiddenPromise = BrowserTestUtils.waitForEvent(
    contextMenu,
    "popuphidden"
  );
  contextMenu.activateItem(toggleMute);
  await popupHiddenPromise;
  await mutedPromise;
}

async function test_playing_icon_on_tab(tab, browser, isPinned) {
  let icon = isPinned ? tab.overlayIcon : tab.overlayIcon;
  let isActiveTab = tab === gBrowser.selectedTab;

  await play(tab);

  await test_tooltip(icon, "Mute tab", isActiveTab, tab);

  ok(
    !("muted" in get_tab_state(tab)),
    "No muted attribute should be persisted"
  );
  ok(
    !("muteReason" in get_tab_state(tab)),
    "No muteReason property should be persisted"
  );

  await test_mute_tab(tab, icon, true);

  ok("muted" in get_tab_state(tab), "Muted attribute should be persisted");
  ok(
    "muteReason" in get_tab_state(tab),
    "muteReason property should be persisted"
  );

  await test_tooltip(icon, "Unmute tab", isActiveTab, tab);

  await test_mute_tab(tab, icon, false);

  ok(
    !("muted" in get_tab_state(tab)),
    "No muted attribute should be persisted"
  );
  ok(
    !("muteReason" in get_tab_state(tab)),
    "No muteReason property should be persisted"
  );

  await test_tooltip(icon, "Mute tab", isActiveTab, tab);

  await test_mute_tab(tab, icon, true);

  await pause(tab);

  ok(
    tab.hasAttribute("muted") && !tab.hasAttribute("soundplaying"),
    "Tab should still be muted but not playing"
  );
  ok(
    tab.muted && !tab.soundPlaying,
    "Tab should still be muted but not playing"
  );

  await test_tooltip(icon, "Unmute tab", isActiveTab, tab);

  await test_mute_tab(tab, icon, false);

  ok(
    !tab.hasAttribute("muted") && !tab.hasAttribute("soundplaying"),
    "Tab should not be be muted or playing"
  );
  ok(!tab.muted && !tab.soundPlaying, "Tab should not be be muted or playing");

  // Make sure it's possible to mute using the context menu.
  await test_muting_using_menu(tab, false);

  // Make sure it's possible to unmute using the context menu.
  await test_muting_using_menu(tab, true);
}

async function test_playing_icon_on_hidden_tab(tab) {
  let oldSelectedTab = gBrowser.selectedTab;
  let otherTabs = [
    await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true),
    await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true),
  ];
  let tabContainer = tab.container;
  let alltabsButton = document.getElementById("alltabs-button");
  let alltabsBadge = alltabsButton.badgeLabel;

  function assertIconShowing() {
    is(
      getComputedStyle(alltabsBadge).backgroundImage,
      'url("chrome://browser/skin/tabbrowser/tab-audio-playing-small.svg")',
      "The audio playing icon is shown"
    );
    is(
      tabContainer.getAttribute("hiddensoundplaying"),
      "true",
      "There are hidden audio tabs"
    );
  }

  function assertIconHidden() {
    is(
      getComputedStyle(alltabsBadge).backgroundImage,
      "none",
      "The audio playing icon is hidden"
    );
    ok(
      !tabContainer.hasAttribute("hiddensoundplaying"),
      "There are no hidden audio tabs"
    );
  }

  // Keep the passed in tab selected.
  gBrowser.selectedTab = tab;

  // Play sound in the other two (visible) tabs.
  await play(otherTabs[0]);
  await play(otherTabs[1]);
  assertIconHidden();

  // Hide one of the noisy tabs, we see the icon.
  await hide_tab(otherTabs[0]);
  assertIconShowing();

  // Hiding the other tab keeps the icon.
  await hide_tab(otherTabs[1]);
  assertIconShowing();

  // Pausing both tabs will hide the icon.
  await pause(otherTabs[0]);
  assertIconShowing();
  await pause(otherTabs[1]);
  assertIconHidden();

  // The icon returns when audio starts again.
  await play(otherTabs[0]);
  await play(otherTabs[1]);
  assertIconShowing();

  // There is still an icon after hiding one tab.
  await show_tab(otherTabs[0]);
  assertIconShowing();

  // The icon is hidden when both of the tabs are shown.
  await show_tab(otherTabs[1]);
  assertIconHidden();

  await BrowserTestUtils.removeTab(otherTabs[0]);
  await BrowserTestUtils.removeTab(otherTabs[1]);

  // Make sure we didn't change the selected tab.
  gBrowser.selectedTab = oldSelectedTab;
}

async function test_swapped_browser_while_playing(oldTab, newBrowser) {
  // The tab was muted so it won't have soundplaying attribute even it's playing.
  ok(
    oldTab.hasAttribute("muted"),
    "Expected the correct muted attribute on the old tab"
  );
  is(
    oldTab.muteReason,
    null,
    "Expected the correct muteReason attribute on the old tab"
  );
  ok(
    !oldTab.hasAttribute("soundplaying"),
    "Expected the correct soundplaying attribute on the old tab"
  );

  let newTab = gBrowser.getTabForBrowser(newBrowser);
  let AttrChangePromise = BrowserTestUtils.waitForEvent(
    newTab,
    "TabAttrModified",
    false,
    event => {
      return event.detail.changed.includes("muted");
    }
  );

  gBrowser.swapBrowsersAndCloseOther(newTab, oldTab);
  await AttrChangePromise;

  ok(
    newTab.hasAttribute("muted"),
    "Expected the correct muted attribute on the new tab"
  );
  is(
    newTab.muteReason,
    null,
    "Expected the correct muteReason property on the new tab"
  );
  ok(
    !newTab.hasAttribute("soundplaying"),
    "Expected the correct soundplaying attribute on the new tab"
  );

  await test_tooltip(newTab.overlayIcon, "Unmute tab", true, newTab);
}

async function test_swapped_browser_while_not_playing(oldTab, newBrowser) {
  ok(
    oldTab.hasAttribute("muted"),
    "Expected the correct muted attribute on the old tab"
  );
  is(
    oldTab.muteReason,
    null,
    "Expected the correct muteReason property on the old tab"
  );
  ok(
    !oldTab.hasAttribute("soundplaying"),
    "Expected the correct soundplaying attribute on the old tab"
  );

  let newTab = gBrowser.getTabForBrowser(newBrowser);
  let AttrChangePromise = BrowserTestUtils.waitForEvent(
    newTab,
    "TabAttrModified",
    false,
    event => {
      return event.detail.changed.includes("muted");
    }
  );

  let AudioPlaybackPromise = new Promise(resolve => {
    let observer = (subject, topic, data) => {
      ok(false, "Should not see an audio-playback notification");
    };
    Services.obs.addObserver(observer, "audio-playback");
    setTimeout(() => {
      Services.obs.removeObserver(observer, "audio-playback");
      resolve();
    }, 100);
  });

  gBrowser.swapBrowsersAndCloseOther(newTab, oldTab);
  await AttrChangePromise;

  ok(
    newTab.hasAttribute("muted"),
    "Expected the correct muted attribute on the new tab"
  );
  is(
    newTab.muteReason,
    null,
    "Expected the correct muteReason property on the new tab"
  );
  ok(
    !newTab.hasAttribute("soundplaying"),
    "Expected the correct soundplaying attribute on the new tab"
  );

  // Wait to see if an audio-playback event is dispatched.
  await AudioPlaybackPromise;

  ok(
    newTab.hasAttribute("muted"),
    "Expected the correct muted attribute on the new tab"
  );
  is(
    newTab.muteReason,
    null,
    "Expected the correct muteReason property on the new tab"
  );
  ok(
    !newTab.hasAttribute("soundplaying"),
    "Expected the correct soundplaying attribute on the new tab"
  );

  await test_tooltip(newTab.overlayIcon, "Unmute tab", true, newTab);
}

async function test_browser_swapping(tab, browser) {
  // First, test swapping with a playing but muted tab.
  await play(tab);

  await test_mute_tab(tab, tab.overlayIcon, true);

  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url: "about:blank",
    },
    async function (newBrowser) {
      await test_swapped_browser_while_playing(tab, newBrowser);

      // Now, test swapping with a muted but not playing tab.
      // Note that the tab remains muted, so we only need to pause playback.
      tab = gBrowser.getTabForBrowser(newBrowser);
      await pause(tab);

      await BrowserTestUtils.withNewTab(
        {
          gBrowser,
          url: "about:blank",
        },
        secondAboutBlankBrowser =>
          test_swapped_browser_while_not_playing(tab, secondAboutBlankBrowser)
      );
    }
  );
}

async function test_click_on_pinned_tab_after_mute() {
  async function taskFn(browser) {
    let tab = gBrowser.getTabForBrowser(browser);

    gBrowser.selectedTab = originallySelectedTab;
    isnot(
      tab,
      gBrowser.selectedTab,
      "Sanity check, the tab should not be selected!"
    );

    // Steps to reproduce the bug:
    //   Pin the tab.
    gBrowser.pinTab(tab);

    //   Start playback and wait for it to finish.
    await play(tab);

    //   Mute the tab.
    let icon = tab.overlayIcon;
    await test_mute_tab(tab, icon, true);

    // Pause playback and wait for it to finish.
    await pause(tab);

    // Unmute tab.
    await test_mute_tab(tab, icon, false);

    // Now click on the tab.
    EventUtils.synthesizeMouseAtCenter(tab.iconImage, { button: 0 });

    is(tab, gBrowser.selectedTab, "Tab switch should be successful");

    // Cleanup.
    gBrowser.unpinTab(tab);
    gBrowser.selectedTab = originallySelectedTab;
  }

  let originallySelectedTab = gBrowser.selectedTab;

  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url: PAGE,
    },
    taskFn
  );
}

// This test only does something useful in e10s!
async function test_cross_process_load() {
  async function taskFn(browser) {
    let tab = gBrowser.getTabForBrowser(browser);

    //   Start playback and wait for it to finish.
    await play(tab);

    let soundPlayingStoppedPromise = BrowserTestUtils.waitForEvent(
      tab,
      "TabAttrModified",
      false,
      event => event.detail.changed.includes("soundplaying")
    );

    // Go to a different process.
    BrowserTestUtils.loadURIString(browser, "about:mozilla");
    await BrowserTestUtils.browserLoaded(browser);

    await soundPlayingStoppedPromise;

    ok(
      !tab.hasAttribute("soundplaying"),
      "Tab should not be playing sound any more"
    );
    ok(!tab.soundPlaying, "Tab should not be playing sound any more");
  }

  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url: PAGE,
    },
    taskFn
  );
}

async function test_mute_keybinding() {
  async function test_muting_using_keyboard(tab) {
    let mutedPromise = get_wait_for_mute_promise(tab, true);
    EventUtils.synthesizeKey("m", { ctrlKey: true });
    await mutedPromise;
    mutedPromise = get_wait_for_mute_promise(tab, false);
    EventUtils.synthesizeKey("m", { ctrlKey: true });
    await mutedPromise;
  }
  async function taskFn(browser) {
    let tab = gBrowser.getTabForBrowser(browser);

    // Make sure it's possible to mute before the tab is playing.
    await test_muting_using_keyboard(tab);

    //   Start playback and wait for it to finish.
    await play(tab);

    // Make sure it's possible to mute after the tab is playing.
    await test_muting_using_keyboard(tab);

    // Pause playback and wait for it to finish.
    await pause(tab);

    // Make sure things work if the tab is pinned.
    gBrowser.pinTab(tab);

    // Make sure it's possible to mute before the tab is playing.
    await test_muting_using_keyboard(tab);

    //   Start playback and wait for it to finish.
    await play(tab);

    // Make sure it's possible to mute after the tab is playing.
    await test_muting_using_keyboard(tab);

    gBrowser.unpinTab(tab);
  }

  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url: PAGE,
    },
    taskFn
  );
}

async function test_on_browser(browser) {
  let tab = gBrowser.getTabForBrowser(browser);

  // Test the icon in a normal tab.
  await test_playing_icon_on_tab(tab, browser, false);

  gBrowser.pinTab(tab);

  // Test the icon in a pinned tab.
  await test_playing_icon_on_tab(tab, browser, true);

  gBrowser.unpinTab(tab);

  // Test the sound playing icon for hidden tabs.
  await test_playing_icon_on_hidden_tab(tab);

  // Retest with another browser in the foreground tab
  if (gBrowser.selectedBrowser.currentURI.spec == PAGE) {
    await BrowserTestUtils.withNewTab(
      {
        gBrowser,
        url: "data:text/html,test",
      },
      () => test_on_browser(browser)
    );
  } else {
    await test_browser_swapping(tab, browser);
  }
}

async function test_delayed_tabattr_removal() {
  async function taskFn(browser) {
    let tab = gBrowser.getTabForBrowser(browser);
    await play(tab);

    // Extend the delay to guarantee the soundplaying attribute
    // is not removed from the tab when audio is stopped. Without
    // the extended delay the attribute could be removed in the
    // same tick and the test wouldn't catch that this broke.
    await pause(tab, { extendedDelay: true });
  }

  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url: PAGE,
    },
    taskFn
  );
}

requestLongerTimeout(2);
add_task(async function test_page() {
  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url: PAGE,
    },
    test_on_browser
  );
});

add_task(test_click_on_pinned_tab_after_mute);

add_task(test_cross_process_load);

add_task(test_mute_keybinding);

add_task(test_delayed_tabattr_removal);
