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

/**
 * Tests to ensure that icons for application provided engines are correctly
 * updated from remote settings.
 */

"use strict";

// A skeleton configuration that gets filled in from TESTS during `add_setup`.
let CONFIG = [
  { identifier: "engine_no_initial_icon" },
  { identifier: "engine_icon_updates" },
  { identifier: "engine_icon_not_local" },
  { identifier: "engine_icon_out_of_date" },
];

async function assertIconMatches(actualIconData, expectedIcon) {
  let expectedBuffer = new Uint8Array(await getFileDataBuffer(expectedIcon));

  Assert.equal(
    actualIconData.length,
    expectedBuffer.length,
    "Should have received matching buffer lengths for the expected icon"
  );
  Assert.ok(
    actualIconData.every((value, index) => value === expectedBuffer[index]),
    "Should have received matching data for the expected icon"
  );
}

async function assertEngineIcon(engineId, expectedIcon, size = 0) {
  // Default value for size is 0 to mimic XPCOM optional parameters.
  let engine = SearchService.getEngineById(engineId);
  let engineIconURL = await engine.getIconURL(size);

  if (expectedIcon) {
    Assert.notEqual(
      engineIconURL,
      null,
      "Should have an icon URL for the engine."
    );

    let response = await fetch(engineIconURL);
    let actualBuffer = new Uint8Array(await response.arrayBuffer());

    await assertIconMatches(actualBuffer, expectedIcon);
  } else {
    Assert.equal(
      engineIconURL,
      null,
      "Should not have an icon URL for the engine."
    );
  }
}

let originalIconId = Services.uuid.generateUUID().toString();
let client;

add_setup(async function setup() {
  useHttpServer();
  SearchTestUtils.useMockIdleService();

  client = RemoteSettings("search-config-icons");
  await client.db.clear();

  sinon
    .stub(RemoteSettingsUtils, "baseAttachmentsURL")
    .returns(`${gHttpURL}/icons/`);

  // Add some initial records and attachments into the remote settings collection.
  await insertRecordIntoCollection(client, {
    id: originalIconId,
    filename: "remoteIcon.ico",
    // This uses a wildcard match to test the icon is still applied correctly.
    engineIdentifiers: ["engine_icon_upd*"],
    imageSize: 16,
  });
  await insertRecordIntoCollection(client, {
    id: Services.uuid.generateUUID().toString(),
    filename: "svgIcon.svg",
    engineIdentifiers: ["engine_icon_updates"],
    imageSize: 32,
  });
  // This attachment is not cached, so we don't have it locally.
  await insertRecordIntoCollection(
    client,
    {
      id: Services.uuid.generateUUID().toString(),
      filename: "bigIcon.ico",
      engineIdentifiers: [
        // This also tests multiple engine idenifiers works.
        "enterprise",
        "next_generation",
        "engine_icon_not_local",
      ],
      imageSize: 16,
    },
    false
  );

  // Add a record that is out of date, and update it with a newer one, but don't
  // cache the attachment for the new one.
  let outOfDateRecordId = Services.uuid.generateUUID().toString();
  await insertRecordIntoCollection(
    client,
    {
      id: outOfDateRecordId,
      filename: "remoteIcon.ico",
      engineIdentifiers: ["engine_icon_out_of_date"],
      imageSize: 16,
      // 10 minutes ago.
      lastModified: Date.now() - 600000,
    },
    true
  );
  let { record } = await mockRecordWithAttachment({
    id: outOfDateRecordId,
    filename: "bigIcon.ico",
    engineIdentifiers: ["engine_icon_out_of_date"],
    imageSize: 16,
  });
  await client.db.update(record);
  await client.db.importChanges({}, record.lastModified);

  SearchTestUtils.setRemoteSettingsConfig(CONFIG);
  await SearchService.init();

  // Testing that an icon is not local generates a `Could not find {id}...`
  // message.
  consoleAllowList.push("Could not find");

  registerCleanupFunction(async () => {
    sinon.restore();
  });
});

