// Enable signature checks for these tests
gUseRealCertChecks = true;

const DATA = "data/signing_checks/";
const ADDONS = {
  unsigned: "unsigned.xpi",
  signed1: "signed1.xpi",
  signed2: "signed2.xpi",
  privileged: "privileged.xpi",

  // Bug 1509093
  // sha256Signed: "signed_bootstrap_sha256_1.xpi",
};

// The ID in signed1.xpi and signed2.xpi
const ID = "test@somewhere.com";
const PR_USEC_PER_MSEC = 1000;

let testserver = createHttpServer({ hosts: ["example.com"] });

Services.prefs.setCharPref(
  "extensions.update.background.url",
  "http://example.com/update.json"
);
Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);

// Creates an add-on with a broken signature by changing an existing file
function createBrokenAddonModify(file) {
  let brokenFile = gTmpD.clone();
  brokenFile.append("broken.xpi");
  file.copyTo(brokenFile.parent, brokenFile.leafName);

  var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
    Ci.nsIStringInputStream
  );
  stream.setByteStringData("FOOBAR");
  var zipW = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
  zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
  zipW.removeEntry("test.txt", false);
  zipW.addEntryStream(
    "test.txt",
    new Date() * PR_USEC_PER_MSEC,
    Ci.nsIZipWriter.COMPRESSION_NONE,
    stream,
    false
  );
  zipW.close();

  return brokenFile;
}

// Creates an add-on with a broken signature by adding a new file
function createBrokenAddonAdd(file) {
  let brokenFile = gTmpD.clone();
  brokenFile.append("broken.xpi");
  file.copyTo(brokenFile.parent, brokenFile.leafName);

  var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
    Ci.nsIStringInputStream
  );
  stream.setByteStringData("FOOBAR");
  var zipW = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
  zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
  zipW.addEntryStream(
    "test2.txt",
    new Date() * PR_USEC_PER_MSEC,
    Ci.nsIZipWriter.COMPRESSION_NONE,
    stream,
    false
  );
  zipW.close();

  return brokenFile;
}

// Creates an add-on with a broken signature by removing an existing file
function createBrokenAddonRemove(file) {
  let brokenFile = gTmpD.clone();
  brokenFile.append("broken.xpi");
  file.copyTo(brokenFile.parent, brokenFile.leafName);

  var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
    Ci.nsIStringInputStream
  );
  stream.setByteStringData("FOOBAR");
  var zipW = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
  zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
  zipW.removeEntry("test.txt", false);
  zipW.close();

  return brokenFile;
}

function serveUpdate(filename) {
  const RESPONSE = {
    addons: {
      [ID]: {
        updates: [
          {
            version: "2.0",
            update_link: `http://example.com/${filename}`,
            applications: {
              gecko: {
                strict_min_version: "4",
                advisory_max_version: "6",
              },
            },
          },
        ],
      },
    },
  };
  AddonTestUtils.registerJSON(testserver, "/update.json", RESPONSE);
}

async function test_install_broken(
  file,
  expectedError,
  expectNullAddon = true
) {
  let install = await AddonManager.getInstallForFile(file);
  await Assert.rejects(
    install.install(),
    /Install failed/,
    "Install of an improperly signed extension should throw"
  );

  Assert.equal(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
  Assert.equal(install.error, expectedError);

  if (expectNullAddon) {
    Assert.equal(install.addon, null);
  }
}

async function test_install_working(file, expectedSignedState) {
  let install = await AddonManager.getInstallForFile(file);
  await install.install();

  Assert.equal(install.state, AddonManager.STATE_INSTALLED);
  Assert.notEqual(install.addon, null);
  Assert.equal(install.addon.signedState, expectedSignedState);

  await install.addon.uninstall();
}

async function test_update_broken(file1, file2, expectedError) {
  // First install the older version
  await Promise.all([
    promiseInstallFile(file1),
    promiseWebExtensionStartup(ID),
  ]);

  testserver.registerFile("/" + file2.leafName, file2);
  serveUpdate(file2.leafName);

  let addon = await promiseAddonByID(ID);
  let update = await promiseFindAddonUpdates(addon);
  let install = update.updateAvailable;
  await Assert.rejects(
    install.install(),
    /Install failed/,
    "Update to an improperly signed extension should throw"
  );

  Assert.equal(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
  Assert.equal(install.error, expectedError);
  Assert.equal(install.addon, null);

  testserver.registerFile("/" + file2.leafName, null);
  testserver.registerPathHandler("/update.json", null);

  await addon.uninstall();
}

async function test_update_working(file1, file2, expectedSignedState) {
  // First install the older version
  await promiseInstallFile(file1);

  testserver.registerFile("/" + file2.leafName, file2);
  serveUpdate(file2.leafName);

  let addon = await promiseAddonByID(ID);
  let update = await promiseFindAddonUpdates(addon);
  let install = update.updateAvailable;
  await Promise.all([install.install(), promiseWebExtensionStartup(ID)]);

  Assert.equal(install.state, AddonManager.STATE_INSTALLED);
  Assert.notEqual(install.addon, null);
  Assert.equal(install.addon.signedState, expectedSignedState);

  testserver.registerFile("/" + file2.leafName, null);
  testserver.registerPathHandler("/update.json", null);

  await install.addon.uninstall();
}

add_setup(async function setup() {
  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
  await promiseStartupManager();
});

// Try to install a broken add-on
add_task(useAMOStageCert(), async function test_install_invalid_modified() {
  let file = createBrokenAddonModify(do_get_file(DATA + ADDONS.signed1));
  await test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
  file.remove(true);
});

add_task(useAMOStageCert(), async function test_install_invalid_added() {
  let file = createBrokenAddonAdd(do_get_file(DATA + ADDONS.signed1));
  await test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
  file.remove(true);
});

add_task(useAMOStageCert(), async function test_install_invalid_removed() {
  let file = createBrokenAddonRemove(do_get_file(DATA + ADDONS.signed1));
  await test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
  file.remove(true);
});

// Try to install an unsigned add-on
add_task(async function test_install_invalid_unsigned() {
  let file = do_get_file(DATA + ADDONS.unsigned);
  await test_install_broken(file, AddonManager.ERROR_SIGNEDSTATE_REQUIRED);
});

// Try to install a signed add-on
add_task(useAMOStageCert(), async function test_install_valid() {
  let file = do_get_file(DATA + ADDONS.signed1);
  await test_install_working(file, AddonManager.SIGNEDSTATE_SIGNED);
});

add_task(
  useAMOStageCert(),
  async function test_install_implicit_id_with_different_root_cert() {
    info(
      `test install error on fail to verify signature on XPI without ID in manifest`
    );

    const xpi = do_get_file("data/webext-implicit-id.xpi");
    const expectedMessage =
      /Cannot find id for addon .+ Preference xpinstall.signatures.dev-root is set/;

    const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
      await test_install_broken(
        xpi,
        AddonManager.ERROR_CORRUPT_FILE,
        // We don't expect the `addon` property on the `install` object to be
        // `null` because that seems to happen later (when the signature is
        // checked).
        false
      );
    });

    AddonTestUtils.checkMessages(messages, {
      expected: [{ message: expectedMessage }],
    });
  }
);

