/* 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 chunking feature of `RemoteSettingsClient.#addResults()`.

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  SuggestionsMap:
    "resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
});

XPCOMUtils.defineLazyModuleGetters(this, {
  ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
});

// This overrides `SuggestionsMap.chunkSize`. Testing the actual value can make
// the test run too long. This is OK because the correctness of the chunking
// behavior doesn't depend on the chunk size.
const TEST_CHUNK_SIZE = 100;

add_task(async function init() {
  // Sanity check the actual `chunkSize` value.
  Assert.equal(
    typeof SuggestionsMap.chunkSize,
    "number",
    "Sanity check: chunkSize is a number"
  );
  Assert.greater(SuggestionsMap.chunkSize, 0, "Sanity check: chunkSize > 0");

  // Set our test value.
  SuggestionsMap.chunkSize = TEST_CHUNK_SIZE;
});

// Tests many suggestions with one keyword each.
add_task(async function chunking_singleKeyword() {
  let suggestionCounts = [
    1 * SuggestionsMap.chunkSize - 1,
    1 * SuggestionsMap.chunkSize,
    1 * SuggestionsMap.chunkSize + 1,
    2 * SuggestionsMap.chunkSize - 1,
    2 * SuggestionsMap.chunkSize,
    2 * SuggestionsMap.chunkSize + 1,
    3 * SuggestionsMap.chunkSize - 1,
    3 * SuggestionsMap.chunkSize,
    3 * SuggestionsMap.chunkSize + 1,
  ];
  for (let count of suggestionCounts) {
    await doChunkingTest(count, 1);
  }
});

// Tests a small number of suggestions with many keywords each.
add_task(async function chunking_manyKeywords() {
  let keywordCounts = [
    1 * SuggestionsMap.chunkSize - 1,
    1 * SuggestionsMap.chunkSize,
    1 * SuggestionsMap.chunkSize + 1,
    2 * SuggestionsMap.chunkSize - 1,
    2 * SuggestionsMap.chunkSize,
    2 * SuggestionsMap.chunkSize + 1,
    3 * SuggestionsMap.chunkSize - 1,
    3 * SuggestionsMap.chunkSize,
    3 * SuggestionsMap.chunkSize + 1,
  ];
  for (let suggestionCount = 1; suggestionCount <= 3; suggestionCount++) {
    for (let keywordCount of keywordCounts) {
      await doChunkingTest(suggestionCount, keywordCount);
    }
  }
});

async function doChunkingTest(suggestionCount, keywordCountPerSuggestion) {
  info(
    "Running chunking test: " +
      JSON.stringify({ suggestionCount, keywordCountPerSuggestion })
  );

  // Create `suggestionCount` suggestions, each with `keywordCountPerSuggestion`
  // keywords.
  let suggestions = [];
  for (let i = 0; i < suggestionCount; i++) {
    let keywords = [];
    for (let k = 0; k < keywordCountPerSuggestion; k++) {
      keywords.push(`keyword-${i}-${k}`);
    }
    suggestions.push({
      keywords,
      id: i,
      url: "http://example.com/" + i,
      title: "Suggestion " + i,
      click_url: "http://example.com/click",
      impression_url: "http://example.com/impression",
      advertiser: "TestAdvertiser",
      iab_category: "22 - Shopping",
    });
  }

  // Add the suggestions.
  let map = new SuggestionsMap();
  await map.add(suggestions);

  // Make sure all keyword-suggestion pairs have been added.
  for (let i = 0; i < suggestionCount; i++) {
    for (let k = 0; k < keywordCountPerSuggestion; k++) {
      let keyword = `keyword-${i}-${k}`;

      // Check the map. Logging all assertions takes a ton of time and makes the
      // test run much longer than it otherwise would, especially if `chunkSize`
      // is large, so only log failing assertions.
      let actualSuggestions = map.get(keyword);
      if (!ObjectUtils.deepEqual(actualSuggestions, [suggestions[i]])) {
        Assert.deepEqual(
          actualSuggestions,
          [suggestions[i]],
          `Suggestion ${i} is present for keyword ${keyword}`
        );
      }
    }
  }
}

add_task(async function duplicateKeywords() {
  let suggestions = [
    {
      title: "suggestion 0",
      keywords: ["a", "a", "a", "b", "b", "c"],
    },
    {
      title: "suggestion 1",
      keywords: ["b", "c", "d"],
    },
    {
      title: "suggestion 2",
      keywords: ["c", "d", "e"],
    },
    {
      title: "suggestion 3",
      keywords: ["f", "f"],
    },
  ];

  let expectedIndexesByKeyword = {
    a: [0],
    b: [0, 1],
    c: [0, 1, 2],
    d: [1, 2],
    e: [2],
    f: [3],
  };

  let map = new SuggestionsMap();
  await map.add(suggestions);

  for (let [keyword, indexes] of Object.entries(expectedIndexesByKeyword)) {
    Assert.deepEqual(
      map.get(keyword),
      indexes.map(i => suggestions[i]),
      "get() with keyword: " + keyword
    );
  }
});

add_task(async function mapKeywords() {
  let suggestions = [
    {
      title: "suggestion 0",
      keywords: ["a", "a", "a", "b", "b", "c"],
    },
    {
      title: "suggestion 1",
      keywords: ["b", "c", "d"],
    },
    {
      title: "suggestion 2",
      keywords: ["c", "d", "e"],
    },
    {
      title: "suggestion 3",
      keywords: ["f", "f"],
    },
  ];

  let expectedIndexesByKeyword = {
    a: [],
    b: [],
    c: [],
    d: [],
    e: [],
    f: [],
    ax: [0],
    bx: [0, 1],
    cx: [0, 1, 2],
    dx: [1, 2],
    ex: [2],
    fx: [3],
    fy: [3],
    fz: [3],
  };

  let map = new SuggestionsMap();
  await map.add(suggestions, keyword => {
    if (keyword == "f") {
      return [keyword + "x", keyword + "y", keyword + "z"];
    }
    return [keyword + "x"];
  });

  for (let [keyword, indexes] of Object.entries(expectedIndexesByKeyword)) {
    Assert.deepEqual(
      map.get(keyword),
      indexes.map(i => suggestions[i]),
      "get() with keyword: " + keyword
    );
  }
});
