/* 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/. */

"use strict";

requestLongerTimeout(2);

const permissionError =
  "error: NotAllowedError: The request is not allowed " +
  "by the user agent or the platform in the current context.";

const SAME_ORIGIN = "https://example.com";
const CROSS_ORIGIN = "https://example.org";

const PATH = "/browser/browser/base/content/test/webrtc/get_user_media.html";
const PATH2 = "/browser/browser/base/content/test/webrtc/get_user_media2.html";

const GRACE_PERIOD_MS = 3000;
const WAIT_PERIOD_MS = GRACE_PERIOD_MS + 500;

// We're inherently testing timeouts (grace periods)
/* eslint-disable mozilla/no-arbitrary-setTimeout */
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
const perms = SitePermissions;

// These tests focus on camera and microphone, so we define some helpers.

async function prompt(audio, video) {
  let observerPromise = expectObserverCalled("getUserMedia:request");
  let promise = promisePopupNotificationShown("webRTC-shareDevices");
  await promiseRequestDevice(audio, video);
  await promise;
  await observerPromise;
  const expectedDeviceSelectorTypes = [
    audio && "microphone",
    video && "camera",
  ].filter(x => x);
  checkDeviceSelectors(expectedDeviceSelectorTypes);
}

async function allow(audio, video) {
  let indicator = promiseIndicatorWindow();
  let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
  let observerPromise2 = expectObserverCalled("recording-device-events");
  await promiseMessage("ok", () => {
    PopupNotifications.panel.firstElementChild.button.click();
  });
  await observerPromise1;
  await observerPromise2;
  Assert.deepEqual(
    Object.assign({ audio: false, video: false }, await getMediaCaptureState()),
    { audio, video },
    `expected ${video ? "camera " : ""} ${audio ? "microphone " : ""}shared`
  );
  await indicator;
  await checkSharingUI({ audio, video });
}

async function deny(action) {
  let observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
  let observerPromise2 = expectObserverCalled("recording-window-ended");
  await promiseMessage(permissionError, () => {
    activateSecondaryAction(action);
  });
  await observerPromise1;
  await observerPromise2;
  await checkNotSharing();
}

async function noPrompt(audio, video) {
  let observerPromises = [
    expectObserverCalled("getUserMedia:request"),
    expectObserverCalled("getUserMedia:response:allow"),
    expectObserverCalled("recording-device-events"),
  ];
  let promise = promiseMessage("ok");
  await promiseRequestDevice(audio, video);
  await promise;
  await Promise.all(observerPromises);
  await promiseNoPopupNotification("webRTC-shareDevices");
  Assert.deepEqual(
    Object.assign({ audio: false, video: false }, await getMediaCaptureState()),
    { audio, video },
    `expected ${video ? "camera " : ""} ${audio ? "microphone " : ""}shared`
  );
  await checkSharingUI({ audio, video });
}

async function navigate(browser, url) {
  await disableObserverVerification();
  let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
  await SpecialPowers.spawn(
    browser,
    [url],
    u => (content.document.location = u)
  );
  await loaded;
  await enableObserverVerification();
}