add_task(async function test_icon_added_unknown_engine() {
  // If the engine is unknown, and this is a new icon, we should still download
  // the icon, in case the engine is added to the configuration later.
  let newIconId = Services.uuid.generateUUID().toString();

  let mock = await mockRecordWithAttachment({
    id: newIconId,
    filename: "bigIcon.ico",
    engineIdentifiers: ["engine_unknown"],
    imageSize: 16,
  });
  await client.db.update(mock.record, Date.now());

  await client.emit("sync", {
    data: {
      current: [mock.record],
      created: [mock.record],
      updated: [],
      deleted: [],
    },
  });

  SearchTestUtils.idleService._fireObservers("idle");

  let icon;
  await TestUtils.waitForCondition(async () => {
    try {
      icon = await client.attachments.get(mock.record);
    } catch (ex) {
      // Do nothing.
    }
    return !!icon;
  }, "Should have loaded the icon into the attachments store.");

  await assertIconMatches(new Uint8Array(icon.buffer), "bigIcon.ico");
});

add_task(async function test_icon_added_existing_engine() {
  // If the engine is unknown, and this is a new icon, we should still download
  // it, in case the engine is added to the configuration later.
  let newIconId = Services.uuid.generateUUID().toString();

  let mock = await mockRecordWithAttachment({
    id: newIconId,
    filename: "bigIcon.ico",
    engineIdentifiers: ["engine_no_initial_icon"],
    imageSize: 16,
  });
  await client.db.update(mock.record, Date.now());

  let promiseIconChanged = SearchTestUtils.promiseSearchNotification(
    SearchUtils.MODIFIED_TYPE.ICON_CHANGED,
    SearchUtils.TOPIC_ENGINE_MODIFIED
  );

  await client.emit("sync", {
    data: {
      current: [mock.record],
      created: [mock.record],
      updated: [],
      deleted: [],
    },
  });

  SearchTestUtils.idleService._fireObservers("idle");

  await promiseIconChanged;
  await assertEngineIcon("engine_no_initial_icon", "bigIcon.ico");
});

add_task(async function test_icon_updated() {
  // Test that when an update for an engine icon is received, the engine is
  // correctly updated.

  // Check the engine has the expected icon to start with.
  await assertEngineIcon("engine_icon_updates", "remoteIcon.ico");

  // Update the icon for the engine.
  let mock = await mockRecordWithAttachment({
    id: originalIconId,
    filename: "bigIcon.ico",
    engineIdentifiers: ["engine_icon_upd*"],
    imageSize: 16,
  });
  await client.db.update(mock.record, Date.now());

  let promiseIconChanged = SearchTestUtils.promiseSearchNotification(
    SearchUtils.MODIFIED_TYPE.ICON_CHANGED,
    SearchUtils.TOPIC_ENGINE_MODIFIED
  );

  await client.emit("sync", {
    data: {
      current: [mock.record],
      created: [],
      updated: [{ new: mock.record }],
      deleted: [],
    },
  });
  SearchTestUtils.idleService._fireObservers("idle");

  await promiseIconChanged;
  await assertEngineIcon("engine_icon_updates", "bigIcon.ico");
  info("Other icon should be untouched.");
  await assertEngineIcon("engine_icon_updates", "svgIcon.svg", 32);
});

add_task(async function test_icon_not_local() {
  // Tests that a download is queued and triggered when the icon for an engine
  // is not in either the local dump nor the cache.

  await assertEngineIcon("engine_icon_not_local", null);

  // A download should have been queued, so fire idle to trigger it.
  let promiseIconChanged = SearchTestUtils.promiseSearchNotification(
    SearchUtils.MODIFIED_TYPE.ICON_CHANGED,
    SearchUtils.TOPIC_ENGINE_MODIFIED
  );
  SearchTestUtils.idleService._fireObservers("idle");
  await promiseIconChanged;

  await assertEngineIcon("engine_icon_not_local", "bigIcon.ico");
});

add_task(async function test_icon_out_of_date() {
  // Tests that a download is queued and triggered when the icon for an engine
  // is not in either the local dump nor the cache.

  await assertEngineIcon("engine_icon_out_of_date", "remoteIcon.ico");

  // A download should have been queued, so fire idle to trigger it.
  let promiseIconChanged = SearchTestUtils.promiseSearchNotification(
    SearchUtils.MODIFIED_TYPE.ICON_CHANGED,
    SearchUtils.TOPIC_ENGINE_MODIFIED
  );
  SearchTestUtils.idleService._fireObservers("idle");
  await promiseIconChanged;

  await assertEngineIcon("engine_icon_out_of_date", "bigIcon.ico");
});
