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

"use strict";

const { sinon } = ChromeUtils.importESModule(
  "resource://testing-common/Sinon.sys.mjs"
);

const NEW_VIDEO_ASPECT_RATIO = 1.334;

async function switchVideoSource(browser, src) {
  await SpecialPowers.spawn(browser, [{ src }], async ({ src }) => {
    let doc = content.document;
    let video = doc.getElementById("no-controls");
    video.src = src;
  });
}

/**
 *
 * @param {object} actual The actual size and position of the window
 * @param {object} expected The expected size and position of the window
 * @param {string} message A message to print before asserting the size and position
 */
function assertEvent(actual, expected, message) {
  info(message);
  isfuzzy(
    actual.width,
    expected.width,
    ACCEPTABLE_DIFFERENCE,
    `The actual width: ${actual.width}. The expected width: ${expected.width}`
  );
  isfuzzy(
    actual.height,
    expected.height,
    ACCEPTABLE_DIFFERENCE,
    `The actual height: ${actual.height}. The expected height: ${expected.height}`
  );
  isfuzzy(
    actual.left,
    expected.left,
    ACCEPTABLE_DIFFERENCE,
    `The actual left: ${actual.left}. The expected left: ${expected.left}`
  );
  isfuzzy(
    actual.top,
    expected.top,
    ACCEPTABLE_DIFFERENCE,
    `The actual top: ${actual.top}. The expected top: ${expected.top}`
  );
}

/**
 * This test is our control test. This tests that when the PiP window exits
 * fullscreen it will return to the size it was before being fullscreened.
 */
add_task(async function testNoSrcChangeFullscreen() {
  // After opening the PiP window, it is resized to 640x360. There is a change
  // the PiP window will open with that size. To prevent that we override the
  // last saved position so we open at (0, 0) and 300x300.
  overrideSavedPosition(0, 0, 300, 300);
  await BrowserTestUtils.withNewTab(
    {
      url: TEST_PAGE,
      gBrowser,
    },
    async browser => {
      await ensureVideosReady(browser);
      let pipWin = await triggerPictureInPicture(browser, "no-controls");
      let controls = pipWin.document.getElementById("controls");
      const screen = pipWin.screen;

      let resizeEventArray = [];
      pipWin.addEventListener("resize", event => {
        let win = event.target;
        let obj = {
          width: win.outerWidth,
          height: win.outerHeight,
          left: win.screenLeft,
          top: win.screenTop,
        };
        resizeEventArray.push(obj);
      });

      // Move the PiP window to an unsaved location
      let left = 100;
      let top = 100;
      pipWin.moveTo(left, top);

      await TestUtils.waitForCondition(
        () => pipWin.screenLeft === 100 && pipWin.screenTop === 100,
        "Waiting for PiP to move to 100, 100"
      );

      let width = 640;
      let height = 360;

      let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize");
      pipWin.resizeTo(width, height);
      await resizePromise;

      Assert.equal(
        resizeEventArray.length,
        1,
        "resizeEventArray should have 1 event"
      );

      let actualEvent = resizeEventArray.splice(0, 1)[0];
      let expectedEvent = { width, height, left, top };
      assertEvent(
        actualEvent,
        expectedEvent,
        "The PiP window has been correctly positioned before fullscreen"
      );

      await promiseFullscreenEntered(pipWin, async () => {
        EventUtils.sendMouseEvent(
          {
            type: "dblclick",
          },
          controls,
          pipWin
        );
      });

      Assert.equal(
        pipWin.document.fullscreenElement,
        pipWin.document.body,
        "Double-click caused us to enter fullscreen."
      );

      await TestUtils.waitForCondition(
        () => resizeEventArray.length === 1,
        "Waiting for resizeEventArray to have 1 event"
      );

      actualEvent = resizeEventArray.splice(0, 1)[0];
      expectedEvent = {
        width: screen.width,
        height: screen.height,
        left: screen.left,
        top: screen.top,
      };
      assertEvent(
        actualEvent,
        expectedEvent,
        "The PiP window has been correctly fullscreened before switching source"
      );

      await promiseFullscreenExited(pipWin, async () => {
        EventUtils.sendMouseEvent(
          {
            type: "dblclick",
          },
          controls,
          pipWin
        );
      });

      Assert.ok(
        !pipWin.document.fullscreenElement,
        "Double-click caused us to exit fullscreen."
      );

      await TestUtils.waitForCondition(
        () => resizeEventArray.length >= 1,
        "Waiting for resizeEventArray to have 1 event, got " +
          resizeEventArray.length
      );

      actualEvent = resizeEventArray.splice(0, 1)[0];
      expectedEvent = { width, height, left, top };
      assertEvent(
        actualEvent,
        expectedEvent,
        "The PiP window has been correctly positioned after exiting fullscreen"
      );

      await ensureMessageAndClosePiP(browser, "no-controls", pipWin, false);

      clearSavedPosition();
    }
  );
});

