/* 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/.
 *
 * Bug 1407366 - A test case for reassuring the size of the content viewport is
 *   rounded if the window is resized when letterboxing is enabled.
 *
 * A helpful note: if this test starts randomly failing; it may be because the
 * zoom level was not reset by an earlier-run test. See Bug 1407366 for an
 * example.
 */

const { RFPHelper } = ChromeUtils.importESModule(
  "resource://gre/modules/RFPHelper.sys.mjs"
);

// A set of test cases which defines the width and the height of the outer window.
const TEST_CASES = [
  { width: 1250, height: 1000 },
  { width: 1500, height: 1050 },
  { width: 1120, height: 760 },
  { width: 800, height: 600 },
  { width: 640, height: 400 },
  { width: 500, height: 350 },
];

function getPlatform() {
  const { OS } = Services.appinfo;
  if (OS == "WINNT") {
    return "win";
  } else if (OS == "Darwin") {
    return "mac";
  }
  return "linux";
}

function handleOSFuzziness(aContent, aTarget) {
  /*
   * On Windows, we observed off-by-one pixel differences that
   * couldn't be expained. When manually setting the window size
   * to try to reproduce it; it did not occur.
   */
  if (getPlatform() == "win") {
    return Math.abs(aContent - aTarget) <= 1;
  }
  return aContent == aTarget;
}

function checkForDefaultSetting(
  aContentWidth,
  aContentHeight,
  aRealWidth,
  aRealHeight
) {
  // We can get the rounded size by subtracting twice the margin.
  let targetWidth = aRealWidth - 2 * RFPHelper.steppedRange(aRealWidth);
  let targetHeight = aRealHeight - 2 * RFPHelper.steppedRange(aRealHeight);

  // This platform-specific code is explained in the large comment below.
  if (getPlatform() != "linux") {
    ok(
      handleOSFuzziness(aContentWidth, targetWidth),
      `Default Dimensions: The content window width is correctly rounded into. ${aRealWidth}px -> ${aContentWidth}px should equal ${targetWidth}px`
    );

    ok(
      handleOSFuzziness(aContentHeight, targetHeight),
      `Default Dimensions: The content window height is correctly rounded into. ${aRealHeight}px -> ${aContentHeight}px should equal ${targetHeight}px`
    );

    // Using ok() above will cause Win/Mac to fail on even the first test, we don't need to repeat it, return true so waitForCondition ends
    return true;
  }
  // Returning true or false depending on if the test succeeded will cause Linux to repeat until it succeeds.
  return (
    handleOSFuzziness(aContentWidth, targetWidth) &&
    handleOSFuzziness(aContentHeight, targetHeight)
  );
}