add_task(
  async function test_install_stage_signed_invalid_with_prod_root_cert() {
    info(
      `test install error on fail to verify signature on XPI with ID in manifest`
    );

    const xpi = do_get_file(DATA + ADDONS.signed1);
    const expectedMessage = /Add-on test@somewhere.com is not correctly signed/;

    const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
      await test_install_broken(
        xpi,
        AddonManager.ERROR_CORRUPT_FILE,
        // We don't expect the `addon` property on the `install` object to be
        // `null` because that seems to happen later (when the signature is
        // checked).
        false
      );
    });

    AddonTestUtils.checkMessages(messages, {
      expected: [{ message: expectedMessage }],
    });
  }
);

// Try to install an add-on signed with SHA-256
add_task(async function test_install_valid_sha256() {
  // Bug 1509093
  // let file = do_get_file(DATA + ADDONS.sha256Signed);
  // await test_install_working(file, AddonManager.SIGNEDSTATE_SIGNED);
});

// Try to install an add-on with the "Mozilla Extensions" OU
add_task(async function test_install_valid_privileged() {
  let file = do_get_file(DATA + ADDONS.privileged);
  try {
    // Prevent install to fail due to privileged.xpi version using
    // a version format that hits a manifest warning.
    // TODO(Bug 1824240): remove this once privileged.xpi can be resigned with a
    // version format that does not hit a manifest warning.
    ExtensionTestUtils.failOnSchemaWarnings(false);
    await test_install_working(file, AddonManager.SIGNEDSTATE_PRIVILEGED);
  } finally {
    ExtensionTestUtils.failOnSchemaWarnings(true);
  }
});

// Try to update to a broken add-on
add_task(useAMOStageCert(), async function test_update_invalid_modified() {
  let file1 = do_get_file(DATA + ADDONS.signed1);
  let file2 = createBrokenAddonModify(do_get_file(DATA + ADDONS.signed2));
  await test_update_broken(file1, file2, AddonManager.ERROR_CORRUPT_FILE);
  file2.remove(true);
});

add_task(useAMOStageCert(), async function test_update_invalid_added() {
  let file1 = do_get_file(DATA + ADDONS.signed1);
  let file2 = createBrokenAddonAdd(do_get_file(DATA + ADDONS.signed2));
  await test_update_broken(file1, file2, AddonManager.ERROR_CORRUPT_FILE);
  file2.remove(true);
});

add_task(useAMOStageCert(), async function test_update_invalid_removed() {
  let file1 = do_get_file(DATA + ADDONS.signed1);
  let file2 = createBrokenAddonRemove(do_get_file(DATA + ADDONS.signed2));
  await test_update_broken(file1, file2, AddonManager.ERROR_CORRUPT_FILE);
  file2.remove(true);
});

// Try to update to an unsigned add-on
add_task(useAMOStageCert(), async function test_update_invalid_unsigned() {
  let file1 = do_get_file(DATA + ADDONS.signed1);
  let file2 = do_get_file(DATA + ADDONS.unsigned);
  await test_update_broken(
    file1,
    file2,
    AddonManager.ERROR_SIGNEDSTATE_REQUIRED
  );
});

// Try to update to a signed add-on
add_task(useAMOStageCert(), async function test_update_valid() {
  let file1 = do_get_file(DATA + ADDONS.signed1);
  let file2 = do_get_file(DATA + ADDONS.signed2);
  await test_update_working(file1, file2, AddonManager.SIGNEDSTATE_SIGNED);
});

add_task(() => promiseShutdownManager());