/**
 * This function tests changing the src of a Picture-in-Picture player while
 * the player is fullscreened and then ensuring the that video stays
 * fullscreened after the src change and that the player will resize to the new
 * video size.
 */
add_task(async function testChangingSameSizeVideoSrcFullscreen() {
  // After opening the PiP window, it is resized to 640x360. There is a change
  // the PiP window will open with that size. To prevent that we override the
  // last saved position so we open at (0, 0) and 300x300.
  overrideSavedPosition(0, 0, 300, 300);
  await BrowserTestUtils.withNewTab(
    {
      url: TEST_PAGE,
      gBrowser,
    },
    async browser => {
      await ensureVideosReady(browser);
      let pipWin = await triggerPictureInPicture(browser, "no-controls");
      let controls = pipWin.document.getElementById("controls");
      const screen = pipWin.screen;
      let sandbox = sinon.createSandbox();
      let resizeToVideoSpy = sandbox.spy(pipWin, "resizeToVideo");

      let resizeEventArray = [];
      pipWin.addEventListener("resize", event => {
        let win = event.target;
        let obj = {
          width: win.outerWidth,
          height: win.outerHeight,
          left: win.screenLeft,
          top: win.screenTop,
        };
        resizeEventArray.push(obj);
      });

      // Move the PiP window to an unsaved location
      let left = 100;
      let top = 100;
      pipWin.moveTo(left, top);

      await TestUtils.waitForCondition(
        () => pipWin.screenLeft === 100 && pipWin.screenTop === 100,
        "Waiting for PiP to move to 100, 100"
      );

      let width = 640;
      let height = 360;

      let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize");
      pipWin.resizeTo(width, height);
      await resizePromise;

      Assert.equal(
        resizeEventArray.length,
        1,
        "resizeEventArray should have 1 event"
      );

      let actualEvent = resizeEventArray.splice(0, 1)[0];
      let expectedEvent = { width, height, left, top };
      assertEvent(
        actualEvent,
        expectedEvent,
        "The PiP window has been correctly positioned before fullscreen"
      );

      await promiseFullscreenEntered(pipWin, async () => {
        EventUtils.sendMouseEvent(
          {
            type: "dblclick",
          },
          controls,
          pipWin
        );
      });

      Assert.equal(
        pipWin.document.fullscreenElement,
        pipWin.document.body,
        "Double-click caused us to enter fullscreen."
      );

      await TestUtils.waitForCondition(
        () => resizeEventArray.length === 1,
        "Waiting for resizeEventArray to have 1 event"
      );

      actualEvent = resizeEventArray.splice(0, 1)[0];
      expectedEvent = {
        width: screen.width,
        height: screen.height,
        left: screen.left,
        top: screen.top,
      };
      assertEvent(
        actualEvent,
        expectedEvent,
        "The PiP window has been correctly fullscreened before switching source"
      );

      await switchVideoSource(browser, "test-video.mp4");

      await TestUtils.waitForCondition(
        () => resizeToVideoSpy.calledOnce,
        "Waiting for deferredResize to be updated"
      );

      await promiseFullscreenExited(pipWin, async () => {
        EventUtils.sendMouseEvent(
          {
            type: "dblclick",
          },
          controls,
          pipWin
        );
      });

      Assert.ok(
        !pipWin.document.fullscreenElement,
        "Double-click caused us to exit fullscreen."
      );

      await TestUtils.waitForCondition(
        () => resizeEventArray.length >= 1,
        "Waiting for resizeEventArray to have 1 event, got " +
          resizeEventArray.length
      );

      actualEvent = resizeEventArray.splice(0, 1)[0];
      expectedEvent = { width, height, left, top };
      assertEvent(
        actualEvent,
        expectedEvent,
        "The PiP window has been correctly positioned after exiting fullscreen"
      );

      sandbox.restore();
      await ensureMessageAndClosePiP(browser, "no-controls", pipWin, false);

      clearSavedPosition();
    }
  );
});

/**
 * This is similar to the previous test but in this test we switch to a video
 * with a different aspect ratio to confirm that the PiP window will take the
 * new aspect ratio after exiting fullscreen. We also exit fullscreen with the
 * escape key instead of double clicking in this test.
 */
