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

"use strict";

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

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  SelectableProfileService:
    "resource:///modules/profiles/SelectableProfileService.sys.mjs",
});

const STORE_ID_PREF = "toolkit.profiles.storeID";

add_task(async function test_recover_regular_profile_no_storeID_in_prefs() {
  let sandbox = sinon.createSandbox();
  let preferencesBackupResource = new PreferencesBackupResource();

  let recoveryPath = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "PreferencesBackupResource-recover-regular-test"
  );
  let destProfilePath = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "PreferencesBackupResource-dest-regular-test"
  );
  // Emulate profiles being disabled
  Services.prefs.setBoolPref("browser.profiles.enabled", false);
  Services.prefs.setBoolPref("browser.profiles.created", false);

  await createTestFiles(recoveryPath, [{ path: "prefs.js" }]);

  sandbox.stub(lazy.SelectableProfileService, "isEnabled").value(false);

  let postRecoveryEntry = await preferencesBackupResource.recover(
    { profilePath: "/some/original/path" },
    recoveryPath,
    destProfilePath
  );

  Assert.equal(postRecoveryEntry, null, "recover() should return null");

  let prefsContent = await IOUtils.readUTF8(
    PathUtils.join(destProfilePath, "prefs.js")
  );
  Assert.ok(
    prefsContent.includes('user_pref("browser.profiles.enabled", false);'),
    "prefs.js should reset browser.profiles.enabled to false"
  );
  Assert.ok(
    prefsContent.includes('user_pref("browser.profiles.created", false);'),
    "prefs.js should reset browser.profiles.created to false"
  );
  Assert.ok(
    prefsContent.includes('user_pref("toolkit.profiles.storeID", "");'),
    "prefs.js should reset storeID to empty string"
  );

  await maybeRemovePath(recoveryPath);
  await maybeRemovePath(destProfilePath);
  Services.prefs.clearUserPref("browser.profiles.enabled");
  Services.prefs.clearUserPref("browser.profiles.created");

  sandbox.restore();
});

add_task(async function test_recover_into_selectable_profile_writes_storeID() {
  let sandbox = sinon.createSandbox();
  let preferencesBackupResource = new PreferencesBackupResource();

  let recoveryPath = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "PreferencesBackupResource-recover-selectable-test"
  );
  let destProfilePath = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "PreferencesBackupResource-dest-selectable-test"
  );

  await createTestFiles(recoveryPath, [{ path: "prefs.js" }]);

  const TEST_STORE_ID = "test-store-id-12345";

  sandbox.stub(lazy.SelectableProfileService, "isEnabled").value(true);
  sandbox
    .stub(lazy.SelectableProfileService, "currentProfile")
    .value({ id: 1 });
  sandbox.stub(lazy.SelectableProfileService, "getDBPref").resolves(true);
  sandbox
    .stub(lazy.SelectableProfileService, "flushSharedPrefToDatabase")
    .resolves();
  sandbox
    .stub(lazy.SelectableProfileService, "addSelectableProfilePrefs")

    .callsFake(async profileDirPath => {
      // This does something similar to what the actual addSelectableProfilePrefs does
      const prefs = [
        `user_pref("browser.profiles.enabled", true);`,
        `user_pref("browser.profiles.created", true);`,
        `user_pref("${STORE_ID_PREF}", "${TEST_STORE_ID}");`,
      ];
      await IOUtils.writeUTF8(
        PathUtils.join(profileDirPath, "prefs.js"),
        prefs.join("\n") + "\n",
        { mode: "appendOrCreate" }
      );
    });

  let postRecoveryEntry = await preferencesBackupResource.recover(
    { profilePath: "/some/original/path" },
    recoveryPath,
    destProfilePath
  );

  Assert.equal(postRecoveryEntry, null, "recover() should return null");

  let prefsContent = await IOUtils.readUTF8(
    PathUtils.join(destProfilePath, "prefs.js")
  );
  Assert.ok(
    prefsContent.includes(`user_pref("${STORE_ID_PREF}", "${TEST_STORE_ID}");`),
    "prefs.js should contain the storeID when recovering into a selectable profile"
  );

  await maybeRemovePath(recoveryPath);
  await maybeRemovePath(destProfilePath);
  sandbox.restore();
});

