/**
 * Tests the migration algorithm in profileStorage.
 */

"use strict";

let FormAutofillStorage;
add_setup(async () => {
  ({ FormAutofillStorage } = ChromeUtils.importESModule(
    "resource://autofill/FormAutofillStorage.sys.mjs"
  ));
});

const TEST_STORE_FILE_NAME = "test-profile.json";

const { ADDRESS_SCHEMA_VERSION } = ChromeUtils.importESModule(
  "resource://autofill/FormAutofillStorageBase.sys.mjs"
);
const { CREDIT_CARD_SCHEMA_VERSION } = ChromeUtils.importESModule(
  "resource://autofill/FormAutofillStorageBase.sys.mjs"
);

const ADDRESS_TESTCASES = [
  {
    description:
      "The record version is equal to the current version. The migration shouldn't be invoked.",
    record: {
      guid: "test-guid",
      version: ADDRESS_SCHEMA_VERSION,
      "given-name": "Timothy",
      name: "John", // The cached name field doesn't align "given-name" but it
      // won't be recomputed because the migration isn't invoked.
    },
    expectedResult: {
      guid: "test-guid",
      version: ADDRESS_SCHEMA_VERSION,
      "given-name": "Timothy",
      name: "John",
    },
  },
  {
    description:
      "The record version is greater than the current version. The migration shouldn't be invoked.",
    record: {
      guid: "test-guid",
      version: 99,
      "given-name": "Timothy",
      name: "John",
    },
    expectedResult: {
      guid: "test-guid",
      version: 99,
      "given-name": "Timothy",
      name: "John",
    },
  },
  {
    description:
      "The record version is less than the current version. The migration should be invoked.",
    record: {
      guid: "test-guid",
      version: 0,
      "given-name": "Timothy",
      name: "John",
    },
    expectedResult: {
      guid: "test-guid",
      version: ADDRESS_SCHEMA_VERSION,
      "given-name": "Timothy",
      name: "Timothy",
    },
  },
  {
    description:
      "The record version is omitted. The migration should be invoked.",
    record: {
      guid: "test-guid",
      "given-name": "Timothy",
      name: "John",
      "unknown-1": "an unknown field from another client",
    },
    expectedResult: {
      guid: "test-guid",
      version: ADDRESS_SCHEMA_VERSION,
      "given-name": "Timothy",
      name: "Timothy",
      "unknown-1": "an unknown field from another client",
    },
  },
  {
    description:
      "The record version is an invalid value. The migration should be invoked.",
    record: {
      guid: "test-guid",
      version: "ABCDE",
      "given-name": "Timothy",
      name: "John",
      "unknown-1": "an unknown field from another client",
    },
    expectedResult: {
      guid: "test-guid",
      version: ADDRESS_SCHEMA_VERSION,
      "given-name": "Timothy",
      name: "Timothy",
      "unknown-1": "an unknown field from another client",
    },
  },
  {
    description:
      "The omitted computed fields should be always recomputed even the record version is up-to-date.",
    record: {
      guid: "test-guid",
      version: ADDRESS_SCHEMA_VERSION,
      "given-name": "Timothy",
    },
    expectedResult: {
      guid: "test-guid",
      version: ADDRESS_SCHEMA_VERSION,
      "given-name": "Timothy",
      name: "Timothy",
    },
  },
  {
    description: "The migration shouldn't be invoked on tombstones.",
    record: {
      guid: "test-guid",
      timeLastModified: 12345,
      deleted: true,
    },
    expectedResult: {
      guid: "test-guid",
      timeLastModified: 12345,
      deleted: true,

      // Make sure no new fields are appended.
      version: undefined,
      name: undefined,
    },
  },
];

const CREDIT_CARD_TESTCASES = [
  {
    description:
      "The record version is equal to the current version. The migration shouldn't be invoked.",
    record: {
      guid: "test-guid",
      version: CREDIT_CARD_SCHEMA_VERSION,
      "cc-name": "Timothy",
      "cc-given-name": "John", // The cached "cc-given-name" field doesn't align
      // "cc-name" but it won't be recomputed because
      // the migration isn't invoked.
    },
    expectedResult: {
      guid: "test-guid",
      version: CREDIT_CARD_SCHEMA_VERSION,
      "cc-name": "Timothy",
      "cc-given-name": "John",
    },
  },
  {
    description:
      "The record version is greater than the current version. The migration shouldn't be invoked.",
    record: {
      guid: "test-guid",
      version: 99,
      "cc-name": "Timothy",
      "cc-given-name": "John",
    },
    expectedResult: {
      guid: "test-guid",
      version: 99,
      "cc-name": "Timothy",
      "cc-given-name": "John",
    },
  },
  {
    description:
      "The record version is less than the current version. The migration should be invoked.",
    record: {
      guid: "test-guid",
      version: 0,
      "cc-name": "Timothy",
      "cc-given-name": "John",
    },
    expectedResult: {
      guid: "test-guid",
      version: CREDIT_CARD_SCHEMA_VERSION,
      "cc-name": "Timothy",
      "cc-given-name": "Timothy",
    },
  },
  {
    description:
      "The record version is omitted. The migration should be invoked.",
    record: {
      guid: "test-guid",
      "cc-name": "Timothy",
      "cc-given-name": "John",
      "unknown-1": "an unknown field from another client",
    },
    expectedResult: {
      guid: "test-guid",
      version: CREDIT_CARD_SCHEMA_VERSION,
      "cc-name": "Timothy",
      "cc-given-name": "Timothy",
      "unknown-1": "an unknown field from another client",
    },
  },
  {
    description:
      "The record version is an invalid value. The migration should be invoked.",
    record: {
      guid: "test-guid",
      version: "ABCDE",
      "cc-name": "Timothy",
      "cc-given-name": "John",
      "unknown-1": "an unknown field from another client",
    },
    expectedResult: {
      guid: "test-guid",
      version: CREDIT_CARD_SCHEMA_VERSION,
      "cc-name": "Timothy",
      "cc-given-name": "Timothy",
      "unknown-1": "an unknown field from another client",
    },
  },
  {
    description:
      "The omitted computed fields should be always recomputed even the record version is up-to-date.",
    record: {
      guid: "test-guid",
      version: CREDIT_CARD_SCHEMA_VERSION,
      "cc-name": "Timothy",
    },
    expectedResult: {
      guid: "test-guid",
      version: CREDIT_CARD_SCHEMA_VERSION,
      "cc-name": "Timothy",
      "cc-given-name": "Timothy",
    },
  },
  {
    description: "The migration shouldn't be invoked on tombstones.",
    record: {
      guid: "test-guid",
      timeLastModified: 12345,
      deleted: true,
    },
    expectedResult: {
      guid: "test-guid",
      timeLastModified: 12345,
      deleted: true,

      // Make sure no new fields are appended.
      version: undefined,
      "cc-given-name": undefined,
    },
  },
];

