/*
 * 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 https://mozilla.org/MPL/2.0/.
 */

// Writing the fake zip and deleting it can take some time.
requestLongerTimeout(2);

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

function checkSteps(importDocument, currentStep, totalSteps) {
  const stepNav = importDocument.getElementById("stepNav");
  is(stepNav.childElementCount, totalSteps, "Expected amount of steps");
  ok(
    stepNav
      .querySelector(`*:nth-child(${currentStep})`)
      ?.classList.contains("current"),
    `Expected step ${currentStep} is active`
  );
}

function checkVisiblePane(importDocument, activePaneId, activeStepId) {
  const panes = importDocument.querySelectorAll(".tabPane");
  for (const pane of panes) {
    if (pane.id === activePaneId) {
      ok(BrowserTestUtils.isVisible(pane), `Pane ${activePaneId} is visible`);
      const steps = pane.querySelectorAll("section[id]");
      for (const step of steps) {
        if (step.id === activeStepId) {
          ok(
            BrowserTestUtils.isVisible(step),
            `Step ${activeStepId} is visible`
          );
        } else {
          ok(BrowserTestUtils.isHidden(step), `Step ${step.id} is hidden`);
        }
      }
    } else {
      ok(BrowserTestUtils.isHidden(pane), `Pane ${pane.id} is not visible`);
    }
  }
}

add_setup(() => {
  MockFilePicker.init(window.browsingContext);
  registerCleanupFunction(() => {
    MockFilePicker.cleanup();
  });
});

