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

"use strict";

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

const SCHEDULED_BACKUPS_ENABLED_PREF = "browser.backup.scheduled.enabled";
const BACKUP_ARCHIVE_ENABLED_PREF = "browser.backup.archive.enabled";
const BACKUP_RESTORE_ENABLED_PREF = "browser.backup.restore.enabled";

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

/**
 * Tests that the section for controlling backup in about:preferences#sync is
 * visible, but can also be hidden via a pref. We need to close and open the
 * pages since we need a page refresh to see the pref change in effect.
 */
add_task(async function test_preferences_visibility() {
  await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => {
    let settings = await waitForBackupSettings(browser);

    // Wait for the sections to be rendered (they're conditionally rendered based on state)
    await BrowserTestUtils.waitForMutationCondition(
      settings.shadowRoot,
      { childList: true, subtree: true },
      () => {
        return (
          settings.shadowRoot.getElementById("backup-toggle-restore-button") &&
          settings.shadowRoot.getElementById("scheduled-backups")
        );
      }
    );

    let restoreSection = settings.restoreSectionEl;
    let archiveSection = settings.archiveSectionEl;
    Assert.ok(
      BrowserTestUtils.isVisible(restoreSection) &&
        BrowserTestUtils.isVisible(archiveSection),
      "Backup section is visible"
    );
  });

  await SpecialPowers.pushPrefEnv({
    set: [[BACKUP_ARCHIVE_ENABLED_PREF, false]],
  });

  await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => {
    let settings = browser.contentDocument.querySelector("backup-settings");

    Assert.ok(
      BrowserTestUtils.isVisible(settings.restoreSectionEl) &&
        !settings.archiveSectionEl,
      "Backup section is visible since restore is enabled"
    );
  });

  await SpecialPowers.pushPrefEnv({
    set: [[BACKUP_RESTORE_ENABLED_PREF, false]],
  });

  await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => {
    let settings = browser.contentDocument.querySelector("backup-settings");

    Assert.ok(
      !settings.restoreSectionEl && !settings.archiveSectionEl,
      "Backup section is not available anymore after both archive and restore are disabled"
    );
  });
  await SpecialPowers.popPrefEnv();
  await SpecialPowers.popPrefEnv();
});

/**
 * Tests that the disable-backup-encryption dialog can disable encryption
 * from the settings page.
 */