add_task(async function test_recover_overwrites_stale_selectable_prefs() {
  let sandbox = sinon.createSandbox();
  let preferencesBackupResource = new PreferencesBackupResource();

  let recoveryPath = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "PreferencesBackupResource-recover-stale-test"
  );
  let destProfilePath = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "PreferencesBackupResource-dest-stale-test"
  );

  const STALE_STORE_ID = "stale-old-store-id";
  const CORRECT_STORE_ID = "correct-new-store-id";

  // Create a backup prefs.js with stale selectable profile prefs
  await IOUtils.writeUTF8(
    PathUtils.join(recoveryPath, "prefs.js"),
    `user_pref("${STORE_ID_PREF}", "${STALE_STORE_ID}");\n` +
      `user_pref("browser.profiles.enabled", false);\n` +
      `user_pref("some.user.pref", "user-value");\n`
  );

  sandbox.stub(lazy.SelectableProfileService, "isEnabled").value(true);
  sandbox
    .stub(lazy.SelectableProfileService, "currentProfile")
    .value({ id: 1 });

  sandbox.stub(lazy.SelectableProfileService, "getDBPref").resolves(true);
  sandbox
    .stub(lazy.SelectableProfileService, "flushSharedPrefToDatabase")
    .resolves();

  sandbox
    .stub(lazy.SelectableProfileService, "addSelectableProfilePrefs")
    .callsFake(async profileDirPath => {
      const prefs = [
        `user_pref("browser.profiles.enabled", true);`,
        `user_pref("browser.profiles.created", true);`,
        `user_pref("${STORE_ID_PREF}", "${CORRECT_STORE_ID}");`,
      ];
      await IOUtils.writeUTF8(
        PathUtils.join(profileDirPath, "prefs.js"),
        prefs.join("\n") + "\n",
        { mode: "appendOrCreate" }
      );
    });

  await preferencesBackupResource.recover(
    { profilePath: "/some/original/path" },
    recoveryPath,
    destProfilePath
  );

  let prefsContent = await IOUtils.readUTF8(
    PathUtils.join(destProfilePath, "prefs.js")
  );

  // Verify both stale and correct storeID appear (stale from backup, correct appended)
  Assert.ok(
    prefsContent.includes(
      `user_pref("${STORE_ID_PREF}", "${STALE_STORE_ID}");`
    ),
    "prefs.js should still contain the stale storeID from backup"
  );
  Assert.ok(
    prefsContent.includes(
      `user_pref("${STORE_ID_PREF}", "${CORRECT_STORE_ID}");`
    ),
    "prefs.js should contain the correct storeID appended at the end"
  );

  // Verify the correct storeID appears AFTER the stale one - the last pref value is the one
  // that will be set!
  let staleIndex = prefsContent.indexOf(
    `user_pref("${STORE_ID_PREF}", "${STALE_STORE_ID}");`
  );
  let correctIndex = prefsContent.indexOf(
    `user_pref("${STORE_ID_PREF}", "${CORRECT_STORE_ID}");`
  );
  Assert.greater(
    correctIndex,
    staleIndex,
    "The correct storeID should appear after the stale one so it takes precedence"
  );

  // Verify user prefs from backup are preserved
  Assert.ok(
    prefsContent.includes('user_pref("some.user.pref", "user-value");'),
    "User prefs from backup should be preserved"
  );

  // Verify the corrected browser.profiles.enabled appears after the stale one
  let staleEnabledIndex = prefsContent.indexOf(
    'user_pref("browser.profiles.enabled", false);'
  );
  let correctEnabledIndex = prefsContent.indexOf(
    'user_pref("browser.profiles.enabled", true);'
  );
  Assert.greater(
    correctEnabledIndex,
    staleEnabledIndex,
    "The correct browser.profiles.enabled should appear after the stale one"
  );

  await maybeRemovePath(recoveryPath);
  await maybeRemovePath(destProfilePath);
  sandbox.restore();
});

/**
 * Test that recovering into a selectable profile uses the most restrictive
 * data collection settings between the backup and the profile group.
 */
add_task(async function test_recover_data_collection_prefs_most_restrictive() {
  let sandbox = sinon.createSandbox();
  let preferencesBackupResource = new PreferencesBackupResource();

  let recoveryPath = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "PreferencesBackupResource-data-collection-test"
  );
  let destProfilePath = await IOUtils.createUniqueDirectory(
    PathUtils.tempDir,
    "PreferencesBackupResource-dest-data-collection-test"
  );

  // Set initial values - we'll verify these change (or don't) based on backup vs group
  Services.prefs.setBoolPref("browser.discovery.enabled", true);
  Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", false);
  Services.prefs.setBoolPref("app.shield.optoutstudies.enabled", true);

  // Get default for a pref we'll omit from backup to test the default fallback
  const defaults = Services.prefs.getDefaultBranch(null);
  const usageUploadDefault = defaults.getBoolPref(
    "datareporting.usage.uploadEnabled",
    false
  );
  Services.prefs.clearUserPref("datareporting.usage.uploadEnabled");

  // Create backup prefs.js - note datareporting.usage.uploadEnabled is missing
  await IOUtils.writeUTF8(
    PathUtils.join(recoveryPath, "prefs.js"),
    `user_pref("browser.discovery.enabled", false);\n` +
      `user_pref("datareporting.healthreport.uploadEnabled", true);\n` +
      `user_pref("app.shield.optoutstudies.enabled", true);\n`
  );

  sandbox.stub(lazy.SelectableProfileService, "isEnabled").value(true);
  sandbox
    .stub(lazy.SelectableProfileService, "currentProfile")
    .value({ id: 1 });

  // Group has all data collection prefs ENABLED
  sandbox.stub(lazy.SelectableProfileService, "getDBPref").resolves(true);

  sandbox
    .stub(lazy.SelectableProfileService, "flushSharedPrefToDatabase")
    .resolves();

  sandbox
    .stub(lazy.SelectableProfileService, "addSelectableProfilePrefs")
    .resolves();

  await preferencesBackupResource.recover(
    { profilePath: "/some/original/path" },
    recoveryPath,
    destProfilePath
  );

  // backup=false, group=true -> set to false (most restrictive)
  Assert.equal(
    Services.prefs.getBoolPref("browser.discovery.enabled"),
    false,
    "browser.discovery.enabled: backup=false, group=true -> false"
  );

  // backup=true, group=true -> no change needed (both enabled)
  Assert.equal(
    Services.prefs.getBoolPref("datareporting.healthreport.uploadEnabled"),
    false,
    "datareporting.healthreport.uploadEnabled: backup=true, group=false -> false"
  );

  Assert.equal(
    Services.prefs.getBoolPref("app.shield.optoutstudies.enabled"),
    true,
    "app.shield.optoutstudies.enabled: backup=true, group=true -> stays true"
  );

  // backup=missing, group=true -> uses default value
  Assert.equal(
    Services.prefs.getBoolPref("datareporting.usage.uploadEnabled"),
    usageUploadDefault,
    "datareporting.usage.uploadEnabled: backup=missing -> uses default"
  );

  Assert.ok(
    lazy.SelectableProfileService.addSelectableProfilePrefs.calledOnceWith(
      destProfilePath
    ),
    "addSelectableProfilePrefs should be called with destProfilePath"
  );

  // Clean up prefs set during test
  Services.prefs.clearUserPref("browser.discovery.enabled");
  Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled");
  Services.prefs.clearUserPref("app.shield.optoutstudies.enabled");
  Services.prefs.clearUserPref("datareporting.usage.uploadEnabled");

  await maybeRemovePath(recoveryPath);
  await maybeRemovePath(destProfilePath);
  sandbox.restore();
});

