const PRINT_DOCUMENT_URI = "chrome://global/content/print.html";
const DEFAULT_PRINTER_NAME = "Mozilla Save to PDF";
const { MockFilePicker } = SpecialPowers;

let pickerMocked = false;

class PrintHelper {
  static async withTestPage(testFn, pagePathname, useHTTPS = false) {
    let pageUrl = "";
    if (pagePathname) {
      pageUrl = useHTTPS
        ? this.getTestPageUrlHTTPS(pagePathname)
        : this.getTestPageUrl(pagePathname);
    } else {
      pageUrl = useHTTPS
        ? this.defaultTestPageUrlHTTPS
        : this.defaultTestPageUrl;
    }
    info("withTestPage: " + pageUrl);
    let isPdf = pageUrl.endsWith(".pdf");

    let taskReturn = await BrowserTestUtils.withNewTab(
      isPdf ? "about:blank" : pageUrl,
      async function (browser) {
        if (isPdf) {
          let loaded = BrowserTestUtils.waitForContentEvent(
            browser,
            "documentloaded",
            false,
            null,
            true
          );
          BrowserTestUtils.startLoadingURIString(browser, pageUrl);
          await loaded;
        }
        await testFn(new PrintHelper(browser));
      }
    );

    await SpecialPowers.popPrefEnv();
    if (isPdf) {
      await SpecialPowers.popPrefEnv();
    }

    // Reset all of the other printing prefs to their default.
    this.resetPrintPrefs();
    return taskReturn;
  }

  static async withTestPageHTTPS(testFn, pagePathname) {
    return this.withTestPage(testFn, pagePathname, /* useHttps */ true);
  }

  static resetPrintPrefs() {
    for (let name of Services.prefs.getChildList("print.")) {
      Services.prefs.clearUserPref(name);
    }
    Services.prefs.clearUserPref("print_printer");
    Services.prefs.clearUserPref("print.more-settings.open");
  }

  static getTestPageUrl(pathName) {
    if (pathName.startsWith("http://") || pathName.startsWith("file://")) {
      return pathName;
    }
    const testPath = getRootDirectory(gTestPath).replace(
      "chrome://mochitests/content",
      "http://example.com"
    );
    return testPath + pathName;
  }

  static getTestPageUrlHTTPS(pathName) {
    if (pathName.startsWith("https://") || pathName.startsWith("file://")) {
      return pathName;
    }
    const testPath = getRootDirectory(gTestPath).replace(
      "chrome://mochitests/content",
      "https://example.com"
    );
    return testPath + pathName;
  }

  static get defaultTestPageUrl() {
    return this.getTestPageUrl("simplifyArticleSample.html");
  }

  static get defaultTestPageUrlHTTPS() {
    return this.getTestPageUrlHTTPS("simplifyArticleSample.html");
  }

  static createMockPaper(paperProperties = {}) {
    return Object.assign(
      {
        id: "regular",
        name: "Regular Size",
        width: 612,
        height: 792,
        unwriteableMargin: Promise.resolve(
          paperProperties.unwriteableMargin || {
            top: 0.1,
            bottom: 0.1,
            left: 0.1,
            right: 0.1,
            QueryInterface: ChromeUtils.generateQI([Ci.nsIPaperMargin]),
          }
        ),
        QueryInterface: ChromeUtils.generateQI([Ci.nsIPaper]),
      },
      paperProperties
    );
  }

  constructor(sourceBrowser) {
    this.sourceBrowser = sourceBrowser;
  }

  async startPrint(condition = {}) {
    this.sourceBrowser.documentGlobal.document
      .getElementById("cmd_print")
      .doCommand();
    return this.waitForDialog(condition);
  }

  async waitForDialog(condition = {}) {
    let dialog = await TestUtils.waitForCondition(
      () => this.dialog,
      "Wait for dialog"
    );
    await dialog._dialogReady;

    if (Object.keys(condition).length === 0) {
      await this.win._initialized;
      // Wait a frame so the rendering spinner is hidden.
      await new Promise(resolve => requestAnimationFrame(resolve));
    } else if (condition.waitFor == "loadComplete") {
      await BrowserTestUtils.waitForAttributeRemoval("loading", this.doc.body);
    }
  }

