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

/**
 * This tests that the engine selector works handles various remote settings
 * scenarios correctly, e.g. configuration updates, bad databases.
 */

"use strict";

let TEST_CONFIG = [
  {
    recordType: "defaultEngines",
    globalDefault: "lycos",
    specificDefaults: [],
  },
  {
    orders: [],
    recordType: "engineOrders",
  },
];

for (let engineName of ["lycos", "altavista", "aol", "excite"]) {
  TEST_CONFIG.push({
    base: {
      name: engineName,
      urls: {
        search: {
          base: `https://example.com/${engineName}`,
          searchTermParamName: "q",
        },
      },
    },
    identifier: engineName,
    recordType: "engine",
  });
}

let getStub;

add_setup(async function () {
  const searchConfigSettings = await RemoteSettings(SearchUtils.SETTINGS_KEY);
  getStub = sinon.stub(searchConfigSettings, "get");

  TEST_CONFIG = SearchTestUtils.expandPartialConfig(TEST_CONFIG);

  // We expect this error from remove settings as we're invalidating the
  // signature.
  consoleAllowList.push("Invalid content signature (abc)");
  // We also test returning an empty configuration.
  consoleAllowList.push("Received empty search configuration");

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

add_task(async function test_selector_basic_get() {
  const listenerSpy = sinon.spy();
  const engineSelector = new SearchEngineSelector(listenerSpy);
  getStub.onFirstCall().returns(TEST_CONFIG);

  const { engines } = await engineSelector.fetchEngineConfiguration({
    locale: "en-US",
    region: "default",
  });

  Assert.deepEqual(
    engines.map(e => e.name),
    ["lycos", "altavista", "aol", "excite"],
    "Should have obtained the correct data from the database."
  );
  Assert.ok(listenerSpy.notCalled, "Should not have called the listener");
});

add_task(async function test_selector_get_reentry() {
  const listenerSpy = sinon.spy();
  const engineSelector = new SearchEngineSelector(listenerSpy);
  let promise = Promise.withResolvers();
  getStub.resetHistory();
  getStub.onFirstCall().returns(promise.promise);
  engineSelector.clearCachedConfigurationForTests();

  let firstResult;
  let secondResult;

  const firstCallPromise = engineSelector
    .fetchEngineConfiguration({
      locale: "en-US",
      region: "default",
    })
    .then(result => (firstResult = result.engines));

  const secondCallPromise = engineSelector
    .fetchEngineConfiguration({
      locale: "en-US",
      region: "default",
    })
    .then(result => (secondResult = result.engines));

  Assert.strictEqual(
    firstResult,
    undefined,
    "Should not have returned the first result yet."
  );

  Assert.strictEqual(
    secondResult,
    undefined,
    "Should not have returned the second result yet."
  );

  promise.resolve(TEST_CONFIG);

  await Promise.all([firstCallPromise, secondCallPromise]);
  Assert.deepEqual(
    firstResult.map(e => e.name),
    ["lycos", "altavista", "aol", "excite"],
    "Should have returned the correct data to the first call"
  );

  Assert.deepEqual(
    secondResult.map(e => e.name),
    ["lycos", "altavista", "aol", "excite"],
    "Should have returned the correct data to the second call"
  );
  Assert.ok(listenerSpy.notCalled, "Should not have called the listener");
});

add_task(async function test_selector_config_update() {
  const listenerSpy = sinon.spy();
  const engineSelector = new SearchEngineSelector(listenerSpy);
  getStub.resetHistory();
  getStub.onFirstCall().returns(TEST_CONFIG);

  const { engines } = await engineSelector.fetchEngineConfiguration({
    locale: "en-US",
    region: "default",
  });

  Assert.deepEqual(
    engines.map(e => e.name),
    ["lycos", "altavista", "aol", "excite"],
    "Should have got the correct configuration"
  );

  Assert.ok(listenerSpy.notCalled, "Should not have called the listener yet");

  const NEW_DATA = SearchTestUtils.expandPartialConfig([
    {
      recordType: "engine",
      identifier: "askjeeves",
      base: { name: "askjeeves" },
    },
    {
      recordType: "defaultEngines",
      globalDefault: "askjeeves",
      specificDefaults: [],
    },
    {
      orders: [],
      recordType: "engineOrders",
    },
  ]);

  getStub.resetHistory();
  getStub.onFirstCall().returns(NEW_DATA);
  await RemoteSettings(SearchUtils.SETTINGS_KEY).emit("sync", {
    data: {
      current: NEW_DATA,
    },
  });

  Assert.ok(listenerSpy.called, "Should have called the listener");

  const result = await engineSelector.fetchEngineConfiguration({
    locale: "en-US",
    region: "default",
  });

  Assert.deepEqual(
    result.engines.map(e => e.name),
    ["askjeeves"],
    "Should have updated the configuration with the new data"
  );
});

add_task(async function test_selector_db_modification() {
  const engineSelector = new SearchEngineSelector();
  // Fill the database with some values that we can use to test that it is cleared.
  const db = RemoteSettings(SearchUtils.SETTINGS_KEY).db;
  await db.importChanges(
    {},
    Date.now(),
    [
      {
        recordType: "engine",
        id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c0",
        identifier: "askjeeves",
        base: { name: "askjeeves" },
        variants: [{ environment: { allRegionsAndLocales: "true" } }],
      },
    ],
    { clear: true }
  );

  // Stub the get() so that the first call simulates a signature error, and
  // the second simulates success reading from the dump.
  getStub.resetHistory();
  getStub
    .onFirstCall()
    .rejects(new RemoteSettingsClient.InvalidSignatureError("abc"));
  getStub.onSecondCall().returns(TEST_CONFIG);

  let result = await engineSelector.fetchEngineConfiguration({
    locale: "en-US",
    region: "default",
  });

  Assert.ok(
    getStub.calledTwice,
    "Should have called the get() function twice."
  );

  const dbTimestamp = await db.getLastModified();
  Assert.equal(dbTimestamp, null, "Should have cleared the database.");

  Assert.deepEqual(
    result.engines.map(e => e.name),
    ["lycos", "altavista", "aol", "excite"],
    "Should have returned the correct data."
  );
});

add_task(async function test_selector_db_modification_never_succeeds() {
  const engineSelector = new SearchEngineSelector();
  // Fill the database with some values that we can use to test that it is cleared.
  const db = RemoteSettings(SearchUtils.SETTINGS_KEY).db;
  await db.importChanges(
    {},
    Date.now(),
    [
      {
        recordType: "engine",
        id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c0",
        identifier: "askjeeves",
        base: { name: "askjeeves" },
        variants: [{ environment: { allRegionsAndLocales: "true" } }],
      },
      {
        recordType: "defaultEngines",
        id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c1",
        globalDefault: "lycos",
        specificDefaults: [],
      },
      {
        id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c2",
        orders: [],
        recordType: "engineOrders",
      },
    ],
    {
      clear: true,
    }
  );

  // Now simulate the condition where for some reason we never get a
  // valid result.
  getStub.reset();
  getStub.rejects(new RemoteSettingsClient.InvalidSignatureError("abc"));

  await Assert.rejects(
    engineSelector.fetchEngineConfiguration({
      locale: "en-US",
      region: "default",
    }),
    ex => ex.result == Cr.NS_ERROR_UNEXPECTED,
    "Should have rejected loading the engine configuration"
  );

  Assert.ok(
    getStub.calledTwice,
    "Should have called the get() function twice."
  );

  const dbTimestamp = await db.getLastModified();
  Assert.equal(dbTimestamp, null, "Should have cleared the database.");
});

add_task(async function test_empty_results() {
  // Check that returning an empty result re-tries.
  const engineSelector = new SearchEngineSelector();
  // Fill the database with some values that we can use to test that it is cleared.
  const db = RemoteSettings(SearchUtils.SETTINGS_KEY).db;
  await db.importChanges(
    {},
    Date.now(),
    [
      {
        recordType: "engine",
        id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c0",
        identifier: "askjeeves",
        base: { name: "askjeeves" },
        variants: [{ environment: { allRegionsAndLocales: "true" } }],
      },
    ],
    {
      clear: true,
    }
  );

  // Stub the get() so that the first call simulates an empty database, and
  // the second simulates success reading from the dump.
  getStub.resetHistory();
  getStub.onFirstCall().returns([]);
  getStub.onSecondCall().returns(TEST_CONFIG);

  let result = await engineSelector.fetchEngineConfiguration({
    locale: "en-US",
    region: "default",
  });

  Assert.ok(
    getStub.calledTwice,
    "Should have called the get() function twice."
  );

  const dbTimestamp = await db.getLastModified();
  Assert.equal(dbTimestamp, null, "Should have cleared the database.");

  Assert.deepEqual(
    result.engines.map(e => e.name),
    ["lycos", "altavista", "aol", "excite"],
    "Should have returned the correct data."
  );
});
