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

/**
 * Tests the LoginExport module.
 */

"use strict";

let { LoginExport } = ChromeUtils.importESModule(
  "resource://gre/modules/LoginExport.sys.mjs"
);
let { sinon } = ChromeUtils.importESModule(
  "resource://testing-common/Sinon.sys.mjs"
);

/**
 * Saves the logins to a temporary CSV file, reads the lines and returns the CSV lines.
 * After extracting the CSV lines, it deletes the tmp file.
 */
async function exportAsCSVInTmpFile() {
  const tmpFilePath = FileTestUtils.getTempFile("logins.csv").path;
  await LoginExport.exportAsCSV(tmpFilePath);
  const csvString = await IOUtils.readUTF8(tmpFilePath);
  await IOUtils.remove(tmpFilePath);
  // CSV uses CRLF
  return csvString.split(/\r\n/);
}

const COMMON_LOGIN_MODS = {
  guid: "{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}",
  timeCreated: 1589617814635,
  timeLastUsed: 1589710449871,
  timePasswordChanged: 1589617846802,
  timesUsed: 1,
  username: "joe@example.com",
  password: "qwerty",
  origin: "https://example.com",
};

/**
 * Generates a new login object with all the form login fields populated.
 */
function exportFormLogin(modifications) {
  return LoginTestUtils.testData.formLogin({
    ...COMMON_LOGIN_MODS,
    formActionOrigin: "https://action.example.com",
    ...modifications,
  });
}

function exportAuthLogin(modifications) {
  return LoginTestUtils.testData.authLogin({
    ...COMMON_LOGIN_MODS,
    httpRealm: "My realm",
    ...modifications,
  });
}

add_setup(async () => {
  let oldLogins = Services.logins;
  Services.logins = { getAllLogins: sinon.stub() };
  registerCleanupFunction(() => {
    Services.logins = oldLogins;
  });
});

add_task(async function test_buildCSVRow() {
  let testObject = {
    null: null,
    emptyString: "",
    number: 99,
    string: "Foo",
  };
  Assert.deepEqual(
    LoginExport._buildCSVRow(testObject, [
      "null",
      "emptyString",
      "number",
      "string",
    ]),
    ["", `""`, `"99"`, `"Foo"`],
    "Check _buildCSVRow with different types"
  );
});

add_task(async function test_no_new_properties_to_export() {
  let login = exportFormLogin();
  Assert.deepEqual(
    Object.keys(login),
    [
      "QueryInterface",
      "displayOrigin",
      "origin",
      "hostname",
      "formActionOrigin",
      "formSubmitURL",
      "httpRealm",
      "username",
      "usernameField",
      "password",
      "passwordField",
      "unknownFields",
      "everSynced",
      "syncCounter",
      "init",
      "equals",
      "matches",
      "clone",
      "guid",
      "timeCreated",
      "timeLastUsed",
      "timePasswordChanged",
      "timesUsed",
      "timeLastBreachAlertDismissed",
    ],
    "Check that no new properties were added to a login that should maybe be exported"
  );
});

add_task(async function test_export_one_form_login() {
  let login = exportFormLogin();
  Services.logins.getAllLogins.returns([login]);

  let rows = await exportAsCSVInTmpFile();

  Assert.equal(
    rows[0],
    '"url","username","password","httpRealm","formActionOrigin","guid","timeCreated","timeLastUsed","timePasswordChanged"',
    "checking csv headers"
  );
  Assert.equal(
    rows[1],
    '"https://example.com","joe@example.com","qwerty",,"https://action.example.com","{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}","1589617814635","1589710449871","1589617846802"',
    `checking login is saved as CSV row\n${JSON.stringify(login)}\n`
  );
});