add_task(
  async function test_recover_non_selectable_overwrites_stale_selectable_prefs() {
    let sandbox = sinon.createSandbox();
    let preferencesBackupResource = new PreferencesBackupResource();

    let recoveryPath = await IOUtils.createUniqueDirectory(
      PathUtils.tempDir,
      "PreferencesBackupResource-recover-non-selectable-stale-test"
    );
    let destProfilePath = await IOUtils.createUniqueDirectory(
      PathUtils.tempDir,
      "PreferencesBackupResource-dest-non-selectable-stale-test"
    );

    const STALE_STORE_ID = "stale-selectable-store-id";

    await IOUtils.writeUTF8(
      PathUtils.join(recoveryPath, "prefs.js"),
      `user_pref("${STORE_ID_PREF}", "${STALE_STORE_ID}");\n` +
        `user_pref("browser.profiles.enabled", true);\n` +
        `user_pref("browser.profiles.created", true);\n` +
        `user_pref("datareporting.dau.cachedUsageProfileGroupID", "stale-group-id");\n` +
        `user_pref("some.user.pref", "user-value");\n`
    );

    // Emulate profiles being disabled
    Services.prefs.setBoolPref("browser.profiles.enabled", false);
    Services.prefs.setBoolPref("browser.profiles.created", false);

    sandbox.stub(lazy.SelectableProfileService, "isEnabled").value(false);
    sandbox.stub(lazy.SelectableProfileService, "currentProfile").value(null);

    await preferencesBackupResource.recover(
      { profilePath: "/some/original/path" },
      recoveryPath,
      destProfilePath
    );

    let prefsContent = await IOUtils.readUTF8(
      PathUtils.join(destProfilePath, "prefs.js")
    );

    Assert.ok(
      prefsContent.includes('user_pref("some.user.pref", "user-value");'),
      "User prefs from backup should be preserved"
    );

    let staleStoreIndex = prefsContent.indexOf(
      `user_pref("${STORE_ID_PREF}", "${STALE_STORE_ID}");`
    );
    let resetStoreIndex = prefsContent.indexOf(
      `user_pref("${STORE_ID_PREF}", "");`
    );
    Assert.greater(
      resetStoreIndex,
      staleStoreIndex,
      "Reset storeID should appear after the stale one so it takes precedence"
    );

    let staleEnabledIndex = prefsContent.indexOf(
      'user_pref("browser.profiles.enabled", true);'
    );
    let resetEnabledIndex = prefsContent.indexOf(
      'user_pref("browser.profiles.enabled", false);'
    );
    Assert.greater(
      resetEnabledIndex,
      staleEnabledIndex,
      "Reset browser.profiles.enabled should appear after the stale one"
    );

    let staleCreatedIndex = prefsContent.indexOf(
      'user_pref("browser.profiles.created", true);'
    );
    let resetCreatedIndex = prefsContent.indexOf(
      'user_pref("browser.profiles.created", false);'
    );
    Assert.greater(
      resetCreatedIndex,
      staleCreatedIndex,
      "Reset browser.profiles.created should appear after the stale one"
    );

    await maybeRemovePath(recoveryPath);
    await maybeRemovePath(destProfilePath);

    // Emulate profiles being disabled
    Services.prefs.clearUserPref("browser.profiles.enabled");
    Services.prefs.clearUserPref("browser.profiles.created");

    sandbox.restore();
  }
);
