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

"use strict";

async function changeRangeTo(helper, destination) {
  info(`changeRangeTo(${destination})`);
  let rangeSelect = helper.get("range-picker");
  const optionIndex = [...rangeSelect.options]
    .map(o => o.value)
    .indexOf(destination);
  const option = rangeSelect.options[optionIndex];
  if (option.selected) {
    return;
  }

  let input = BrowserTestUtils.waitForEvent(rangeSelect, "input");

  let popupOpen = BrowserTestUtils.waitForSelectPopupShown(window);

  rangeSelect.focus();
  rangeSelect.scrollIntoView({ block: "center" });
  EventUtils.sendKey("space", helper.win);

  const selectPopup = await popupOpen;
  if (selectPopup.isNativeMenu) {
    selectPopup.activateItem(selectPopup.childNodes[optionIndex]);
  } else {
    const enabledOptions = getRangeOptions(helper);
    const numberMove =
      enabledOptions.indexOf(destination) -
      enabledOptions.indexOf(rangeSelect.value);
    const direction = numberMove > 0 ? "down" : "up";
    if (!numberMove) {
      return;
    }
    for (let i = Math.abs(numberMove); i > 0; i--) {
      EventUtils.sendKey(direction, window);
    }
    EventUtils.sendKey("return", window);
  }

  await input;
}

function getRangeOptions(helper) {
  let rangeSelect = helper.get("range-picker");
  let options = [];
  for (let el of rangeSelect.options) {
    if (!el.disabled) {
      options.push(el.value);
    }
  }
  return options;
}

function getSheetCount(helper) {
  return helper.doc.l10n.getAttributes(helper.get("sheet-count")).args
    .sheetCount;
}

add_task(async function testRangeResetAfterScale() {
  const mockPrinterName = "Fake Printer";
  await PrintHelper.withTestPage(async helper => {
    helper.addMockPrinter(mockPrinterName);
    await helper.startPrint();
    await helper.setupMockPrint();

    helper.mockFilePicker("changeRangeFromScale.pdf");
    await changeRangeTo(helper, "custom");

    await helper.openMoreSettings();
    let scaleRadio = helper.get("percent-scale-choice");
    await helper.waitForPreview(() => helper.click(scaleRadio));
    let percentScale = helper.get("percent-scale");
    await helper.waitForPreview(() => helper.text(percentScale, "200"));

    let customRange = helper.get("custom-range");
    let rangeError = helper.get("error-invalid-range");
    await helper.waitForPreview(() => {
      helper.text(customRange, "3");
    });

    ok(rangeError.hidden, "Range error is hidden");

    await helper.text(percentScale, "10");
    EventUtils.sendKey("return", helper.win);

    await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
    ok(!rangeError.hidden, "Range error is showing");
    await helper.closeDialog();
  });
});

add_task(async function testRangeResetAfterPaperSize() {
  await PrintHelper.withTestPage(async helper => {
    await helper.startPrint();
    await helper.waitForPreview(() =>
      helper.dispatchSettingsChange({ paperId: "iso_a5" })
    );
    await helper.setupMockPrint();

    await helper.openMoreSettings();
    let scaleRadio = helper.get("percent-scale-choice");
    await helper.waitForPreview(() => helper.click(scaleRadio));
    let percentScale = helper.get("percent-scale");
    await helper.waitForPreview(() => helper.text(percentScale, "200"));

    let customRange = helper.get("custom-range");
    await changeRangeTo(helper, "custom");
    await BrowserTestUtils.waitForAttributeRemoval("hidden", customRange);

    let rangeError = helper.get("error-invalid-range");
    await helper.waitForPreview(() => {
      helper.text(customRange, "6");
    });

    ok(rangeError.hidden, "Range error is hidden");

    helper.dispatchSettingsChange({ paperId: "iso_a3" });
    await TestUtils.waitForCondition(
      () => helper.get("paper-size-picker").value == "iso_a3",
      "Wait for paper size select to update"
    );
    EventUtils.sendKey("return", helper.win);

    await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
    ok(!rangeError.hidden, "Range error is showing");
    await helper.closeDialog();
  });
});

