/* 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(
  "moz-src:///browser/components/places/Interactions.sys.mjs"
);

ChromeUtils.defineESModuleGetters(this, {
  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
  sinon: "resource://testing-common/Sinon.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.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);
    }
  );
}

async function insertIntoMozPlacesMetadata(
  place_id,
  {
    referrer_place_id = null,
    created_at = Date.now(),
    updated_at = Date.now(),
    total_view_time = 0,
    typing_time = 0,
    key_presses = 0,
    scrolling_time = 0,
    scrolling_distance = 0,
    document_type = 0,
    search_query_id = null,
  }
) {
  info("Inserting interaction into moz_places_metadata.");
  await PlacesUtils.withConnectionWrapper(
    "interactions::insertIntoMozPlacesMetadata",
    async db => {
      await db.execute(
        `
        INSERT INTO moz_places_metadata (
          place_id,
          referrer_place_id,
          created_at,
          updated_at,
          total_view_time,
          typing_time,
          key_presses,
          scrolling_time,
          scrolling_distance,
          document_type,
          search_query_id
        ) VALUES (
         :place_id,
         :referrer_place_id,
         :created_at,
         :updated_at,
         :total_view_time,
         :typing_time,
         :key_presses,
         :scrolling_time,
         :scrolling_distance,
         :document_type,
         :search_query_id
        )
      `,
        {
          place_id,
          referrer_place_id,
          created_at,
          updated_at,
          total_view_time,
          typing_time,
          key_presses,
          scrolling_time,
          scrolling_distance,
          document_type,
          search_query_id,
        }
      );
    }
  );
}