  beforeInit(initFn) {
    // Run a function when the print.html document is created,
    // but before its init is called from the domcontentloaded handler
    TestUtils.topicObserved("document-element-inserted", doc => {
      return (
        doc.nodePrincipal.isSystemPrincipal &&
        doc.contentType == "text/html" &&
        doc.URL.startsWith("chrome://global/content/print.html")
      );
    }).then(([doc]) => {
      doc.addEventListener("DOMContentLoaded", () => {
        initFn(doc.documentGlobal);
      });
    });
  }

  async withClosingFn(closeFn) {
    let { dialog } = this;
    await closeFn();
    if (this.dialog) {
      await TestUtils.waitForCondition(
        () => !this.dialog,
        "Wait for dialog to close"
      );
    }
    await dialog._closingPromise;
  }

  resetSettings() {
    this.win.PrintEventHandler.settings =
      this.win.PrintEventHandler.defaultSettings;
    this.win.PrintEventHandler.saveSettingsToPrefs(
      this.win.PrintEventHandler.kInitSaveAll
    );
  }

  async closeDialog() {
    this.resetSettings();
    await this.withClosingFn(() => this.dialog.close());
  }

  assertDialogClosed() {
    is(this._dialogs.length, 0, "There are no print dialogs");
  }

  assertDialogOpen() {
    is(this._dialogs.length, 1, "There is one print dialog");
    ok(BrowserTestUtils.isVisible(this.dialog._box), "The dialog is visible");
  }

  assertDialogHidden() {
    is(this._dialogs.length, 1, "There is one print dialog");
    ok(BrowserTestUtils.isHidden(this.dialog._box), "The dialog is hidden");
    Assert.greater(
      this.dialog._box.getBoundingClientRect().width,
      0,
      "The dialog should still have boxes"
    );
  }

  async assertPrintToFile(file, testFn) {
    ok(!file.exists(), "File does not exist before printing");
    await this.withClosingFn(testFn);
    await TestUtils.waitForCondition(
      () => file.exists() && file.fileSize > 0,
      "Wait for target file to get created",
      50
    );
    ok(file.exists(), "Created target file");

    await TestUtils.waitForCondition(
      () => file.fileSize > 0,
      "Wait for the print progress to run",
      50
    );

    Assert.greater(file.fileSize, 0, "Target file not empty");
  }

  setupMockPrint() {
    if (this.resolveShowSystemDialog) {
      throw new Error("Print already mocked");
    }

    // Create some Promises that we can resolve from the test.
    let showSystemDialogPromise = new Promise(resolve => {
      this.resolveShowSystemDialog = result => {
        if (result !== undefined) {
          resolve(result);
        } else {
          resolve(true);
        }
      };
    });
    let printPromise = new Promise((resolve, reject) => {
      this.resolvePrint = resolve;
      this.rejectPrint = reject;
    });

    // Mock PrintEventHandler with our Promises.
    this.win.PrintEventHandler._showPrintDialog = (window, haveSelection) => {
      this.systemDialogOpenedWithSelection = haveSelection;
      return showSystemDialogPromise;
    };
    this.win.PrintEventHandler._doPrint = (bc, settings) => {
      this._printedSettings = settings;
      return printPromise;
    };
  }

  addMockPrinter(opts = {}) {
    if (typeof opts == "string") {
      opts = { name: opts };
    }
    let {
      name = "Mock Printer",
      paperList,
      printerInfoPromise = Promise.resolve(),
      paperSizeUnit = Ci.nsIPrintSettings.kPaperSizeInches,
      paperId,
      sortAfterLocal = false,
    } = opts;
    let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
      Ci.nsIPrintSettingsService
    );
    // Use the fallbackPaperList as the default for mock printers
    if (!paperList) {
      info("addMockPrinter, using the fallbackPaperList");
      paperList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance(
        Ci.nsIPrinterList
      ).fallbackPaperList;
    }