var gTests = [
  {
    desc: "getUserMedia camera+mic survives track.stop but not past grace",
    run: async function checkAudioVideoGracePastStop() {
      await prompt(true, true);
      await allow(true, true);

      info(
        "After closing all streams, gUM(camera+mic) returns a stream " +
          "without prompting within grace period."
      );
      await closeStream();
      await checkNotSharingWithinGracePeriod();
      await noPrompt(true, true);

      info(
        "After closing all streams, gUM(mic) returns a stream " +
          "without prompting within grace period."
      );
      await closeStream();
      await checkNotSharingWithinGracePeriod();
      await noPrompt(true, false);

      info(
        "After closing all streams, gUM(camera) returns a stream " +
          "without prompting within grace period."
      );
      await closeStream();
      await checkNotSharingWithinGracePeriod();
      await noPrompt(false, true);

      info("gUM(screen) still causes a prompt.");
      let observerPromise = expectObserverCalled("getUserMedia:request");
      let promise = promisePopupNotificationShown("webRTC-shareDevices");
      await promiseRequestDevice(false, true, null, "screen");
      await promise;
      await observerPromise;

      is(
        PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
        "webRTC-shareScreen-notification-icon",
        "anchored to device icon"
      );
      checkDeviceSelectors(["screen"]);

      observerPromise = expectObserverCalled("getUserMedia:response:deny");
      await promiseMessage(permissionError, () => {
        activateSecondaryAction(kActionDeny);
      });
      await observerPromise;
      perms.removeFromPrincipal(null, "screen", gBrowser.selectedBrowser);

      await closeStream();
      info("Closed stream. Waiting past grace period.");
      await checkNotSharingWithinGracePeriod();
      await wait(WAIT_PERIOD_MS);
      await checkNotSharing();

      info("After grace period expires, gUM(camera) causes a prompt.");
      await prompt(false, true);
      await deny(kActionDeny);
      perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);

      info("After grace period expires, gUM(mic) causes a prompt.");
      await prompt(true, false);
      await deny(kActionDeny);
      perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
    },
  },

  {
    desc: "getUserMedia camera+mic survives page reload but not past grace",
    run: async function checkAudioVideoGracePastReload() {
      await prompt(true, true);
      await allow(true, true);
      await closeStream();

      await reloadFromContent();
      info(
        "After page reload, gUM(camera+mic) returns a stream " +
          "without prompting within grace period."
      );
      await checkNotSharingWithinGracePeriod();
      await noPrompt(true, true);
      await closeStream();

      await reloadAsUser();
      info(
        "After user page reload, gUM(camera+mic) returns a stream " +
          "without prompting within grace period."
      );
      await checkNotSharingWithinGracePeriod();
      await noPrompt(true, true);

      info("gUM(screen) still causes a prompt.");
      let observerPromise = expectObserverCalled("getUserMedia:request");
      let promise = promisePopupNotificationShown("webRTC-shareDevices");
      await promiseRequestDevice(false, true, null, "screen");
      await promise;
      await observerPromise;

      is(
        PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
        "webRTC-shareScreen-notification-icon",
        "anchored to device icon"
      );
      checkDeviceSelectors(["screen"]);

      observerPromise = expectObserverCalled("getUserMedia:response:deny");
      await promiseMessage(permissionError, () => {
        activateSecondaryAction(kActionDeny);
      });
      await observerPromise;
      perms.removeFromPrincipal(null, "screen", gBrowser.selectedBrowser);

      await closeStream();
      info("Closed stream. Waiting past grace period.");
      await checkNotSharingWithinGracePeriod();
      await wait(WAIT_PERIOD_MS);
      await checkNotSharing();

      info("After grace period expires, gUM(camera) causes a prompt.");
      await prompt(false, true);
      await deny(kActionDeny);
      perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);

      info("After grace period expires, gUM(mic) causes a prompt.");
      await prompt(true, false);
      await deny(kActionDeny);
      perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
    },
  },

  {
    desc: "getUserMedia camera+mic grace period does not carry over to new tab",
    run: async function checkAudioVideoGraceEndsNewTab() {
      await prompt(true, true);
      await allow(true, true);

      info("Open same page in a new tab");
      await disableObserverVerification();
      await BrowserTestUtils.withNewTab(SAME_ORIGIN + PATH, async () => {
        info("In new tab, gUM(camera+mic) causes a prompt.");
        await prompt(true, true);
      });
      info("Closed tab");
      await enableObserverVerification();
      await closeStream();
      info("Closed stream. Waiting past grace period.");
      await checkNotSharingWithinGracePeriod();
      await wait(WAIT_PERIOD_MS);
      await checkNotSharing();

      info("After grace period expires, gUM(camera+mic) causes a prompt.");
      await prompt(true, true);
      await deny(kActionDeny);
      perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
      perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
    },
  },

  {
    desc: "getUserMedia camera+mic survives navigation but not past grace",
    run: async function checkAudioVideoGracePastNavigation(browser) {
      // Use longer grace period in this test to accommodate navigation
      const LONG_GRACE_PERIOD_MS = 9000;
      const LONG_WAIT_PERIOD_MS = LONG_GRACE_PERIOD_MS + 500;
      await SpecialPowers.pushPrefEnv({
        set: [
          ["privacy.webrtc.deviceGracePeriodTimeoutMs", LONG_GRACE_PERIOD_MS],
        ],
      });
      await prompt(true, true);
      await allow(true, true);
      await closeStream();

      info("Navigate to a second same-origin page");
      await navigate(browser, SAME_ORIGIN + PATH2);
      info(
        "After navigating to second same-origin page, gUM(camera+mic) " +
          "returns a stream without prompting within grace period."
      );
      await checkNotSharingWithinGracePeriod();
      await noPrompt(true, true);
      await closeStream();

      info("Closed stream. Waiting past grace period.");
      await checkNotSharingWithinGracePeriod();
      await wait(LONG_WAIT_PERIOD_MS);
      await checkNotSharing();

      info("After grace period expires, gUM(camera+mic) causes a prompt.");
      await prompt(true, true);
      await allow(true, true);

      info("Navigate to a different-origin page");
      await navigate(browser, CROSS_ORIGIN + PATH2);
      info(
        "After navigating to a different-origin page, gUM(camera+mic) " +
          "causes a prompt."
      );
      await prompt(true, true);
      await deny(kActionDeny);
      perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
      perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);

      info("Navigate back to the first page");
      await navigate(browser, SAME_ORIGIN + PATH);
      info(
        "After navigating back to the first page, gUM(camera+mic) " +
          "returns a stream without prompting within grace period."
      );
      await checkNotSharingWithinGracePeriod();
      await noPrompt(true, true);
      await closeStream();
      info("Closed stream. Waiting past grace period.");
      await checkNotSharingWithinGracePeriod();
      await wait(LONG_WAIT_PERIOD_MS);
      await checkNotSharing();

      info("After grace period expires, gUM(camera+mic) causes a prompt.");
      await prompt(true, true);
      await deny(kActionDeny);
      perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
      perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
    },
  },

  {
    desc: "getUserMedia camera+mic grace period cleared on permission block",
    run: async function checkAudioVideoGraceEndsNewTab() {
      await SpecialPowers.pushPrefEnv({
        set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", 10000]],
      });
      info("Set up longer camera grace period.");
      await prompt(false, true);
      await allow(false, true);
      await closeStream();
      let principal = gBrowser.selectedBrowser.contentPrincipal;
      info("Request both to get prompted so we can block both.");
      await prompt(true, true);
      // We need to remember this decision to set a block permission here and not just 'Not now' the request, see Bug:1609578
      await deny(kActionNever);
      // Clear the block so we can prompt again.
      perms.removeFromPrincipal(principal, "camera", gBrowser.selectedBrowser);
      perms.removeFromPrincipal(
        principal,
        "microphone",
        gBrowser.selectedBrowser
      );

      info("Revoking permission clears camera grace period.");
      await prompt(false, true);
      await deny(kActionDeny);
      perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);

      info("Set up longer microphone grace period.");
      await prompt(true, false);
      await allow(true, false);
      await closeStream();

      info("Request both to get prompted so we can block both.");
      await prompt(true, true);
      // We need to remember this decision to be able to set a block permission here
      await deny(kActionNever);
      perms.removeFromPrincipal(principal, "camera", gBrowser.selectedBrowser);
      perms.removeFromPrincipal(
        principal,
        "microphone",
        gBrowser.selectedBrowser
      );

      info("Revoking permission clears microphone grace period.");
      await prompt(true, false);
      // We need to remember this decision to be able to set a block permission here
      await deny(kActionNever);
      perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
    },
  },
];

add_task(async function test() {
  await SpecialPowers.pushPrefEnv({
    set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", GRACE_PERIOD_MS]],
  });
  await runTests(gTests);
});
