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

// Tests alternative pages frecency when the preference is enabled on startup.
// Note: the order of the tests here matters, since we are emulating subsquent
// starts of the recalculator component with different initial conditions.

async function restartRecalculator() {
  let subject = {};
  PlacesFrecencyRecalculator.observe(
    subject,
    "test-alternative-frecency-init",
    ""
  );
  await subject.promise;
}

async function getAllPages() {
  let db = await PlacesUtils.promiseDBConnection();
  let rows = await db.execute(`SELECT * FROM moz_places`);
  Assert.greater(rows.length, 0);
  return rows.map(r => ({
    url: r.getResultByName("url"),
    frecency: r.getResultByName("frecency"),
    recalc_frecency: r.getResultByName("recalc_frecency"),
    alt_frecency: r.getResultByName("alt_frecency"),
    recalc_alt_frecency: r.getResultByName("recalc_alt_frecency"),
  }));
}

add_setup(async function () {
  await PlacesTestUtils.addVisits([
    "https://testdomain1.moz.org/",
    "https://testdomain2.moz.org/",
    "https://testdomain3.moz.org/",
    "https://zerofrecency.moz.org/",
  ]);

  await PlacesTestUtils.updateDatabaseValues(
    "moz_places",
    {
      frecency: 0,
      recalc_alt_frecency: 0,
    },
    {
      url: "https://zerofrecency.moz.org/",
    }
  );

  Assert.ok(
    PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.enabled,
    "Check the pref is enabled"
  );

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

add_task(async function test_init() {
  // Set alt_frecency to NULL and recalc_alt_frecency = 1 for the entries in
  // moz_places to verify they are recalculated.
  await PlacesTestUtils.updateDatabaseValues("moz_places", {
    alt_frecency: null,
    recalc_alt_frecency: 1,
  });

  // Upon startup of this test file, the constructor sets PlacesUtils.metadata
  // which will cause subsequent calls to restartRecalculator to never
  // immediately trigger a frecency recalculation. Thus, we deliberately reset
  // metadata for restartRecalculator() to simulate construction of the class
  // and trigger routines that should happen on startup.
  await PlacesTestUtils.clearMetadata();
  PlacesUtils.metadata.cache.clear();

  Assert.equal(
    await PlacesUtils.metadata.get(
      PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
      Object.create(null)
    ).version,
    undefined,
    "Check the algorithm version has not been stored"
  );

  await restartRecalculator();

  // Avoid hitting the cache, we want to check the actual database value.
  Assert.equal(
    (
      await PlacesUtils.metadata.get(
        PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
        Object.create(null)
      )
    ).version,
    PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables.version,
    "Check the algorithm version has been stored"
  );

  // Check all alternative frecencies have been calculated, since we just have
  // a few.
  let pages = await getAllPages();
  Assert.ok(
    pages.every(p => p.recalc_alt_frecency == 0),
    "All the entries have been recalculated"
  );
  Assert.ok(
    pages.some(p => p.frecency == 0 && p.alt_frecency == 0),
    "There's at least one entry with zero frecency"
  );
  Assert.ok(
    pages.every(p =>
      p.frecency == 0 ? p.alt_frecency == 0 : p.alt_frecency > 0
    ),
    "All the entries have been recalculated"
  );
  Assert.ok(
    PlacesFrecencyRecalculator.isRecalculationPending,
    "Recalculation should be pending"
  );
});

add_task(async function test_different_version() {
  let pages = await getAllPages();
  Assert.ok(
    pages.every(p => p.recalc_alt_frecency == 0),
    "All the entries should not need recalculation"
  );

  // Set alt_frecency to NULL to verify all the entries are recalculated.
  await PlacesTestUtils.updateDatabaseValues("moz_places", {
    alt_frecency: null,
  });

  // It doesn't matter that the version is, it just have to be different.
  let variables = await PlacesUtils.metadata.get(
    PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
    Object.create(null)
  );
  variables.version = 999;
  await PlacesUtils.metadata.set(
    PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
    variables
  );

  await restartRecalculator();

  // Check alternative frecency has been marked for recalculation.
  // Note just after init we reculate a chunk, and this test code is expected
  // to run before that... though we can't be sure, so if this starts failing
  // intermittently we'll have to add more synchronization test code.
  pages = await getAllPages();
  // Ensure moz_meta has been updated.
  Assert.ok(
    PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.enabled,
    "Check the pref is enabled"
  );
  // Avoid hitting the cache, we want to check the actual database value.
  PlacesUtils.metadata.cache.clear();
  Assert.deepEqual(
    (
      await PlacesUtils.metadata.get(
        PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
        Object.create(null)
      )
    ).version,
    PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables.version,
    "Check the algorithm version has been stored"
  );

  // Check all alternative frecencies have been calculated, since we just have
  // a few.
  pages = await getAllPages();
  Assert.ok(
    pages.every(p => p.recalc_alt_frecency == 0),
    "All the entries have been recalculated"
  );
  Assert.ok(
    pages.every(p =>
      p.frecency == 0 ? p.alt_frecency == 0 : p.alt_frecency > 0
    ),
    "All the entries have been recalculated"
  );
  Assert.ok(
    PlacesFrecencyRecalculator.isRecalculationPending,
    "Recalculation should be pending"
  );
});

add_task(async function test_different_variables() {
  let pages = await getAllPages();
  Assert.ok(
    pages.every(p => p.recalc_alt_frecency == 0),
    "All the entries should not need recalculation"
  );

  // Set alt_frecency to NULL to verify all the entries are recalculated.
  await PlacesTestUtils.updateDatabaseValues("moz_places", {
    alt_frecency: null,
  });

  // Change variables.
  let variables = await PlacesUtils.metadata.get(
    PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
    Object.create(null)
  );
  Assert.greater(Object.keys(variables).length, 1);
  Assert.ok("version" in variables, "At least the version is always present");
  await PlacesUtils.metadata.set(
    PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
    {
      version:
        PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables
          .version,
      someVar: 1,
    }
  );

  await restartRecalculator();

  // Ensure moz_meta has been updated.
  Assert.ok(
    PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.enabled,
    "Check the pref is enabled"
  );
  // Avoid hitting the cache, we want to check the actual database value.
  PlacesUtils.metadata.cache.clear();
  Assert.deepEqual(
    await PlacesUtils.metadata.get(
      PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
      Object.create(null)
    ),
    variables,
    "Check the algorithm variables have been stored"
  );

  // Check all alternative frecencies have been calculated, since we just have
  // a few.
  pages = await getAllPages();
  Assert.ok(
    pages.every(p => p.recalc_alt_frecency == 0),
    "All the entries have been recalculated"
  );
  Assert.ok(
    pages.every(p =>
      p.frecency == 0 ? p.alt_frecency == 0 : p.alt_frecency > 0
    ),
    "All the entries have been recalculated"
  );
  Assert.ok(
    PlacesFrecencyRecalculator.isRecalculationPending,
    "Recalculation should be pending"
  );
});

add_task(async function test_score() {
  await restartRecalculator();

  // This is not intended to cover the algorithm as a whole, but just as a
  // sanity check for scores.

  // Add before visits to properly set visit source.
  await PlacesUtils.bookmarks.insert({
    url: "https://visitedbookmark.moz.org",
    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
  });

  await PlacesTestUtils.addVisits([
    {
      url: "https://low.moz.org",
      transition: PlacesUtils.history.TRANSITIONS.FRAMED_LINK,
    },
    {
      url: "https://visitedbookmark.moz.org",
      transition: PlacesUtils.history.TRANSITIONS.FRAMED_LINK,
    },
    {
      url: "https://old.moz.org",
      visitDate: (Date.now() - 2 * 86400000) * 1000,
    },
    { url: "https://base.moz.org" },
    { url: "https://manyvisits.moz.org" },
    { url: "https://manyvisits.moz.org" },
    { url: "https://manyvisits.moz.org" },
    { url: "https://manyvisits.moz.org" },
  ]);
  await PlacesUtils.bookmarks.insert({
    url: "https://unvisitedbookmark.moz.org",
    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
  });
  await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
  let getFrecency = url =>
    PlacesTestUtils.getDatabaseValue("moz_places", "alt_frecency", {
      url,
    });
  let low = await getFrecency("https://low.moz.org/");
  let old = await getFrecency("https://old.moz.org/");
  Assert.greater(old, low);
  let base = await getFrecency("https://base.moz.org/");
  Assert.greater(base, old);
  let unvisitedBm = await getFrecency("https://unvisitedbookmark.moz.org/");
  Assert.greater(unvisitedBm, base);
  let manyVisits = await getFrecency("https://manyvisits.moz.org/");
  Assert.greater(manyVisits, unvisitedBm);
  let visitedBm = await getFrecency("https://visitedbookmark.moz.org/");
  Assert.greater(visitedBm, base);
});
