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

"use strict";

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

const { PlacesQuery } = ChromeUtils.importESModule(
  "resource://gre/modules/PlacesQuery.sys.mjs"
);

const placesQuery = new PlacesQuery();

registerCleanupFunction(() => placesQuery.close());

async function waitForUpdateHistoryTask(updateTask) {
  const promise = new Promise(resolve =>
    placesQuery.observeHistory(newHistory => resolve(newHistory))
  );
  await updateTask();
  return promise;
}

add_task(async function test_visits_cache_is_updated() {
  const now = new Date();
  info("Insert the first visit.");
  await PlacesUtils.history.insert({
    url: "https://www.example.com/",
    title: "Example Domain",
    visits: [{ date: now }],
  });
  let history = await placesQuery.getHistory();
  const mapKey = placesQuery.getStartOfDayTimestamp(now);
  history = history.get(mapKey);
  Assert.equal(history.length, 1);
  Assert.equal(history[0].url, "https://www.example.com/");
  Assert.equal(history[0].date.getTime(), now.getTime());
  Assert.equal(history[0].title, "Example Domain");

  info("Insert the next visit.");
  history = await waitForUpdateHistoryTask(() =>
    PlacesUtils.history.insert({
      url: "https://example.net/",
      visits: [{ date: now }],
    })
  );
  history = history.get(mapKey);
  Assert.equal(history.length, 2);
  Assert.equal(
    history[0].url,
    "https://example.net/",
    "The most recent visit should come first."
  );
  Assert.equal(history[0].date.getTime(), now.getTime());

  info("Remove the first visit.");
  history = await waitForUpdateHistoryTask(() =>
    PlacesUtils.history.remove("https://www.example.com/")
  );
  history = history.get(mapKey);
  Assert.equal(history.length, 1);
  Assert.equal(history[0].url, "https://example.net/");

  info("Remove all visits.");
  history = await waitForUpdateHistoryTask(() => PlacesUtils.history.clear());
  Assert.equal(history.size, 0);
});

add_task(async function test_filter_visits_by_age() {
  const now = new Date();
  await PlacesUtils.history.insertMany([
    {
      url: "https://www.example.com/",
      visits: [{ date: new Date("2000-01-01T12:00:00") }],
    },
    {
      url: "https://example.net/",
      visits: [{ date: now }],
    },
  ]);
  let history = await placesQuery.getHistory({ daysOld: 1 });
  Assert.equal(history.size, 1, "The older visit should be excluded.");
  Assert.equal(
    history.get(placesQuery.getStartOfDayTimestamp(now))[0].url,
    "https://example.net/"
  );
  await PlacesUtils.history.clear();
});

add_task(async function test_visits_sort_option() {
  const now = new Date();
  info("Get history sorted by site.");
  await PlacesUtils.history.insertMany([
    { url: "https://www.reddit.com/", visits: [{ date: now }] },
    { url: "https://twitter.com/", visits: [{ date: now }] },
  ]);
  let history = await placesQuery.getHistory({ sortBy: "site" });
  ["reddit.com", "twitter.com"].forEach(domain =>
    Assert.ok(history.has(domain))
  );

  info("Insert the next visit.");
  history = await waitForUpdateHistoryTask(() =>
    PlacesUtils.history.insert({
      url: "https://www.wikipedia.org/",
      visits: [{ date: now }],
    })
  );
  ["reddit.com", "twitter.com", "wikipedia.org"].forEach(domain =>
    Assert.ok(history.has(domain))
  );

  info("Update the sort order.");
  history = await placesQuery.getHistory({ sortBy: "date" });
  const mapKey = placesQuery.getStartOfDayTimestamp(now);
  Assert.equal(history.get(mapKey).length, 3);

  info("Insert the next visit.");
  history = await waitForUpdateHistoryTask(() =>
    PlacesUtils.history.insert({
      url: "https://www.youtube.com/",
      visits: [{ date: now }],
    })
  );
  Assert.equal(history.get(mapKey).length, 4);
  await PlacesUtils.history.clear();
});

add_task(async function test_visits_limit_option() {
  await PlacesUtils.history.insertMany([
    {
      url: "https://www.example.com/",
      visits: [{ date: new Date() }],
    },
    {
      url: "https://example.net/",
      visits: [{ date: new Date() }],
    },
  ]);
  let history = await placesQuery.getHistory({ limit: 1 });
  Assert.equal(
    [...history.values()].reduce((acc, { length }) => acc + length, 0),
    1,
    "Number of visits should be limited to 1."
  );
  await PlacesUtils.history.clear();
});

add_task(async function test_dedupe_visits_by_url() {
  const today = new Date();
  const yesterday = new Date(
    today.getFullYear(),
    today.getMonth(),
    today.getDate() - 1
  );
  await PlacesUtils.history.insertMany([
    {
      url: "https://www.example.com/",
      visits: [{ date: yesterday }],
    },
    {
      url: "https://www.example.com/",
      visits: [{ date: today }],
    },
    {
      url: "https://www.example.com/",
      visits: [{ date: today }],
    },
  ]);
  info("Get history sorted by date.");
  let history = await placesQuery.getHistory({ sortBy: "date" });
  Assert.equal(
    history.get(placesQuery.getStartOfDayTimestamp(yesterday)).length,
    1,
    "There was only one visit from yesterday."
  );
  Assert.equal(
    history.get(placesQuery.getStartOfDayTimestamp(today)).length,
    1,
    "The duplicate visit from today should be removed."
  );
  history = await waitForUpdateHistoryTask(() =>
    PlacesUtils.history.insert({
      url: "https://www.example.com/",
      visits: [{ date: today }],
    })
  );
  Assert.equal(
    history.get(placesQuery.getStartOfDayTimestamp(today)).length,
    1,
    "Visits inserted from `page-visited` events should be deduped."
  );

  info("Get history sorted by site.");
  history = await placesQuery.getHistory({ sortBy: "site" });
  Assert.equal(
    history.get("example.com").length,
    1,
    "The duplicate visits for this site should be removed."
  );
  history = await waitForUpdateHistoryTask(() =>
    PlacesUtils.history.insert({
      url: "https://www.example.com/",
      visits: [{ date: yesterday }],
    })
  );
  const visits = history.get("example.com");
  Assert.equal(
    visits.length,
    1,
    "Visits inserted from `page-visited` events should be deduped."
  );
  Assert.equal(
    visits[0].date.getTime(),
    today.getTime(),
    "Deduping keeps the most recent visit."
  );

  await PlacesUtils.history.clear();
});