async function test_dynamical_window_rounding(aWindow, aCheckFunc) {
  // We need to wait for the updating the margins for the newly opened tab, or
  // it will affect the following tests.
  let promiseForTheFirstRounding = TestUtils.topicObserved(
    "test:letterboxing:update-margin-finish"
  );

  info("Open a content tab for testing.");
  let tab = await BrowserTestUtils.openNewForegroundTab(
    aWindow.gBrowser,
    TEST_PATH + "file_dummy.html"
  );

  info("Wait until the margins are applied for the opened tab.");
  await promiseForTheFirstRounding;

  let getContainerSize = aTab => {
    let browserContainer = aWindow.gBrowser.getBrowserContainer(
      aTab.linkedBrowser
    );
    return {
      containerWidth: browserContainer.clientWidth,
      containerHeight: browserContainer.clientHeight,
    };
  };

  for (let { width, height } of TEST_CASES) {
    let caseString = "Case " + width + "x" + height + ": ";
    // Create a promise for waiting for the margin update.
    let promiseRounding = TestUtils.topicObserved(
      "test:letterboxing:update-margin-finish"
    );

    let { containerWidth, containerHeight } = getContainerSize(tab);

    info(
      caseString +
        "Resize the window and wait until resize event happened (currently " +
        containerWidth +
        "x" +
        containerHeight +
        ")"
    );
    await new Promise(resolve => {
      ({ containerWidth, containerHeight } = getContainerSize(tab));
      info(
        caseString +
          "Resizing (currently " +
          containerWidth +
          "x" +
          containerHeight +
          ")"
      );

      aWindow.onresize = () => {
        ({ containerWidth, containerHeight } = getContainerSize(tab));
        info(
          caseString +
            "Resized (currently " +
            containerWidth +
            "x" +
            containerHeight +
            ")"
        );
        if (getPlatform() == "linux" && containerWidth != width) {
          /*
           * We observed frequent test failures that resulted from receiving an onresize
           * event where the browser was resized to an earlier requested dimension. This
           * resize event happens on Linux only, and is an artifact of the asynchronous
           * resizing. (See more discussion on 1407366#53)
           *
           * We cope with this problem in two ways.
           *
           * 1: If we detect that the browser was resized to the wrong value; we
           *    redo the resize. (This is the lines of code immediately following this
           *    comment)
           * 2: We repeat the test until it works using waitForCondition(). But we still
           *    test Win/Mac more thoroughly: they do not loop in waitForCondition more
           *    than once, and can fail the test on the first attempt (because their
           *    check() functions use ok() while on Linux, we do not all ok() and instead
           *    rely on waitForCondition to fail).
           *
           * The logging statements in this test, and RFPHelper.sys.mjs, help narrow down and
           * illustrate the issue.
           */
          info(caseString + "We hit the weird resize bug. Resize it again.");
          aWindow.resizeTo(width, height);
        } else {
          resolve();
        }
      };
      aWindow.resizeTo(width, height);
    });

    ({ containerWidth, containerHeight } = getContainerSize(tab));
    info(
      caseString +
        "Waiting until margin has been updated on browser element. (currently " +
        containerWidth +
        "x" +
        containerHeight +
        ")"
    );
    await promiseRounding;

    info(caseString + "Get innerWidth/Height from the content.");
    await BrowserTestUtils.waitForCondition(async () => {
      let { contentWidth, contentHeight } = await SpecialPowers.spawn(
        tab.linkedBrowser,
        [],
        () => {
          return {
            contentWidth: content.innerWidth,
            contentHeight: content.innerHeight,
          };
        }
      );

      info(caseString + "Check the result.");
      return aCheckFunc(
        contentWidth,
        contentHeight,
        containerWidth,
        containerHeight
      );
    }, "Default Dimensions: The content window width is correctly rounded into.");
  }

  BrowserTestUtils.removeTab(tab);
}

async function test_customize_width_and_height(aWindow) {
  const test_dimensions = `120x80, 200x143, 335x255, 600x312, 742x447, 813x558,
                           990x672, 1200x733, 1470x858`;

  await SpecialPowers.pushPrefEnv({
    set: [
      ["privacy.resistFingerprinting.letterboxing.dimensions", test_dimensions],
    ],
  });

  let dimensions_set = test_dimensions.split(",").map(item => {
    let sizes = item.split("x").map(size => parseInt(size, 10));

    return {
      width: sizes[0],
      height: sizes[1],
    };
  });

  let checkDimension = (
    aContentWidth,
    aContentHeight,
    aRealWidth,
    aRealHeight
  ) => {
    let matchingArea = aRealWidth * aRealHeight;
    let minWaste = Number.MAX_SAFE_INTEGER;
    let targetDimensions = undefined;

    // Find the dimensions which waste the least content area.
    for (let dim of dimensions_set) {
      if (dim.width > aRealWidth || dim.height > aRealHeight) {
        continue;
      }

      let waste = matchingArea - dim.width * dim.height;

      if (waste >= 0 && waste < minWaste) {
        targetDimensions = dim;
        minWaste = waste;
      }
    }

    // This platform-specific code is explained in the large comment above.
    if (getPlatform() != "linux") {
      ok(
        handleOSFuzziness(aContentWidth, targetDimensions.width),
        `Custom Dimension: The content window width is correctly rounded into. ${aRealWidth}px -> ${aContentWidth}px should equal ${targetDimensions.width}`
      );

      ok(
        handleOSFuzziness(aContentHeight, targetDimensions.height),
        `Custom Dimension: The content window height is correctly rounded into. ${aRealHeight}px -> ${aContentHeight}px should equal ${targetDimensions.height}`
      );

      // Using ok() above will cause Win/Mac to fail on even the first test, we don't need to repeat it, return true so waitForCondition ends
      return true;
    }
    // Returning true or false depending on if the test succeeded will cause Linux to repeat until it succeeds.
    return (
      handleOSFuzziness(aContentWidth, targetDimensions.width) &&
      handleOSFuzziness(aContentHeight, targetDimensions.height)
    );
  };

  await test_dynamical_window_rounding(aWindow, checkDimension);

  await SpecialPowers.popPrefEnv();
}

