/* 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/. */

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

ChromeUtils.defineESModuleGetters(this, {
  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
  sinon: "resource://testing-common/Sinon.sys.mjs",
});

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "pageViewIdleTime",
  "browser.places.interactions.pageViewIdleTime",
  60
);

add_setup(async function global_setup() {
  // Disable idle management because it interacts with our code, causing
  // unexpected intermittent failures, we'll fake idle notifications when
  // we need to test it.
  let idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService(
    Ci.nsIUserIdleService
  );
  idleService.removeIdleObserver(Interactions, pageViewIdleTime);
  registerCleanupFunction(() => {
    idleService.addIdleObserver(Interactions, pageViewIdleTime);
  });

  // Clean interactions for each test.
  await Interactions.reset();
  registerCleanupFunction(async () => {
    await Interactions.reset();
  });
});

/**
 * Ensures that a list of interactions have been permanently stored.
 *
 * @param {Array} expected list of interactions to be found.
 * @param {boolean} [dontFlush] Avoid flushing pending data.
 */
async function assertDatabaseValues(expected, { dontFlush = false } = {}) {
  await Interactions.interactionUpdatePromise;
  if (!dontFlush) {
    await Interactions.store.flush();
  }

  let interactions = await PlacesUtils.withConnectionWrapper(
    "head.js::assertDatabaseValues",
    async db => {
      let rows = await db.execute(`
        SELECT h.url AS url, h2.url as referrer_url, total_view_time, key_presses, typing_time, scrolling_time, scrolling_distance
        FROM moz_places_metadata m
        JOIN moz_places h ON h.id = m.place_id
        LEFT JOIN moz_places h2 ON h2.id = m.referrer_place_id
        ORDER BY created_at ASC
      `);
      return rows.map(r => ({
        url: r.getResultByName("url"),
        referrerUrl: r.getResultByName("referrer_url"),
        keypresses: r.getResultByName("key_presses"),
        typingTime: r.getResultByName("typing_time"),
        totalViewTime: r.getResultByName("total_view_time"),
        scrollingTime: r.getResultByName("scrolling_time"),
        scrollingDistance: r.getResultByName("scrolling_distance"),
      }));
    }
  );
  info(
    `Found ${interactions.length} interactions:\n ${JSON.stringify(
      interactions
    )}`
  );
  Assert.equal(
    interactions.length,
    expected.length,
    "Found the expected number of entries"
  );
  for (let i = 0; i < Math.min(expected.length, interactions.length); i++) {
    let actual = interactions[i];
    Assert.equal(
      actual.url,
      expected[i].url,
      "Should have saved the page into the database"
    );

    if (expected[i].exactTotalViewTime != undefined) {
      Assert.equal(
        actual.totalViewTime,
        expected[i].exactTotalViewTime,
        "Should have kept the exact time."
      );
    } else if (expected[i].totalViewTime != undefined) {
      Assert.greaterOrEqual(
        actual.totalViewTime,
        expected[i].totalViewTime,
        "Should have stored the interaction time"
      );
    }

    if (expected[i].maxViewTime != undefined) {
      Assert.less(
        actual.totalViewTime,
        expected[i].maxViewTime,
        "Should have recorded an interaction below the maximum expected"
      );
    }

    if (expected[i].keypresses != undefined) {
      Assert.equal(
        actual.keypresses,
        expected[i].keypresses,
        "Should have saved the keypresses into the database"
      );
    }

    if (expected[i].exactTypingTime != undefined) {
      Assert.equal(
        actual.typingTime,
        expected[i].exactTypingTime,
        "Should have stored the exact typing time."
      );
    } else if (expected[i].typingTimeIsGreaterThan != undefined) {
      Assert.greater(
        actual.typingTime,
        expected[i].typingTimeIsGreaterThan,
        "Should have stored at least this amount of typing time."
      );
    } else if (expected[i].typingTimeIsLessThan != undefined) {
      Assert.less(
        actual.typingTime,
        expected[i].typingTimeIsLessThan,
        "Should have stored less than this amount of typing time."
      );
    }

    if (expected[i].exactScrollingDistance != undefined) {
      Assert.equal(
        actual.scrollingDistance,
        expected[i].exactScrollingDistance,
        "Should have scrolled by exactly least this distance"
      );
    } else if (expected[i].exactScrollingTime != undefined) {
      Assert.greater(
        actual.scrollingTime,
        expected[i].exactScrollingTime,
        "Should have scrolled for exactly least this duration"
      );
    }

    if (expected[i].scrollingDistanceIsGreaterThan != undefined) {
      Assert.greater(
        actual.scrollingDistance,
        expected[i].scrollingDistanceIsGreaterThan,
        "Should have scrolled by at least this distance"
      );
    } else if (expected[i].scrollingTimeIsGreaterThan != undefined) {
      Assert.greater(
        actual.scrollingTime,
        expected[i].scrollingTimeIsGreaterThan,
        "Should have scrolled for at least this duration"
      );
    }
  }
}

/**
 * Ensures that a list of interactions have been permanently stored.
 *
 * @param {string} url The url to query.
 * @param {string} property The property to extract.
 * @returns {*}
 *   Returns the selected database value.
 */
async function getDatabaseValue(url, property) {
  await Interactions.store.flush();
  const PROP_TRANSLATOR = {
    totalViewTime: "total_view_time",
    keypresses: "key_presses",
    typingTime: "typing_time",
  };
  property = PROP_TRANSLATOR[property] || property;

  return PlacesUtils.withConnectionWrapper(
    "head.js::getDatabaseValue",
    async db => {
      let rows = await db.execute(
        `
        SELECT * FROM moz_places_metadata m
        JOIN moz_places h ON h.id = m.place_id
        WHERE url = :url
        ORDER BY created_at DESC
      `,
        { url }
      );
      return rows?.[0].getResultByName(property);
    }
  );
}
