/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

add_task(async function () {
  let tab1 = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    "about:blank?1"
  );
  let tab2 = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    "about:blank?2"
  );

  gBrowser.selectedTab = tab1;

  async function background() {
    function promiseUpdated(tabId, attr) {
      return new Promise(resolve => {
        let onUpdated = (tabId_, changeInfo, tab) => {
          if (tabId == tabId_ && attr in changeInfo) {
            browser.tabs.onUpdated.removeListener(onUpdated);

            resolve({ changeInfo, tab });
          }
        };
        browser.tabs.onUpdated.addListener(onUpdated);
      });
    }

    let deferred = {};
    browser.test.onMessage.addListener((message, tabId, result) => {
      if (message == "change-tab-done" && deferred[tabId]) {
        deferred[tabId].resolve(result);
      }
    });

    function changeTab(tabId, attr, on) {
      return new Promise((resolve, reject) => {
        deferred[tabId] = { resolve, reject };
        browser.test.sendMessage("change-tab", tabId, attr, on);
      });
    }

    try {
      let tabs = await browser.tabs.query({ lastFocusedWindow: true });
      browser.test.assertEq(tabs.length, 3, "We have three tabs");

      for (let tab of tabs) {
        // Note: We want to check that these are actual boolean values, not
        // just that they evaluate as false.
        browser.test.assertEq(false, tab.mutedInfo.muted, "Tab is not muted");
        browser.test.assertEq(
          undefined,
          tab.mutedInfo.reason,
          "Tab has no muted info reason"
        );
        browser.test.assertEq(false, tab.audible, "Tab is not audible");
      }

      let windowId = tabs[0].windowId;
      let tabIds = [tabs[1].id, tabs[2].id];

      browser.test.log(
        "Test initial queries for muted and audible return no tabs"
      );
      let silent = await browser.tabs.query({ windowId, audible: false });
      let audible = await browser.tabs.query({ windowId, audible: true });
      let muted = await browser.tabs.query({ windowId, muted: true });
      let nonMuted = await browser.tabs.query({ windowId, muted: false });

      browser.test.assertEq(3, silent.length, "Three silent tabs");
      browser.test.assertEq(0, audible.length, "No audible tabs");

      browser.test.assertEq(0, muted.length, "No muted tabs");
      browser.test.assertEq(3, nonMuted.length, "Three non-muted tabs");

      browser.test.log(
        "Toggle muted and audible externally on one tab each, and check results"
      );
      [muted, audible] = await Promise.all([
        promiseUpdated(tabIds[0], "mutedInfo"),
        promiseUpdated(tabIds[1], "audible"),
        changeTab(tabIds[0], "muted", true),
        changeTab(tabIds[1], "audible", true),
      ]);

      for (let obj of [muted.changeInfo, muted.tab]) {
        browser.test.assertEq(true, obj.mutedInfo.muted, "Tab is muted");
        browser.test.assertEq(
          "user",
          obj.mutedInfo.reason,
          "Tab was muted by the user"
        );
      }

      browser.test.assertEq(
        true,
        audible.changeInfo.audible,
        "Tab audible state changed"
      );
      browser.test.assertEq(true, audible.tab.audible, "Tab is audible");

      browser.test.log(
        "Re-check queries. Expect one audible and one muted tab"
      );
      silent = await browser.tabs.query({ windowId, audible: false });
      audible = await browser.tabs.query({ windowId, audible: true });
      muted = await browser.tabs.query({ windowId, muted: true });
      nonMuted = await browser.tabs.query({ windowId, muted: false });

      browser.test.assertEq(2, silent.length, "Two silent tabs");
      browser.test.assertEq(1, audible.length, "One audible tab");

      browser.test.assertEq(1, muted.length, "One muted tab");
      browser.test.assertEq(2, nonMuted.length, "Two non-muted tabs");

      browser.test.assertEq(true, muted[0].mutedInfo.muted, "Tab is muted");
      browser.test.assertEq(
        "user",
        muted[0].mutedInfo.reason,
        "Tab was muted by the user"
      );

      browser.test.assertEq(true, audible[0].audible, "Tab is audible");

      browser.test.log(
        "Toggle muted internally on two tabs, and check results"
      );
      [nonMuted, muted] = await Promise.all([
        promiseUpdated(tabIds[0], "mutedInfo"),
        promiseUpdated(tabIds[1], "mutedInfo"),
        browser.tabs.update(tabIds[0], { muted: false }),
        browser.tabs.update(tabIds[1], { muted: true }),
      ]);

      for (let obj of [nonMuted.changeInfo, nonMuted.tab]) {
        browser.test.assertEq(false, obj.mutedInfo.muted, "Tab is not muted");
      }
      for (let obj of [muted.changeInfo, muted.tab]) {
        browser.test.assertEq(true, obj.mutedInfo.muted, "Tab is muted");
      }

      for (let obj of [
        nonMuted.changeInfo,
        nonMuted.tab,
        muted.changeInfo,
        muted.tab,
      ]) {
        browser.test.assertEq(
          "extension",
          obj.mutedInfo.reason,
          "Mute state changed by extension"
        );

        browser.test.assertEq(
          browser.runtime.id,
          obj.mutedInfo.extensionId,
          "Mute state changed by extension"
        );
      }

      browser.test.log("Test that mutedInfo is preserved by sessionstore");
      let tab = await changeTab(tabIds[1], "duplicate").then(browser.tabs.get);

      browser.test.assertEq(true, tab.mutedInfo.muted, "Tab is muted");

      browser.test.assertEq(
        "extension",
        tab.mutedInfo.reason,
        "Mute state changed by extension"
      );

      browser.test.assertEq(
        browser.runtime.id,
        tab.mutedInfo.extensionId,
        "Mute state changed by extension"
      );

      browser.test.log("Unmute externally, and check results");
      [nonMuted] = await Promise.all([
        promiseUpdated(tabIds[1], "mutedInfo"),
        changeTab(tabIds[1], "muted", false),
        browser.tabs.remove(tab.id),
      ]);

      for (let obj of [nonMuted.changeInfo, nonMuted.tab]) {
        browser.test.assertEq(false, obj.mutedInfo.muted, "Tab is not muted");
        browser.test.assertEq(
          "user",
          obj.mutedInfo.reason,
          "Mute state changed by user"
        );
      }

      browser.test.notifyPass("tab-audio");
    } catch (e) {
      browser.test.fail(`${e} :: ${e.stack}`);
      browser.test.notifyFail("tab-audio");
    }
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["tabs"],
    },

    background,
  });

  extension.onMessage("change-tab", (tabId, attr, on) => {
    const {
      Management: {
        global: { tabTracker },
      },
    } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");

    let tab = tabTracker.getTab(tabId);

    if (attr == "muted") {
      // Ideally we'd simulate a click on the tab audio icon for this, but the
      // handler relies on CSS :hover states, which are complicated and fragile
      // to simulate.
      if (tab.muted != on) {
        tab.toggleMuteAudio();
      }
    } else if (attr == "audible") {
      let browser = tab.linkedBrowser;
      if (on) {
        browser.audioPlaybackStarted();
      } else {
        browser.audioPlaybackStopped();
      }
    } else if (attr == "duplicate") {
      // This is a bit of a hack. It won't be necessary once we have
      // `tabs.duplicate`.
      let newTab = gBrowser.duplicateTab(tab);
      BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(
        () => {
          extension.sendMessage(
            "change-tab-done",
            tabId,
            tabTracker.getId(newTab)
          );
        }
      );
      return;
    }

    extension.sendMessage("change-tab-done", tabId);
  });

  await extension.startup();

  await extension.awaitFinish("tab-audio");

  await extension.unload();

  BrowserTestUtils.removeTab(tab1);
  BrowserTestUtils.removeTab(tab2);
});