add_task(async function testInvalidRangeResetAfterDestinationChange() {
  const mockPrinterName = "Fake Printer";
  await PrintHelper.withTestPage(async helper => {
    helper.addMockPrinter(mockPrinterName);
    await helper.startPrint();

    let destinationPicker = helper.get("printer-picker");
    let customPageRange = helper.get("custom-range");

    await helper.assertSettingsNotChanged({ pageRanges: [] }, async () => {
      await changeRangeTo(helper, "custom");
    });
    let rangeError = helper.get("error-invalid-range");

    await helper.assertSettingsNotChanged({ pageRanges: [] }, async () => {
      ok(rangeError.hidden, "Range error is hidden");
      await helper.text(customPageRange, "9");
      await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
      ok(!rangeError.hidden, "Range error is showing");
    });

    is(destinationPicker.disabled, false, "Destination picker is enabled");

    // Select a new printer
    helper.dispatchSettingsChange({ printerName: mockPrinterName });
    await TestUtils.waitForCondition(
      () => rangeError.hidden,
      "Wait for range error to be hidden"
    );
    is(customPageRange.value, "", "Page range has reset");
    await helper.closeDialog();
  });
});

add_task(async function testPageRangeSets() {
  await PrintHelper.withTestPage(async helper => {
    await helper.startPrint();

    let customRange = helper.get("custom-range");
    let pageRangeInput = helper.get("page-range-input");
    let invalidError = helper.get("error-invalid-range");
    let invalidOverflowError = helper.get("error-invalid-start-range-overflow");

    ok(customRange.hidden, "Custom range input is hidden");

    await changeRangeTo(helper, "custom");
    await BrowserTestUtils.waitForAttributeRemoval("hidden", customRange);

    ok(!customRange.hidden, "Custom range is showing");
    is(helper.doc.activeElement, customRange, "Custom range field is focused");

    // We need to set the input to something to ensure we do not return early
    // out of our validation function
    helper.text(helper.get("custom-range"), ",");

    let validStrings = {
      1: [1, 1],
      "1,": [1, 1],
      2: [2, 2],
      "1-2": [1, 2],
      "1,2": [1, 2],
      "1,2,": [1, 2],
      "2,1": [1, 2],
      "1,3": [1, 1, 3, 3],
      "1-1,3": [1, 1, 3, 3],
      "1,3-3": [1, 1, 3, 3],
      "10-33": [10, 33],
      "1-": [1, 50],
      "-": [],
      "-20": [1, 20],
      "-1,1-": [1, 50],
      "-1,1-2": [1, 2],
      ",9": [9, 9],
      ",": [],
      "1,2,1,20,5": [1, 2, 5, 5, 20, 20],
      "1-17,4,12-19": [1, 19],
      "43-46,42,47-": [42, 50],
    };

    for (let [str, expected] of Object.entries(validStrings)) {
      pageRangeInput._validateRangeInput(str, 50);
      let pageRanges = pageRangeInput.formatPageRange();
      ok(
        expected.length == pageRanges.length &&
          expected.every((page, index) => page === pageRanges[index]),
        `Expected page range for "${str}" matches "${expected}"`
      );

      ok(invalidError.hidden, "Generic error message is hidden");
      ok(invalidOverflowError.hidden, "Start overflow error message is hidden");
    }

    let invalidStrings = ["51", "1,51", "1-51", "4-1", "--", "0", "-90"];

    for (let str of invalidStrings) {
      pageRangeInput._validateRangeInput(str, 50);
      is(pageRangeInput._pagesSet.size, 0, `There are no pages in the set`);
      ok(!pageRangeInput.validity, "Input is invalid");
    }

    await helper.closeDialog();
  });
});

add_task(async function testPageRangeSelect() {
  await PrintHelper.withTestPage(async helper => {
    await helper.startPrint();

    let pageRangeInput = helper.get("page-range-input");

    await changeRangeTo(helper, "all");
    let pageRanges = pageRangeInput.formatPageRange();
    ok(!pageRanges.length, "Page range for all should be []");

    await changeRangeTo(helper, "current");
    pageRanges = pageRangeInput.formatPageRange();
    ok(
      pageRanges.length == 2 &&
        [1, 1].every((page, index) => page === pageRanges[index]),
      "The first page should be the current page"
    );

    pageRangeInput._validateRangeInput("9", 50);
    pageRanges = pageRangeInput.formatPageRange();
    ok(
      pageRanges.length == 2 &&
        [9, 9].every((page, index) => page === pageRanges[index]),
      `Expected page range for "${pageRanges}" matches [9, 9]"`
    );

    await changeRangeTo(helper, "odd");
    pageRanges = pageRangeInput.formatPageRange();
    ok(
      pageRanges.length == 4 &&
        [1, 1, 3, 3].every((page, index) => page === pageRanges[index]),
      "Page range for odd should be [1, 1, 3, 3]"
    );

    await changeRangeTo(helper, "even");
    pageRanges = pageRangeInput.formatPageRange();
    ok(
      pageRanges.length == 2 &&
        [2, 2].every((page, index) => page === pageRanges[index]),
      "Page range for even should be [2, 2]"
    );
  }, "longerArticle.html");
});