    let defaultSettings = PSSVC.createNewPrintSettings();
    defaultSettings.printerName = name;
    defaultSettings.toFileName = "";
    defaultSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatNative;
    defaultSettings.outputDestination =
      Ci.nsIPrintSettings.kOutputDestinationPrinter;
    defaultSettings.paperSizeUnit = paperSizeUnit;
    if (paperId) {
      defaultSettings.paperId = paperId;
    }

    if (
      defaultSettings.paperId &&
      Array.from(paperList).find(p => p.id == defaultSettings.paperId)
    ) {
      info(
        `addMockPrinter, using paperId: ${defaultSettings.paperId} from the paperList`
      );
    } else if (paperList.length) {
      defaultSettings.paperId = paperList[0].id;
      info(
        `addMockPrinter, corrected default paperId setting value: ${defaultSettings.paperId}`
      );
    }

    let printer = {
      name,
      sortAfterLocal,
      supportsColor: Promise.resolve(true),
      supportsMonochrome: Promise.resolve(true),
      printerInfo: printerInfoPromise.then(() => ({
        paperList,
        defaultSettings,
        QueryInterface: ChromeUtils.generateQI([Ci.nsIPrinterInfo]),
      })),
      QueryInterface: ChromeUtils.generateQI([Ci.nsIPrinter]),
    };