add_task(async function testProfileImport() {
  Services.fog.testResetFOG();

  const profileDir = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "profile-tmp"
  );
  await IOUtils.writeUTF8(
    PathUtils.join(profileDir, "prefs.js"),
    [
      ["mail.smtpserver.smtp1.username", "smtp-user-1"],
      ["mail.smtpservers", "smtp1"],
      ["mail.identity.id1.smtpServer", "smtp1"],
      ["mail.server.server2.type", "none"],
      ["mail.server.server6.type", "imap"],
      ["mail.account.account1.server", "server2"],
      ["mail.account.account2.server", "server6"],
      ["mail.account.account2.identities", "id1"],
      ["mail.accountmanager.accounts", "account2,account1"],
    ]
      .map(
        ([name, value]) =>
          `user_pref(${JSON.stringify(name)}, ${JSON.stringify(value)});`
      )
      .join("\n")
  );

  const badProfileDir = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "profile-bad"
  );

  registerCleanupFunction(async () => {
    await IOUtils.remove(profileDir, {
      recursive: true,
    });
    await IOUtils.remove(badProfileDir, {
      recursive: true,
    });
  });

  const tab = await new Promise(resolve => {
    const newTab = window.openTab("contentTab", {
      url: "about:import",
      onLoad() {
        resolve(newTab);
      },
    });
  });
  const importDocument = tab.browser.contentDocument;

  ok(
    BrowserTestUtils.isHidden(importDocument.getElementById("exportDocs")),
    "Export docs link is hidden"
  );
  ok(
    BrowserTestUtils.isVisible(importDocument.getElementById("importDocs")),
    "Import docs link is visible"
  );

  checkSteps(importDocument, 1, 4);
  checkVisiblePane(importDocument, "tabPane-start", "start-sources");
  ok(
    importDocument.querySelector('#start-sources input[value="Thunderbird"]')
      .checked,
    "Thunderbird profile is selected by default"
  );
  ok(
    BrowserTestUtils.isHidden(importDocument.getElementById("startBackButton")),
    "Back button is hidden in first step"
  );
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#tabPane-start .continue",
    {},
    tab.browser
  );
  const appPane = importDocument.getElementById("tabPane-app");
  await BrowserTestUtils.waitForMutationCondition(
    appPane,
    {
      attributes: true,
    },
    () => BrowserTestUtils.isVisible(appPane)
  );

  checkSteps(importDocument, 2, 4);
  checkVisiblePane(importDocument, "tabPane-app", "app-profiles");
  ok(
    BrowserTestUtils.isVisible(
      importDocument.getElementById("profileBackButton")
    ),
    "Back button is visible"
  );

  // Try to import from a source that doesn't contain a profile.

  MockFilePicker.setFiles([await IOUtils.getFile(badProfileDir)]);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    '#filePickerList [value="file-picker-dir"]',
    {},
    tab.browser
  );
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#profileNextButton",
    {},
    tab.browser
  );
  const itemsStep = importDocument.getElementById("app-items");
  await BrowserTestUtils.waitForMutationCondition(
    itemsStep,
    {
      attributes: true,
    },
    () => BrowserTestUtils.isVisible(itemsStep)
  );

  checkSteps(importDocument, 3, 4);
  checkVisiblePane(importDocument, "tabPane-app", "app-items");

  Assert.ok(
    BrowserTestUtils.isVisible(
      importDocument.getElementById("appSourceInvalid")
    ),
    "invalid source message should be visible"
  );
  Assert.ok(
    BrowserTestUtils.isHidden(importDocument.getElementById("appSourceItems")),
    "item selection should be hidden"
  );
  Assert.ok(
    importDocument.getElementById("profileNextButton").disabled,
    "forward button should be disabled"
  );

  // Go back, and try to import from a source that does contain a profile.

  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#profileBackButton",
    {},
    tab.browser
  );

  MockFilePicker.setFiles([await IOUtils.getFile(profileDir)]);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    '#filePickerList [value="file-picker-dir"]',
    {},
    tab.browser
  );
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#profileNextButton",
    {},
    tab.browser
  );
  await BrowserTestUtils.waitForMutationCondition(
    itemsStep,
    {
      attributes: true,
    },
    () => BrowserTestUtils.isVisible(itemsStep)
  );

  checkSteps(importDocument, 3, 4);
  checkVisiblePane(importDocument, "tabPane-app", "app-items");

  Assert.ok(
    BrowserTestUtils.isHidden(
      importDocument.getElementById("appSourceInvalid")
    ),
    "invalid source message should be hidden"
  );
  Assert.ok(
    BrowserTestUtils.isVisible(importDocument.getElementById("appSourceItems")),
    "item selection should be visible"
  );
  Assert.ok(
    !importDocument.getElementById("profileNextButton").disabled,
    "forward button should not be disabled"
  );

  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#profileNextButton",
    {},
    tab.browser
  );
  const summaryStep = importDocument.getElementById("app-summary");
  await BrowserTestUtils.waitForMutationCondition(
    summaryStep,
    {
      attributes: true,
    },
    () => BrowserTestUtils.isVisible(summaryStep)
  );

  checkSteps(importDocument, 4, 4);
  checkVisiblePane(importDocument, "tabPane-app", "app-summary");
  ok(
    BrowserTestUtils.isHidden(
      importDocument.getElementById("profileNextButton")
    ),
    "Can't advance from summary step"
  );

  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#appStartImport",
    {},
    tab.browser
  );

  const progressPane = importDocument.querySelector(
    "#app-summary .progressPane"
  );
  await BrowserTestUtils.waitForMutationCondition(
    appPane,
    {
      attributes: true,
    },
    () => BrowserTestUtils.isVisible(progressPane)
  );
  ok(
    BrowserTestUtils.isHidden(importDocument.getElementById("appStartImport")),
    "Import button is hidden while import is in progress"
  );

  const finish = importDocument.querySelector("#app-summary .progressFinish");
  await BrowserTestUtils.waitForMutationCondition(
    appPane,
    {
      attributes: true,
    },
    () => BrowserTestUtils.isVisible(finish)
  );
  ok(
    BrowserTestUtils.isVisible(progressPane),
    "When import succeeds and finish is shown, progress is still displayed"
  );

  // We close the tab ourselves instead of hitting the finish button, since
  // restarting Thunderbird within the test is a headache.
  document.getElementById("tabmail").closeTab(tab);

  const gleanEvents = Glean.mail.import.testGetValue();
  Assert.equal(
    gleanEvents.length,
    1,
    "the import should have been recorded in telemetry"
  );
  Assert.deepEqual(
    gleanEvents[0].extra,
    {
      importer: "Thunderbird,directory",
      types: "accounts,addressBooks,calendars,mailMessages",
      result: "succeeded",
    },
    "the telemetry data should be correct"
  );
});