add_task(async function test_disable_backup_encryption_confirm() {
  Services.telemetry.clearEvents();
  Services.fog.testResetFOG();

  await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => {
    let sandbox = sinon.createSandbox();
    let disableEncryptionStub = sandbox
      .stub(BackupService.prototype, "disableEncryption")
      .resolves(true);
    let createBackupStub = sandbox
      .stub(BackupService.prototype, "createBackup")
      .resolves(true);

    Assert.ok(
      Services.prefs.getBoolPref(BACKUP_RESTORE_ENABLED_PREF),
      "Restore pref is back to true"
    );
    Assert.ok(
      Services.prefs.getBoolPref(BACKUP_ARCHIVE_ENABLED_PREF),
      "Archive pref is back to true"
    );

    await SpecialPowers.pushPrefEnv({
      set: [[SCHEDULED_BACKUPS_ENABLED_PREF, true]],
    });

    let settings = await waitForBackupSettings(browser);

    /**
     * For this test, we can pretend that browser-settings receives a backupServiceState
     * with encryptionEnable set to true. Normally, Lit only detects reactive property updates if a
     * property's reference changes (ex. completely replace backupServiceState with a new object),
     * which we actually do after calling BackupService.stateUpdate() and BackupUIParent.sendState().
     *
     * Since we only care about encryptionEnabled, we can just call Lit's requestUpdate() to force
     * the update explicitly.
     */
    settings.backupServiceState.encryptionEnabled = true;
    await settings.requestUpdate();
    await settings.updateComplete;

    // Wait for the sensitive data section to render (requires archiveEnabledStatus
    // and scheduledBackupsEnabled to be set by BackupService state updates)
    await BrowserTestUtils.waitForMutationCondition(
      settings.shadowRoot,
      { childList: true, subtree: true },
      () => settings.shadowRoot.querySelector("moz-checkbox")
    );

    let sensitiveDataCheckbox = settings.sensitiveDataCheckboxInputEl;

    Assert.ok(sensitiveDataCheckbox, "Sensitive data checkbox should be found");

    Assert.ok(
      sensitiveDataCheckbox.checked,
      "Sensitive data checkbox should be checked"
    );

    let disableBackupEncryption = settings.disableBackupEncryptionEl;

    Assert.ok(
      disableBackupEncryption,
      "disable-backup-encryption should be found"
    );

    let confirmButton = disableBackupEncryption.confirmButtonEl;
    let promise = BrowserTestUtils.waitForEvent(
      window,
      "BackupUI:DisableEncryption"
    );

    Assert.ok(confirmButton, "Confirm button should be found");

    confirmButton.click();

    await promise;
    await settings.updateComplete;

    Assert.ok(
      disableEncryptionStub.calledOnce,
      "BackupService was called to disable encryption"
    );

    Assert.ok(
      createBackupStub.calledOnce,
      "BackupService was called to create a new backup"
    );
    Assert.equal(
      createBackupStub.firstCall.args[0].reason,
      "encryption",
      "Backup reason is set"
    );

    let events = Glean.browserBackup.passwordRemoved.testGetValue();
    Assert.equal(events.length, 1, "Found the passwordRemoved Glean event.");

    sandbox.restore();
    await SpecialPowers.popPrefEnv();
  });
});

/**
 * Tests that the a backup file can be restored from the settings page.
 */
add_task(async function test_restore_from_backup() {
  await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => {
    let sandbox = sinon.createSandbox();
    let recoverFromBackupArchiveStub = sandbox
      .stub(BackupService.prototype, "recoverFromBackupArchive")
      .resolves();

    const mockBackupFilePath = await IOUtils.createUniqueFile(
      PathUtils.tempDir,
      "backup.html"
    );
    const mockBackupFile = Cc["@mozilla.org/file/local;1"].createInstance(
      Ci.nsIFile
    );
    mockBackupFile.initWithPath(mockBackupFilePath);

    let filePickerShownPromise = new Promise(resolve => {
      MockFilePicker.showCallback = async () => {
        Assert.ok(true, "Filepicker shown");
        MockFilePicker.setFiles([mockBackupFile]);
        resolve();
      };
    });
    MockFilePicker.returnValue = MockFilePicker.returnOK;

    let settings = browser.contentDocument.querySelector("backup-settings");

    await settings.updateComplete;

    Assert.ok(
      settings.restoreFromBackupButtonEl,
      "Button to restore backups should be found"
    );

    settings.restoreFromBackupButtonEl.click();

    await settings.updateComplete;

    let restoreFromBackup = settings.restoreFromBackupEl;

    Assert.ok(restoreFromBackup, "restore-from-backup should be found");

    let infoPromise = BrowserTestUtils.waitForEvent(
      window,
      "BackupUI:GetBackupFileInfo"
    );

    restoreFromBackup.chooseButtonEl.click();

    await filePickerShownPromise;
    restoreFromBackup.backupServiceState = {
      ...restoreFromBackup.backupServiceState,
      backupFileToRestore: mockBackupFilePath,
    };
    await restoreFromBackup.updateComplete;

    // Dispatch the event that would normally be sent by BackupUIChild
    // after a file is selected
    restoreFromBackup.dispatchEvent(
      new CustomEvent("BackupUI:SelectNewFilepickerPath", {
        bubbles: true,
        composed: true,
        detail: {
          path: mockBackupFilePath,
          filename: mockBackupFile.leafName,
          iconURL: "",
        },
      })
    );

    await infoPromise;
    // Set mock file info
    restoreFromBackup.backupServiceState = {
      ...restoreFromBackup.backupServiceState,
      backupFileInfo: {
        date: new Date(),
        isEncrypted: true,
      },
      selectableProfilesAllowed: false,
    };
    await restoreFromBackup.updateComplete;

    let bs = BackupService.get();
    let originalState = bs.state;
    sandbox.stub(bs, "state").get(() => ({
      ...originalState,
      backupFileToRestore: mockBackupFilePath,
      backupFileInfo: {
        date: new Date(),
        isEncrypted: true,
      },
    }));

    // Set password for file
    restoreFromBackup.passwordInput.value = "h-*@Vfge3_hGxdpwqr@w";

    let restorePromise = BrowserTestUtils.waitForEvent(
      window,
      "BackupUI:RestoreFromBackupFile"
    );

    Assert.ok(
      restoreFromBackup.confirmButtonEl,
      "Confirm button should be found"
    );
    Assert.ok(
      !restoreFromBackup.confirmButtonEl.disabled,
      "Confirm button should not be disabled"
    );

    await restoreFromBackup.updateComplete;

    let quitObservedPromise = TestUtils.topicObserved(
      "quit-application-requested",
      subject => {
        let cancelQuit = subject.QueryInterface(Ci.nsISupportsPRBool);
        cancelQuit.data = true;
        return true;
      }
    );

    restoreFromBackup.confirmButtonEl.click();

    await restorePromise.then(e => {
      Assert.equal(
        e.detail.backupPassword,
        "h-*@Vfge3_hGxdpwqr@w",
        "Event should contain the password"
      );
      Assert.equal(
        e.detail.restoreType,
        "add",
        "restoreType should default to 'add'"
      );
    });

    await quitObservedPromise;

    Assert.ok(
      recoverFromBackupArchiveStub.calledOnce,
      "BackupService was called to start a recovery from a backup archive."
    );

    sandbox.restore();
  });
});