add_task(async function test_export_one_auth_login() {
  let login = exportAuthLogin();
  Services.logins.getAllLogins.returns([login]);

  let rows = await exportAsCSVInTmpFile();

  Assert.equal(
    rows[0],
    '"url","username","password","httpRealm","formActionOrigin","guid","timeCreated","timeLastUsed","timePasswordChanged"',
    "checking csv headers"
  );
  Assert.equal(
    rows[1],
    '"https://example.com","joe@example.com","qwerty","My realm",,"{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}","1589617814635","1589710449871","1589617846802"',
    `checking login is saved as CSV row\n${JSON.stringify(login)}\n`
  );
});

add_task(async function test_export_escapes_values() {
  let login = exportFormLogin({
    password: "!@#$%^&*()_+,'",
  });
  Services.logins.getAllLogins.returns([login]);

  let rows = await exportAsCSVInTmpFile();

  Assert.equal(
    rows[1],
    '"https://example.com","joe@example.com","!@#$%^&*()_+,\'",,"https://action.example.com","{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}","1589617814635","1589710449871","1589617846802"',
    `checking login correctly escapes CSV characters \n${JSON.stringify(login)}`
  );
});

add_task(async function test_export_multiple_rows() {
  let logins = await LoginTestUtils.testData.loginList();
  // Note, because we're stubbing this method and avoiding the actual login manager logic,
  // login de-duplication does not occur
  Services.logins.getAllLogins.returns(logins);

  let actualRows = await exportAsCSVInTmpFile();
  let expectedRows = [
    '"url","username","password","httpRealm","formActionOrigin","guid","timeCreated","timeLastUsed","timePasswordChanged"',
    '"http://www.example.com","the username","the password for www.example.com",,"http://www.example.com",,,,',
    '"https://www.example.com","the username","the password for https",,"https://www.example.com",,,,',
    '"https://example.com","the username","the password for example.com",,"https://example.com",,,,',
    '"http://www3.example.com","the username","the password",,"http://www.example.com",,,,',
    '"http://www3.example.com","the username","the password",,"https://www.example.com",,,,',
    '"http://www3.example.com","the username","the password",,"http://example.com",,,,',
    '"http://www4.example.com","username one","password one",,"http://www4.example.com",,,,',
    '"http://www4.example.com","username two","password two",,"http://www4.example.com",,,,',
    '"http://www4.example.com","","password three",,"http://www4.example.com",,,,',
    '"http://www5.example.com","multi username","multi password",,"http://www5.example.com",,,,',
    '"http://www6.example.com","","12345",,"http://www6.example.com",,,,',
    '"https://www7.example.com:8080","8080_username","8080_pass",,"https://www7.example.com:8080",,,,',
    '"https://www7.example.com:8080","8080_username2","8080_pass2","My dev server",,,,,',
    '"http://www.example.org","the username","the password","The HTTP Realm",,,,,',
    '"ftp://ftp.example.org","the username","the password","ftp://ftp.example.org",,,,,',
    '"http://www2.example.org","the username","the password","The HTTP Realm",,,,,',
    '"http://www2.example.org","the username other","the password other","The HTTP Realm Other",,,,,',
    '"http://example.net","the username","the password",,"http://example.net",,,,',
    '"http://example.net","the username","the password",,"http://www.example.net",,,,',
    '"http://example.net","username two","the password",,"http://www.example.net",,,,',
    '"http://example.net","the username","the password","The HTTP Realm",,,,,',
    '"http://example.net","username two","the password","The HTTP Realm Other",,,,,',
    '"ftp://example.net","the username","the password","ftp://example.net",,,,,',
    '"http://example.net","the username","the password","",,,,,',
    '"chrome://example_extension","the username","the password one","Example Login One",,,,,',
    '"chrome://example_extension","the username","the password two","Example Login Two",,,,,',
    '"file://","file: username","file: password",,"file://",,,,',
    '"https://js.example.com","javascript: username","javascript: password",,"javascript:",,,,',
  ];

  Assert.equal(actualRows.length, expectedRows.length, "Check number of lines");
  for (let i = 0; i < logins.length; i++) {
    let login = logins[i];
    Assert.equal(
      actualRows[i],
      expectedRows[i],
      `checking CSV correctly writes row at index=${i} \n${JSON.stringify(
        login
      )}\n`
    );
  }
});