add_task(async function testImportLargeZIP() {
  const profileDir = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "profile-tmp"
  );
  const profileZip = PathUtils.join(profileDir, "profile.zip");
  // This block makes sure the ZIP file's fake contents go out of scope once
  // written out.
  {
    const halfAGigabyte = new Uint8Array(2 ** 29);
    await IOUtils.write(profileZip, halfAGigabyte);
    info("ZIP is 0.5 GB big now");
    for (let i = 0; i < 3; ++i) {
      await IOUtils.write(profileZip, halfAGigabyte, { mode: "append" });
      info(`ZIP is ${(i + 2) * 0.5} GB big now`);
    }
  }
  await IOUtils.write(
    profileZip,
    new Uint8Array(2), // These are extra bytes beyond 2 GB
    {
      mode: "append",
    }
  );
  const filePath = Services.io
    .newURI(PathUtils.toFileURI(profileZip))
    .QueryInterface(Ci.nsIFileURL);
  MockFilePicker.setFiles([filePath.file]);
  registerCleanupFunction(async () => {
    await IOUtils.remove(profileDir, {
      recursive: true,
    });
  });

  const tab = await new Promise(resolve => {
    const newTab = window.openTab("contentTab", {
      url: "about:import",
      onLoad() {
        resolve(newTab);
      },
    });
  });
  const importDocument = tab.browser.contentDocument;

  checkSteps(importDocument, 1, 4);
  checkVisiblePane(importDocument, "tabPane-start", "start-sources");
  ok(
    importDocument.querySelector('#start-sources input[value="Thunderbird"]')
      .checked,
    "Thunderbird profile is selected by default"
  );
  ok(
    BrowserTestUtils.isHidden(importDocument.getElementById("startBackButton")),
    "Back button is hidden in first step"
  );
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#tabPane-start .continue",
    {},
    tab.browser
  );
  const appPane = importDocument.getElementById("tabPane-app");
  await BrowserTestUtils.waitForMutationCondition(
    appPane,
    {
      attributes: true,
    },
    () => BrowserTestUtils.isVisible(appPane)
  );

  checkSteps(importDocument, 2, 4);
  checkVisiblePane(importDocument, "tabPane-app", "app-profiles");
  ok(
    BrowserTestUtils.isVisible(
      importDocument.getElementById("profileBackButton")
    ),
    "Back button is visible"
  );
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#appFilePickerZip",
    {},
    tab.browser
  );
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#profileNextButton",
    {},
    tab.browser
  );

  const notificationBox = importDocument.getElementById("errorNotifications");
  await BrowserTestUtils.waitForMutationCondition(
    notificationBox,
    {
      childList: true,
    },
    () => notificationBox.childElementCount > 0
  );

  document.getElementById("tabmail").closeTab(tab);
});

/**
 * Open the second pane for each of the options listed in the first pane.
 * This loads the each importer's module and calls `getSourceProfiles()`
 * which, if nothing else, proves the module loads and the function works
 * without error.
 */
add_task(async function testImportModules() {
  const tab = await new Promise(resolve => {
    const newTab = window.openTab("contentTab", {
      url: "about:import",
      onLoad() {
        resolve(newTab);
      },
    });
  });
  const importDocument = tab.browser.contentDocument;

  checkVisiblePane(importDocument, "tabPane-start", "start-sources");
  const sources = importDocument.querySelectorAll("#start-sources input");
  for (const source of sources) {
    if (source.value == "Outlook") {
      // We can't load the Outlook importer in a CI test, because it will
      // trigger the MAPI code, display an error message from Windows, and
      // never return.
      continue;
    }

    info(`Opening ${source.value} pane.`);
    await BrowserTestUtils.synthesizeMouseAtCenter(
      `#start-sources input[value="${source.value}"]`,
      {},
      tab.browser
    );
    await BrowserTestUtils.synthesizeMouseAtCenter(
      "#tabPane-start .continue",
      {},
      tab.browser
    );

    if (source.value != "file") {
      checkVisiblePane(importDocument, "tabPane-app", "app-profiles");
      Assert.ok(
        BrowserTestUtils.isVisible(
          importDocument.getElementById("profileBackButton")
        ),
        "Back button is visible"
      );
      await BrowserTestUtils.synthesizeMouseAtCenter(
        "#profileBackButton",
        {},
        tab.browser
      );
    } else {
      checkVisiblePane(importDocument, "tabPane-start", "start-file");
      Assert.ok(
        BrowserTestUtils.isVisible(
          importDocument.getElementById("startBackButton")
        ),
        "Back button is visible"
      );
      await BrowserTestUtils.synthesizeMouseAtCenter(
        "#startBackButton",
        {},
        tab.browser
      );
    }

    checkVisiblePane(importDocument, "tabPane-start", "start-sources");
  }

  document.getElementById("tabmail").closeTab(tab);
});