/**
 * Tests that the most recent backup information is shown inside of the
 * component. Also tests that the "Show in folder" and "Edit" buttons open
 * file pickers.
 */
add_task(async function test_last_backup_info_and_location() {
  // We'll override the default location for writing backup archives so that
  // we don't pollute this machine's Documents folder.
  const TEST_PROFILE_PATH = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "testLastBackupInfo"
  );

  await SpecialPowers.pushPrefEnv({
    set: [[SCHEDULED_BACKUPS_ENABLED_PREF, true]],
  });

  await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => {
    let sandbox = sinon.createSandbox();
    let bs = getAndMaybeInitBackupService();

    await SpecialPowers.pushPrefEnv({
      set: [["browser.backup.location", TEST_PROFILE_PATH]],
    });

    Assert.ok(bs.state.backupDirPath, "Backup Dir Path was set");

    let settings = browser.contentDocument.querySelector("backup-settings");
    await settings.updateComplete;

    let stateUpdated = BrowserTestUtils.waitForEvent(
      bs,
      "BackupService:StateUpdate",
      false,
      () => {
        return bs.state.lastBackupDate && bs.state.lastBackupFileName;
      }
    );
    let { archivePath } = await bs.createBackup();
    registerCleanupFunction(async () => {
      // No matter what happens after this, make sure to clean this file up.
      await IOUtils.remove(archivePath);
    });
    await stateUpdated;

    await settings.updateComplete;

    let dateArgs = JSON.parse(
      settings.lastBackupDateEl.getAttribute("data-l10n-args")
    );
    // The lastBackupDate is stored in seconds, but Fluent expects milliseconds,
    // so we'll check that it was converted.
    Assert.equal(
      dateArgs.date,
      bs.state.lastBackupDate * 1000,
      "Should have the backup date as a Fluent arg, in milliseconds"
    );

    let locationArgs = JSON.parse(
      settings.lastBackupFileNameEl.getAttribute("data-l10n-args")
    );
    Assert.equal(
      locationArgs.fileName,
      bs.state.lastBackupFileName,
      "Should have the backup file name as a Fluent arg"
    );

    // Mocking out nsLocalFile isn't something that works very well, so we'll
    // just stub out BackupService.showBackupLocation with something that'll
    // resolve showBackupLocationPromise, and rely on manual testing for
    // showing the location of the backup file.
    let showBackupLocationPromise = new Promise(resolve => {
      let showBackupLocationStub = sandbox.stub(bs, "showBackupLocation");
      showBackupLocationStub.callsFake(() => {
        resolve();
      });
    });

    settings.backupLocationShowButtonEl.click();
    await showBackupLocationPromise;

    const TEST_NEW_BACKUP_PARENT_PATH = await IOUtils.createUniqueDirectory(
      PathUtils.tempDir,
      "testNewBackupParent"
    );
    let newBackupParent = await IOUtils.getDirectory(
      TEST_NEW_BACKUP_PARENT_PATH
    );
    registerCleanupFunction(async function () {
      try {
        await IOUtils.remove(TEST_NEW_BACKUP_PARENT_PATH, { recursive: true });
      } catch (_e) {
        Assert.ok(false, "Had some trouble cleaning up the backup directory");
      }
    });

    sandbox.stub(bs, "deleteLastBackup").resolves();

    let filePickerShownPromise = new Promise(resolve => {
      MockFilePicker.showCallback = async () => {
        Assert.ok(true, "Filepicker shown");
        MockFilePicker.setFiles([newBackupParent]);
        resolve();
      };
    });
    MockFilePicker.returnValue = MockFilePicker.returnOK;

    await BrowserTestUtils.waitForMutationCondition(
      settings.shadowRoot,
      { childList: true, subtree: true },
      () => settings.shadowRoot.getElementById("last-backup-location")
    );

    let locationInput = settings.shadowRoot.getElementById(
      "last-backup-location"
    );
    await locationInput.updateComplete;

    stateUpdated = BrowserTestUtils.waitForEvent(
      bs,
      "BackupService:StateUpdate",
      false,
      () => {
        return bs.state.backupDirPath?.startsWith(TEST_NEW_BACKUP_PARENT_PATH);
      }
    );

    let editButton = locationInput.chooseFolderButtonEl;
    editButton.click();
    await filePickerShownPromise;
    await stateUpdated;

    await SpecialPowers.popPrefEnv();
    sandbox.restore();
  });
  await SpecialPowers.popPrefEnv();
});