add_task(async function test_search_visits() {
  const now = new Date();
  await PlacesUtils.history.insertMany([
    {
      url: "https://www.example.com/",
      title: "First Visit",
      visits: [{ date: now }],
    },
    {
      url: "https://example.net/",
      title: "Second Visit",
      visits: [{ date: now }],
    },
  ]);

  let results = await placesQuery.searchHistory("Visit");
  Assert.equal(results.length, 2, "Both visits match the search query.");

  results = await placesQuery.searchHistory("First Visit");
  Assert.equal(results.length, 1, "One visit matches the search query.");

  results = await placesQuery.searchHistory("Bogus");
  Assert.equal(results.length, 0, "Neither visit matches the search query.");

  await PlacesUtils.history.clear();
});

add_task(async function test_search_interrupt() {
  const { promise, reject } = Promise.withResolvers();
  const mockDatabase = {
    executeCached: async (_, { query }) => {
      if (query === "First Query") {
        // Simulate a slow-running query which runs long enough to be
        // interrupted by the next one.
        await promise;
      }
      return [];
    },
    interrupt: () => reject("interrupt() was called."),
  };
  const stub = sinon
    .stub(PlacesUtils, "promiseLargeCacheDBConnection")
    .resolves(mockDatabase);

  const promiseFirstQuery = placesQuery.searchHistory("First Query");
  const promiseSecondQuery = placesQuery.searchHistory("Second Query");
  await Assert.rejects(
    promiseFirstQuery,
    /interrupt/,
    "The first query was interrupted."
  );
  ok(await promiseSecondQuery, "The second query resolved normally.");

  stub.restore();
});

add_task(async function test_sort_by_date_and_site() {
  const today = new Date();
  const yesterday = new Date(
    today.getFullYear(),
    today.getMonth(),
    today.getDate() - 1
  );
  await PlacesUtils.history.insertMany([
    {
      url: "https://en.wikipedia.org/wiki/Mozilla",
      visits: [{ date: yesterday }],
    },
    {
      url: "https://en.wikipedia.org/wiki/Mozilla",
      visits: [{ date: today }],
    },
    {
      url: "https://en.wikipedia.org/wiki/Firefox",
      visits: [{ date: today }],
    },
    {
      url: "https://www.example.com/",
      visits: [{ date: today }],
    },
  ]);

  let history = await placesQuery.getHistory({ sortBy: "datesite" });
  const sitesFromYesterday = history.get(
    placesQuery.getStartOfDayTimestamp(yesterday)
  );
  Assert.equal(sitesFromYesterday.size, 1, "One site was visited yesterday.");
  Assert.equal(
    sitesFromYesterday.get("en.wikipedia.org").length,
    1,
    "One visit was made to en.wikipedia.org."
  );

  let sitesFromToday = history.get(placesQuery.getStartOfDayTimestamp(today));
  Assert.equal(sitesFromToday.size, 2, "Two sites were visited today.");
  Assert.equal(
    sitesFromToday.get("en.wikipedia.org").length,
    2,
    "Two visits were made to en.wikipedia.org."
  );
  Assert.equal(
    sitesFromToday.get("example.com").length,
    1,
    "One visit was made to example.com."
  );

  info("Insert the next visit.");
  history = await waitForUpdateHistoryTask(() =>
    PlacesUtils.history.insert({
      url: "https://example.net/",
      visits: [{ date: today }],
    })
  );
  sitesFromToday = history.get(placesQuery.getStartOfDayTimestamp(today));
  Assert.equal(sitesFromToday.size, 3, "Three sites were visited today.");
  Assert.equal(
    sitesFromToday.get("example.net").length,
    1,
    "One visit was made to example.net."
  );

  await PlacesUtils.history.clear();
});

add_task(async function test_sort_by_last_visited() {
  const today = new Date();
  const yesterday = new Date(
    today.getFullYear(),
    today.getMonth(),
    today.getDate() - 1
  );

  info("Insert multiple visits to the same URL.");
  await PlacesUtils.history.insertMany([
    {
      url: "https://www.example.com/",
      visits: [{ date: yesterday }],
    },
    {
      url: "https://www.example.com/",
      visits: [{ date: today }],
    },
  ]);

  let history = await placesQuery.getHistory({ sortBy: "lastvisited" });
  Assert.equal(history.length, 1, "Visits to the same URL are deduped.");

  info("Insert a more recent visit.");
  history = await waitForUpdateHistoryTask(() =>
    PlacesUtils.history.insert({
      url: "https://www.reddit.com/",
      visits: [{ date: new Date() }],
    })
  );
  Assert.deepEqual(
    history.map(({ url }) => url),
    ["https://www.reddit.com/", "https://www.example.com/"],
    "History is sorted by recency."
  );

  await PlacesUtils.history.clear();
});