add_task(async function testChangingDifferentSizeVideoSrcFullscreen() {
  // After opening the PiP window, it is resized to 640x360. There is a change
  // the PiP window will open with that size. To prevent that we override the
  // last saved position so we open at (0, 0) and 300x300.
  overrideSavedPosition(0, 0, 300, 300);
  await BrowserTestUtils.withNewTab(
    {
      url: TEST_PAGE,
      gBrowser,
    },
    async browser => {
      await ensureVideosReady(browser);
      let pipWin = await triggerPictureInPicture(browser, "no-controls");
      let controls = pipWin.document.getElementById("controls");
      const screen = pipWin.screen;
      let sandbox = sinon.createSandbox();
      let resizeToVideoSpy = sandbox.spy(pipWin, "resizeToVideo");

      let resizeEventArray = [];
      pipWin.addEventListener("resize", event => {
        let win = event.target;
        let obj = {
          width: win.outerWidth,
          height: win.outerHeight,
          left: win.screenLeft,
          top: win.screenTop,
        };
        resizeEventArray.push(obj);
      });

      // Move the PiP window to an unsaved location
      let left = 100;
      let top = 100;
      pipWin.moveTo(left, top);

      await TestUtils.waitForCondition(
        () => pipWin.screenLeft === 100 && pipWin.screenTop === 100,
        "Waiting for PiP to move to 100, 100"
      );

      let width = 640;
      let height = 360;

      let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize");
      pipWin.resizeTo(width, height);
      await resizePromise;

      Assert.equal(
        resizeEventArray.length,
        1,
        "resizeEventArray should have 1 event"
      );

      let actualEvent = resizeEventArray.splice(0, 1)[0];
      let expectedEvent = { width, height, left, top };
      assertEvent(
        actualEvent,
        expectedEvent,
        "The PiP window has been correctly positioned before fullscreen"
      );

      await promiseFullscreenEntered(pipWin, async () => {
        EventUtils.sendMouseEvent(
          {
            type: "dblclick",
          },
          controls,
          pipWin
        );
      });

      Assert.equal(
        pipWin.document.fullscreenElement,
        pipWin.document.body,
        "Double-click caused us to enter fullscreen."
      );

      await TestUtils.waitForCondition(
        () => resizeEventArray.length === 1,
        "Waiting for resizeEventArray to have 1 event"
      );

      actualEvent = resizeEventArray.splice(0, 1)[0];
      expectedEvent = {
        width: screen.width,
        height: screen.height,
        left: screen.left,
        top: screen.top,
      };
      assertEvent(
        actualEvent,
        expectedEvent,
        "The PiP window has been correctly fullscreened before switching source"
      );

      let previousWidth = pipWin.getDeferredResize().width;

      await switchVideoSource(browser, "test-video-long.mp4");

      // Confirm that we are updating the `deferredResize` and not actually resizing
      await TestUtils.waitForCondition(
        () => resizeToVideoSpy.calledOnce,
        "Waiting for deferredResize to be updated"
      );

      // Confirm that we updated the deferredResize to the new width
      await TestUtils.waitForCondition(
        () => previousWidth !== pipWin.getDeferredResize().width,
        "Waiting for deferredResize to update"
      );

      await promiseFullscreenExited(pipWin, async () => {
        EventUtils.synthesizeKey("KEY_Escape", {}, pipWin);
      });

      Assert.ok(
        !pipWin.document.fullscreenElement,
        "Escape key caused us to exit fullscreen."
      );

      await TestUtils.waitForCondition(
        () => resizeEventArray.length >= 1,
        "Waiting for resizeEventArray to have 1 event, got " +
          resizeEventArray.length
      );

      actualEvent = resizeEventArray.splice(0, 1)[0];
      expectedEvent = {
        width: height * NEW_VIDEO_ASPECT_RATIO,
        height,
        left,
        top,
      };

      // When two resize events happen very close together we optimize by
      // "coalescing" the two resizes into a single resize event. Sometimes
      // the events aren't "coalesced" together (I don't know why) so I check
      // if the most recent event is what we are looking for and if it is not
      // then I'll wait for the resize event we are looking for.
      if (
        Math.abs(
          actualEvent.width - expectedEvent.width <= ACCEPTABLE_DIFFERENCE
        )
      ) {
        // The exit fullscreen resize events were "coalesced".
        assertEvent(
          actualEvent,
          expectedEvent,
          "The PiP window has been correctly positioned after exiting fullscreen"
        );
      } else {
        // For some reason the exit fullscreen resize events weren't "coalesced"
        // so we have to wait for the next resize event.
        await TestUtils.waitForCondition(
          () => resizeEventArray.length === 1,
          "Waiting for resizeEventArray to have 1 event"
        );

        actualEvent = resizeEventArray.splice(0, 1)[0];

        assertEvent(
          actualEvent,
          expectedEvent,
          "The PiP window has been correctly positioned after exiting fullscreen"
        );
      }

      sandbox.restore();
      await ensureMessageAndClosePiP(browser, "no-controls", pipWin, false);

      clearSavedPosition();
    }
  );
});