add_task(async function test_dialogs_close_on_cancel_with_restore_disabled() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [BACKUP_ARCHIVE_ENABLED_PREF, true],
      [BACKUP_RESTORE_ENABLED_PREF, false],
    ],
  });

  await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => {
    let settings = browser.contentDocument.querySelector("backup-settings");
    await settings.updateComplete;

    for (let dialog of settings.dialogs.filter(element => !!element)) {
      dialog.showModal();
      is(dialog.open, true, `${dialog.id} was opened.`);
      settings.dispatchEvent(new CustomEvent("dialogCancel"));
      is(dialog.open, false, `${dialog.id} was closed by dialogCancel.`);
    }
  });

  await SpecialPowers.popPrefEnv();
});

add_task(async function test_dialogs_close_on_cancel_with_archive_disabled() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [BACKUP_ARCHIVE_ENABLED_PREF, false],
      [BACKUP_RESTORE_ENABLED_PREF, true],
    ],
  });

  await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => {
    let settings = browser.contentDocument.querySelector("backup-settings");
    await settings.updateComplete;

    for (let dialog of settings.dialogs.filter(element => !!element)) {
      dialog.showModal();
      is(dialog.open, true, `${dialog.id} was opened.`);
      settings.dispatchEvent(new CustomEvent("dialogCancel"));
      is(dialog.open, false, `${dialog.id} was closed by dialogCancel.`);
    }
  });

  await SpecialPowers.popPrefEnv();
});