    if (!this._mockPrinters) {
      this._mockPrinters = [printer];
      this.beforeInit(win => (win._mockPrinters = this._mockPrinters));
    } else {
      this._mockPrinters.push(printer);
    }
    return printer;
  }

  get _tabDialogBox() {
    return this.sourceBrowser.documentGlobal.gBrowser.getTabDialogBox(
      this.sourceBrowser
    );
  }

  get _tabDialogBoxManager() {
    return this._tabDialogBox.getTabDialogManager();
  }

  get _dialogs() {
    return this._tabDialogBox.getTabDialogManager()._dialogs;
  }

  get dialog() {
    return this._dialogs.find(dlg =>
      dlg._box.querySelector(".printSettingsBrowser")
    );
  }

  get paginationElem() {
    return this.dialog._box.querySelector(".printPreviewNavigation");
  }

  get paginationSheetIndicator() {
    return this.paginationElem.shadowRoot.querySelector("#sheetIndicator");
  }

  get currentPrintPreviewBrowser() {
    return this.win.PrintEventHandler.printPreviewEl.lastPreviewBrowser;
  }

  get _printBrowser() {
    return this.dialog._frame;
  }

  get doc() {
    return this._printBrowser.contentDocument;
  }

  get win() {
    return this._printBrowser.contentWindow;
  }

  get(id) {
    return this.doc.getElementById(id);
  }

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

  get sourceURI() {
    return this.win.PrintEventHandler.activeCurrentURI;
  }

  async waitForReaderModeReady() {
    if (gBrowser.selectedBrowser.isArticle) {
      return;
    }
    await new Promise(resolve => {
      let onReaderModeChange = {
        receiveMessage(message) {
          if (
            message.data &&
            message.data.isArticle !== undefined &&
            gBrowser.selectedBrowser.isArticle
          ) {
            AboutReaderParent.removeMessageListener(
              "Reader:UpdateReaderButton",
              onReaderModeChange
            );
            resolve();
          }
        },
      };
      AboutReaderParent.addMessageListener(
        "Reader:UpdateReaderButton",
        onReaderModeChange
      );
    });
  }

  async waitForPreview(changeFn) {
    changeFn();
    await BrowserTestUtils.waitForEvent(this.doc, "preview-updated");
  }

  async waitForSettingsEvent(changeFn) {
    let changed = BrowserTestUtils.waitForEvent(this.doc, "print-settings");
    await changeFn?.();
    await BrowserTestUtils.waitForCondition(
      () => !this.win.PrintEventHandler._delayedSettingsChangeTask.isArmed,
      "Wait for all delayed tasks to execute"
    );
    await changed;
  }

  click(el, { scroll = true } = {}) {
    if (scroll) {
      el.scrollIntoView();
    }
    ok(BrowserTestUtils.isVisible(el), "Element must be visible to click");
    EventUtils.synthesizeMouseAtCenter(el, {}, this.win);
  }

  text(el, text) {
    this.click(el);
    el.value = "";
    EventUtils.sendString(text, this.win);
  }

  async openMoreSettings(options) {
    let details = this.get("more-settings");
    if (!details.open) {
      this.click(details.firstElementChild, options);
    }
    await this.awaitAnimationFrame();
  }

  dispatchSettingsChange(settings) {
    this.doc.dispatchEvent(
      new CustomEvent("update-print-settings", {
        detail: settings,
      })
    );
  }

  get settings() {
    return this.win.PrintEventHandler.settings;
  }

  get viewSettings() {
    return this.win.PrintEventHandler.viewSettings;
  }

  _assertMatches(a, b, msg) {
    if (Array.isArray(a)) {
      is(a.length, b.length, msg);
      for (let i = 0; i < a.length; ++i) {
        this._assertMatches(a[i], b[i], msg);
      }
      return;
    }
    is(a, b, msg);
  }

  assertSettingsMatch(expected) {
    let { settings } = this;
    for (let [setting, value] of Object.entries(expected)) {
      this._assertMatches(settings[setting], value, `${setting} matches`);
    }
  }

  assertPrintedWithSettings(expected) {
    ok(this._printedSettings, "Printed settings have been recorded");
    for (let [setting, value] of Object.entries(expected)) {
      this._assertMatches(
        this._printedSettings[setting],
        value,
        `${setting} matches printed setting`
      );
    }
  }

  get _lastPrintPreviewSettings() {
    return this.win.PrintEventHandler._lastPrintPreviewSettings;
  }

  assertPreviewedWithSettings(expected) {
    let settings = this._lastPrintPreviewSettings;
    ok(settings, "Last preview settings are available");
    for (let [setting, value] of Object.entries(expected)) {
      this._assertMatches(
        settings[setting],
        value,
        `${setting} matches previewed setting`
      );
    }
  }

  async assertSettingsChanged(from, to, changeFn) {
    is(
      Object.keys(from).length,
      Object.keys(to).length,
      "Got the same number of settings to check"
    );
    ok(
      Object.keys(from).every(s => s in to),
      "Checking the same setting names"
    );
    this.assertSettingsMatch(from);
    await changeFn();
    this.assertSettingsMatch(to);
  }

  async assertSettingsNotChanged(settings, changeFn) {
    await this.assertSettingsChanged(settings, settings, changeFn);
  }

  awaitAnimationFrame() {
    return new Promise(resolve => this.win.requestAnimationFrame(resolve));
  }

  mockFilePickerCancel() {
    if (!pickerMocked) {
      pickerMocked = true;
      MockFilePicker.init();
      registerCleanupFunction(() => MockFilePicker.cleanup());
    }
    MockFilePicker.returnValue = MockFilePicker.returnCancel;
  }

  mockFilePicker(filename) {
    if (!pickerMocked) {
      pickerMocked = true;
      MockFilePicker.init();
      registerCleanupFunction(() => MockFilePicker.cleanup());
    }
    MockFilePicker.returnValue = MockFilePicker.returnOK;
    let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
    file.append(filename);
    registerCleanupFunction(() => {
      if (file.exists()) {
        file.remove(false);
      }
    });
    MockFilePicker.setFiles([file]);
    return file;
  }
}

function waitForPreviewVisible() {
  return BrowserTestUtils.waitForCondition(function () {
    let preview = document.querySelector(".printPreviewBrowser");
    return preview && BrowserTestUtils.isVisible(preview);
  });
}