let do_check_record_matches = (expectedRecord, record) => {
  for (let key in expectedRecord) {
    Assert.equal(expectedRecord[key], record[key]);
  }
};

add_task(async function test_migrateAddressRecords() {
  let path = getTempFile(TEST_STORE_FILE_NAME).path;

  let profileStorage = new FormAutofillStorage(path);
  await profileStorage.initialize();

  for (let testcase of ADDRESS_TESTCASES) {
    info(testcase.description);
    profileStorage._store.data.addresses = [testcase.record];
    await profileStorage.addresses._migrateRecord(testcase.record, 0);
    do_check_record_matches(
      testcase.expectedResult,
      profileStorage.addresses._data[0]
    );
  }
});

add_task(async function test_migrateCreditCardRecords() {
  let path = getTempFile(TEST_STORE_FILE_NAME).path;

  let profileStorage = new FormAutofillStorage(path);
  await profileStorage.initialize();

  for (let testcase of CREDIT_CARD_TESTCASES) {
    info(testcase.description);
    profileStorage._store.data.creditCards = [testcase.record];
    await profileStorage.creditCards._migrateRecord(testcase.record, 0);
    do_check_record_matches(
      testcase.expectedResult,
      profileStorage.creditCards._data[0]
    );
  }
});

add_task(async function test_migrateEncryptedCreditCardNumber() {
  let path = getTempFile(TEST_STORE_FILE_NAME).path;

  let profileStorage = new FormAutofillStorage(path);
  await profileStorage.initialize();

  info("v1 and v2 schema cards should be abandoned.");

  let v1record = {
    guid: "test-guid1",
    version: 1,
    "cc-name": "Timothy",
    "cc-number-encrypted": "aaaa",
  };

  let v2record = {
    guid: "test-guid2",
    version: 2,
    "cc-name": "Bob",
    "cc-number-encrypted": "bbbb",
  };

  profileStorage._store.data.creditCards = [v1record, v2record];
  await profileStorage.creditCards._migrateRecord(v1record, 0);
  await profileStorage.creditCards._migrateRecord(v2record, 1);
  v1record = profileStorage.creditCards._data[0];
  v2record = profileStorage.creditCards._data[1];

  Assert.ok(v1record.deleted);
  Assert.ok(v2record.deleted);
});

add_task(async function test_migrateDeprecatedCreditCardV4() {
  let path = getTempFile(TEST_STORE_FILE_NAME).path;

  let profileStorage = new FormAutofillStorage(path);
  await profileStorage.initialize();

  let records = [
    {
      guid: "test-guid1",
      version: CREDIT_CARD_SCHEMA_VERSION,
      "cc-name": "Alice",
      _sync: {
        changeCounter: 0,
        lastSyncedFields: {},
      },
    },
    {
      guid: "test-guid2",
      version: 4,
      "cc-name": "Timothy",
      _sync: {
        changeCounter: 0,
        lastSyncedFields: {},
      },
    },
    {
      guid: "test-guid3",
      version: 4,
      "cc-name": "Bob",
    },
  ];

  profileStorage._store.data.creditCards = records;
  for (let idx = 0; idx < records.length; idx++) {
    await profileStorage.creditCards._migrateRecord(records[idx], idx);
  }

  profileStorage.creditCards.pullSyncChanges();

  // Record that has already synced before, do not sync again
  equal(getSyncChangeCounter(profileStorage.creditCards, records[0].guid), 0);

  // alaways force sync v4 record
  equal(records[1].version, CREDIT_CARD_SCHEMA_VERSION);
  equal(getSyncChangeCounter(profileStorage.creditCards, records[1].guid), 1);

  equal(records[2].version, CREDIT_CARD_SCHEMA_VERSION);
  equal(getSyncChangeCounter(profileStorage.creditCards, records[2].guid), 1);
});