async function test_no_rounding_for_chrome(aWindow) {
  // First, resize the window to a size which is not rounded.
  await new Promise(resolve => {
    aWindow.onresize = () => resolve();
    aWindow.resizeTo(700, 450);
  });

  // open a chrome privilege tab, like about:config.
  let tab = await BrowserTestUtils.openNewForegroundTab(
    aWindow.gBrowser,
    "about:config"
  );

  // Check that the browser element should not have a margin.
  is(
    tab.linkedBrowser.style.margin,
    "",
    "There is no margin around chrome tab."
  );

  BrowserTestUtils.removeTab(tab);
}

// Tests that the findbar opening and closing causes a margin update.
async function test_findbar(aWindow) {
  // First, resize the window to a size which is not rounded.
  await new Promise(resolve => {
    aWindow.onresize = () => resolve();
    aWindow.resizeTo(701, 451);
  });

  let tab = await BrowserTestUtils.openNewForegroundTab(
    aWindow.gBrowser,
    TEST_PATH + "file_dummy.html"
  );

  let promiseRounding = TestUtils.topicObserved(
    "test:letterboxing:update-margin-finish"
  );

  let findBarOpenPromise = BrowserTestUtils.waitForEvent(
    aWindow,
    "findbaropen"
  );
  EventUtils.synthesizeKey("F", { accelKey: true }, aWindow);
  await findBarOpenPromise;
  await promiseRounding;

  ok(true, "Margin updated when findbar opened");

  promiseRounding = TestUtils.topicObserved(
    "test:letterboxing:update-margin-finish"
  );

  let findBarClosePromise = BrowserTestUtils.waitForEvent(
    aWindow,
    "findbarclose"
  );
  EventUtils.synthesizeKey("KEY_Escape", {}, aWindow);
  await findBarClosePromise;
  await promiseRounding;

  ok(true, "Margin updated when findbar closed");

  BrowserTestUtils.removeTab(tab);
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["privacy.resistFingerprinting.letterboxing", true],
      ["privacy.resistFingerprinting.letterboxing.testing", true],
    ],
  });
});

add_task(async function do_tests() {
  // Store the original window size before testing.
  let originalOuterWidth = window.outerWidth;
  let originalOuterHeight = window.outerHeight;

  info("Run test for the default window rounding.");
  await test_dynamical_window_rounding(window, checkForDefaultSetting);

  info("Run test for the window rounding with customized dimensions.");
  await test_customize_width_and_height(window);

  info("Run test for no margin around tab with the chrome privilege.");
  await test_no_rounding_for_chrome(window);

  await test_findbar(window);

  // Restore the original window size.
  window.outerWidth = originalOuterWidth;
  window.outerHeight = originalOuterHeight;

  // Testing that whether the dynamical rounding works for new windows.
  let win = await BrowserTestUtils.openNewBrowserWindow();

  info("Run test for the default window rounding in new window.");
  await test_dynamical_window_rounding(win, checkForDefaultSetting);

  info(
    "Run test for the window rounding with customized dimensions in new window."
  );
  await test_customize_width_and_height(win);

  info(
    "Run test for no margin around tab with the chrome privilege in new window."
  );
  await test_no_rounding_for_chrome(win);

  await test_findbar(win);

  await BrowserTestUtils.closeWindow(win);
});