add_task(async function testRangeError() {
  await PrintHelper.withTestPage(async helper => {
    await helper.startPrint();

    await changeRangeTo(helper, "custom");

    let invalidError = helper.get("error-invalid-range");
    let invalidOverflowError = helper.get("error-invalid-start-range-overflow");

    ok(invalidError.hidden, "Generic error message is hidden");
    ok(invalidOverflowError.hidden, "Start overflow error message is hidden");

    helper.text(helper.get("custom-range"), "4");

    await BrowserTestUtils.waitForAttributeRemoval("hidden", invalidError);

    ok(!invalidError.hidden, "Generic error message is showing");
    ok(invalidOverflowError.hidden, "Start overflow error message is hidden");

    await helper.closeDialog();
  });
});

add_task(async function testStartOverflowRangeError() {
  await PrintHelper.withTestPage(async helper => {
    await helper.startPrint();

    await changeRangeTo(helper, "custom");

    await helper.openMoreSettings();
    let scaleRadio = helper.get("percent-scale-choice");
    await helper.waitForPreview(() => helper.click(scaleRadio));
    let percentScale = helper.get("percent-scale");
    await helper.waitForPreview(() => helper.text(percentScale, "200"));

    let invalidError = helper.get("error-invalid-range");
    let invalidOverflowError = helper.get("error-invalid-start-range-overflow");

    ok(invalidError.hidden, "Generic error message is hidden");
    ok(invalidOverflowError.hidden, "Start overflow error message is hidden");

    helper.text(helper.get("custom-range"), "2-1");

    await BrowserTestUtils.waitForAttributeRemoval(
      "hidden",
      invalidOverflowError
    );

    ok(invalidError.hidden, "Generic error message is hidden");
    ok(!invalidOverflowError.hidden, "Start overflow error message is showing");

    await helper.closeDialog();
  });
});

add_task(async function testErrorClearedAfterSwitchingToAll() {
  await PrintHelper.withTestPage(async helper => {
    await helper.startPrint();

    await changeRangeTo(helper, "custom");

    let customRange = helper.get("custom-range");
    let rangeError = helper.get("error-invalid-range");
    ok(rangeError.hidden, "Generic error message is hidden");

    helper.text(customRange, "3");

    await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
    ok(!rangeError.hidden, "Generic error message is showing");

    await changeRangeTo(helper, "all");

    await TestUtils.waitForCondition(
      () => rangeError.hidden,
      "Wait for range error to be hidden"
    );
    ok(customRange.hidden, "Custom range is hidden");
    is(
      helper.doc.activeElement,
      helper.get("range-picker"),
      "Range picker remains focused"
    );
    await helper.closeDialog();
  });
});

add_task(async function testPageCountChangeNoRangeNoRerender() {
  await PrintHelper.withTestPage(async helper => {
    let customPrinter = "A printer";
    helper.addMockPrinter(customPrinter);

    await helper.startPrint();

    await helper.assertSettingsChanged(
      { printerName: "Mozilla Save to PDF" },
      { printerName: customPrinter },
      async () => {
        let destinationPicker = helper.get("printer-picker");
        destinationPicker.focus();
        await Promise.all([
          helper.waitForPreview(() =>
            helper.dispatchSettingsChange({ printerName: customPrinter })
          ),
          helper.waitForSettingsEvent(),
        ]);
      }
    );

    // Change a setting that will change the number of pages. Since pageRanges
    // is set to "all" then there shouldn't be a re-render because of it.
    let previewUpdateCount = 0;
    ok(!helper.hasPendingPreview, "No preview is pending");
    helper.doc.addEventListener("preview-updated", () => previewUpdateCount++);

    // Ensure the sheet count will change.
    let initialSheetCount = getSheetCount(helper);

    await helper.assertSettingsChanged(
      { marginLeft: 0.5, marginRight: 0.5 },
      { marginLeft: 3, marginRight: 3 },
      async () => {
        await Promise.all([
          helper.waitForPreview(() =>
            helper.dispatchSettingsChange({ marginLeft: 3, marginRight: 3 })
          ),
          BrowserTestUtils.waitForEvent(helper.doc, "page-count"),
          helper.waitForSettingsEvent(),
        ]);
      }
    );

    let newSheetCount = getSheetCount(helper);
    Assert.less(
      initialSheetCount,
      newSheetCount,
      `There are more sheets now ${initialSheetCount} < ${newSheetCount}`
    );

    ok(!helper.hasPendingPreview, "No preview is pending");
    is(previewUpdateCount, 1, "Only one preview update fired");

    await helper.closeDialog();
  });
});

add_task(async function testPageCountChangeRangeNoRerender() {
  await PrintHelper.withTestPage(async helper => {
    let customPrinter = "A printer";
    helper.addMockPrinter(customPrinter);

    await helper.startPrint();

    await helper.assertSettingsChanged(
      { printerName: "Mozilla Save to PDF", pageRanges: [] },
      { printerName: customPrinter, pageRanges: [1, 1] },
      async () => {
        let destinationPicker = helper.get("printer-picker");
        destinationPicker.focus();
        await Promise.all([
          helper.waitForPreview(() =>
            helper.dispatchSettingsChange({ printerName: customPrinter })
          ),
          helper.waitForSettingsEvent(),
        ]);

        await helper.waitForPreview(async () => {
          await changeRangeTo(helper, "custom");
          helper.text(helper.get("custom-range"), "1");
        });
      }
    );

    // Change a setting that will change the number of pages. Since pageRanges
    // is set to a page that is in the new range, there shouldn't be a re-render.
    let previewUpdateCount = 0;
    ok(!helper.hasPendingPreview, "No preview is pending");
    helper.doc.addEventListener("preview-updated", () => previewUpdateCount++);

    await helper.assertSettingsChanged(
      { marginLeft: 0.5, marginRight: 0.5 },
      { marginLeft: 3, marginRight: 3 },
      async () => {
        await Promise.all([
          helper.waitForPreview(() =>
            helper.dispatchSettingsChange({ marginLeft: 3, marginRight: 3 })
          ),
          BrowserTestUtils.waitForEvent(helper.doc, "page-count"),
          helper.waitForSettingsEvent(),
        ]);
      }
    );

    let newSheetCount = getSheetCount(helper);
    is(newSheetCount, 1, "There's still only one sheet");

    ok(!helper.hasPendingPreview, "No preview is pending");
    is(previewUpdateCount, 1, "Only one preview update fired");

    await helper.closeDialog();
  });
});

add_task(async function testPageCountChangeRangeRerender() {
  await PrintHelper.withTestPage(async helper => {
    let customPrinter = "A printer";
    helper.addMockPrinter(customPrinter);

    await helper.startPrint();

    await helper.assertSettingsChanged(
      { printerName: "Mozilla Save to PDF", pageRanges: [] },
      { printerName: customPrinter, pageRanges: [1, 1] },
      async () => {
        let destinationPicker = helper.get("printer-picker");
        destinationPicker.focus();
        await Promise.all([
          helper.waitForPreview(() =>
            helper.dispatchSettingsChange({ printerName: customPrinter })
          ),
          helper.waitForSettingsEvent(),
        ]);

        await helper.waitForPreview(async () => {
          await changeRangeTo(helper, "custom");
          helper.text(helper.get("custom-range"), "1-");
        });
      }
    );

    // Change a setting that will change the number of pages. Since pageRanges
    // is from 1-N the calculated page range will need to be updated.
    let previewUpdateCount = 0;
    ok(!helper.hasPendingPreview, "No preview is pending");
    helper.doc.addEventListener("preview-updated", () => previewUpdateCount++);
    let renderedTwice = TestUtils.waitForCondition(
      () => previewUpdateCount == 2
    );

    // Ensure the sheet count will change.
    let initialSheetCount = getSheetCount(helper);

    await helper.assertSettingsChanged(
      { marginLeft: 0.5, marginRight: 0.5 },
      { marginLeft: 3, marginRight: 3 },
      async () => {
        await Promise.all([
          helper.waitForPreview(() =>
            helper.dispatchSettingsChange({ marginLeft: 3, marginRight: 3 })
          ),
          BrowserTestUtils.waitForEvent(helper.doc, "page-count"),
          helper.waitForSettingsEvent(),
        ]);
        await renderedTwice;
      }
    );

    let newSheetCount = getSheetCount(helper);
    Assert.less(
      initialSheetCount,
      newSheetCount,
      `There are more sheets now ${initialSheetCount} < ${newSheetCount}`
    );
    Assert.deepEqual(
      helper.viewSettings.pageRanges,
      [1, newSheetCount],
      "The new range is the updated full page range"
    );

    ok(!helper.hasPendingPreview, "No preview is pending");
    is(previewUpdateCount, 2, "Preview updated again to show new page range");

    await helper.closeDialog();
  });
});
