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

ChromeUtils.defineESModuleGetters(globalThis, {
  SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
});
const { ProfileAge } = ChromeUtils.importESModule(
  "resource://gre/modules/ProfileAge.sys.mjs"
);

const HAS_IMPORTED_HISTORY_PREF = "browser.migrate.interactions.history";
const IMPORT_HISTORY_DISMISSED_PREF =
  "browser.tabs.firefox-view.importHistory.dismissed";
const HISTORY_EVENT = [["firefoxview_next", "history", "visits", undefined]];
const SHOW_ALL_HISTORY_EVENT = [
  ["firefoxview_next", "show_all_history", "tabs", undefined],
];
const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";

const DAY_MS = 24 * 60 * 60 * 1000;
const today = new Date();
const yesterday = new Date(Date.now() - DAY_MS);
const twoDaysAgo = new Date(Date.now() - DAY_MS * 2);
const threeDaysAgo = new Date(Date.now() - DAY_MS * 3);
const fourDaysAgo = new Date(Date.now() - DAY_MS * 4);
const oneMonthAgo = new Date(today);
const dates = [
  today,
  yesterday,
  twoDaysAgo,
  threeDaysAgo,
  fourDaysAgo,
  oneMonthAgo,
];

// Set the date for the first day of the last month
oneMonthAgo.setDate(1);
if (oneMonthAgo.getMonth() === 0) {
  // If today's date is in January, use first day in December from the previous year
  oneMonthAgo.setMonth(11);
  oneMonthAgo.setFullYear(oneMonthAgo.getFullYear() - 1);
} else {
  oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
}

function isElInViewport(element) {
  const boundingRect = element.getBoundingClientRect();
  return (
    boundingRect.top >= 0 &&
    boundingRect.left >= 0 &&
    boundingRect.bottom <=
      (window.innerHeight || document.documentElement.clientHeight) &&
    boundingRect.right <=
      (window.innerWidth || document.documentElement.clientWidth)
  );
}

async function historyComponentReady(historyComponent, expectedHistoryItems) {
  await TestUtils.waitForCondition(
    () => historyComponent.controller.totalVisitsCount === expectedHistoryItems,
    "History component ready"
  );

  let expected = historyComponent.controller.historyVisits.length;
  let actual = historyComponent.cards.length;

  is(expected, actual, `Total number of cards should be ${expected}`);
}

async function historyTelemetry() {
  await TestUtils.waitForCondition(
    () => {
      let events = Services.telemetry.snapshotEvents(
        Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
        false
      ).parent;
      return events && events.length >= 1;
    },
    "Waiting for history firefoxview telemetry event.",
    200,
    100
  );

  TelemetryTestUtils.assertEvents(
    HISTORY_EVENT,
    { category: "firefoxview_next" },
    { clear: true, process: "parent" }
  );
}

async function sortHistoryTelemetry(sortHistoryEvent) {
  await TestUtils.waitForCondition(
    () => {
      let events = Services.telemetry.snapshotEvents(
        Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
        false
      ).parent;
      return events && events.length >= 1;
    },
    "Waiting for sort_history firefoxview telemetry event.",
    200,
    100
  );

  TelemetryTestUtils.assertEvents(
    sortHistoryEvent,
    { category: "firefoxview_next" },
    { clear: true, process: "parent" }
  );
}

async function showAllHistoryTelemetry() {
  await TestUtils.waitForCondition(
    () => {
      let events = Services.telemetry.snapshotEvents(
        Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
        false
      ).parent;
      return events && events.length >= 1;
    },
    "Waiting for show_all_history firefoxview telemetry event.",
    200,
    100
  );

  TelemetryTestUtils.assertEvents(
    SHOW_ALL_HISTORY_EVENT,
    { category: "firefoxview_next" },
    { clear: true, process: "parent" }
  );
}

async function addHistoryItems(dateAdded) {
  await PlacesUtils.history.insert({
    url: URLs[0],
    title: "Example Domain 1",
    visits: [{ date: dateAdded }],
  });
  await PlacesUtils.history.insert({
    url: URLs[1],
    title: "Example Domain 2",
    visits: [{ date: dateAdded }],
  });
  await PlacesUtils.history.insert({
    url: URLs[2],
    title: "Example Domain 3",
    visits: [{ date: dateAdded }],
  });
  await PlacesUtils.history.insert({
    url: URLs[3],
    title: "Example Domain 4",
    visits: [{ date: dateAdded }],
  });
}

function createHistoryEntries() {
  let historyEntries = [];
  for (let i = 0; i < 4; i++) {
    historyEntries.push({
      url: URLs[i],
      title: `Example Domain ${i}`,
      visits: dates.map(date => [{ date }]),
    });
  }
  return historyEntries;
}

add_setup(async () => {
  registerCleanupFunction(async () => {
    await SpecialPowers.popPrefEnv();
    await PlacesUtils.history.clear();
  });
});

add_task(async function test_list_ordering() {
  await PlacesUtils.history.clear();
  const historyEntries = createHistoryEntries();
  await PlacesUtils.history.insertMany(historyEntries);
  await withFirefoxView({}, async browser => {
    const { document } = browser.contentWindow;

    await navigateToViewAndWait(document, "history");

    let historyComponent = await TestUtils.waitForCondition(
      () => document.querySelector("view-history"),
      "History component rendered"
    );
    historyComponent.profileAge = 8;

    await historyComponentReady(historyComponent, historyEntries.length);

    let firstCard = historyComponent.cards[0];

    info("The first card should have a header for 'Today'.");
    await BrowserTestUtils.waitForMutationCondition(
      firstCard.querySelector("[slot=header]"),
      { attributes: true },
      () =>
        document.l10n.getAttributes(firstCard.querySelector("[slot=header]"))
          .id === "firefoxview-history-date-today"
    );

    // Select first history item in first card
    await clearAllParentTelemetryEvents();
    await TestUtils.waitForCondition(() => {
      return historyComponent.lists[0].rowEls.length;
    });
    let firstHistoryLink = historyComponent.lists[0].rowEls[0].mainEl;
    let promiseHidden = BrowserTestUtils.waitForEvent(
      document,
      "visibilitychange"
    );
    await EventUtils.synthesizeMouseAtCenter(firstHistoryLink, {}, content);
    await historyTelemetry();
    await promiseHidden;
    await openFirefoxViewTab(browser.ownerGlobal);

    // Test number of cards when sorted by site/domain
    await clearAllParentTelemetryEvents();
    let sortHistoryEvent = [
      [
        "firefoxview_next",
        "sort_history",
        "tabs",
        undefined,
        { sort_type: "site", search_start: "false" },
      ],
    ];
    // Select sort by site option
    await EventUtils.synthesizeMouseAtCenter(
      historyComponent.sortInputs[1],
      {},
      content
    );
    await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
    await sortHistoryTelemetry(sortHistoryEvent);

    let expectedNumOfCards = historyComponent.controller.historyVisits.length;

    info(`Total number of cards should be ${expectedNumOfCards}`);
    await BrowserTestUtils.waitForMutationCondition(
      historyComponent.shadowRoot,
      { childList: true, subtree: true },
      () => expectedNumOfCards === historyComponent.cards.length
    );

    await clearAllParentTelemetryEvents();
    sortHistoryEvent = [
      [
        "firefoxview_next",
        "sort_history",
        "tabs",
        undefined,
        { sort_type: "date", search_start: "false" },
      ],
    ];
    // Select sort by date option
    await EventUtils.synthesizeMouseAtCenter(
      historyComponent.sortInputs[0],
      {},
      content
    );
    await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
    await sortHistoryTelemetry(sortHistoryEvent);

    // clean up extra tabs
    while (gBrowser.tabs.length > 1) {
      BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
    }
  });
});

add_task(async function test_empty_states() {
  await PlacesUtils.history.clear();
  await withFirefoxView({}, async browser => {
    const { document } = browser.contentWindow;

    await navigateToViewAndWait(document, "history");

    let historyComponent = document.querySelector("view-history");
    historyComponent.profileAge = 8;
    await TestUtils.waitForCondition(() => historyComponent.emptyState);
    let emptyStateCard = historyComponent.emptyState;
    ok(
      emptyStateCard.headerEl.textContent.includes(
        "Get back to where you’ve been"
      ),
      "Initial empty state header has the expected text."
    );
    ok(
      emptyStateCard.descriptionEls[0].textContent.includes(
        "As you browse, the pages you visit will be listed here."
      ),
      "Initial empty state description has the expected text."
    );

    // Test empty state when History mode is set to never remember
    Services.prefs.setBoolPref(NEVER_REMEMBER_HISTORY_PREF, true);
    // Manually update the history component from the test, since changing this setting
    // in about:preferences will require a browser reload
    historyComponent.requestUpdate();
    await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
    emptyStateCard = historyComponent.emptyState;
    ok(
      emptyStateCard.headerEl.textContent.includes("Nothing to show"),
      "Empty state with never remember history header has the expected text."
    );
    ok(
      emptyStateCard.descriptionEls[1].textContent.includes(
        "remember your activity as you browse. To change that"
      ),
      "Empty state with never remember history description has the expected text."
    );
    // Reset History mode to Remember
    Services.prefs.setBoolPref(NEVER_REMEMBER_HISTORY_PREF, false);
    // Manually update the history component from the test, since changing this setting
    // in about:preferences will require a browser reload
    historyComponent.requestUpdate();
    await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);

    // Test import history banner shows if profile age is 7 days or less and
    // user hasn't already imported history from another browser
    Services.prefs.setBoolPref(IMPORT_HISTORY_DISMISSED_PREF, false);
    Services.prefs.setBoolPref(HAS_IMPORTED_HISTORY_PREF, true);
    ok(!historyComponent.cards.length, "Import history banner not shown yet");
    historyComponent.profileAge = 0;
    await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
    ok(
      !historyComponent.cards.length,
      "Import history banner still not shown yet"
    );
    Services.prefs.setBoolPref(HAS_IMPORTED_HISTORY_PREF, false);
    await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
    ok(
      historyComponent.cards[0].textContent.includes(
        "Import history from another browser"
      ),
      "Import history banner is shown"
    );
    let importHistoryCloseButton =
      historyComponent.cards[0].querySelector("moz-button.close");
    importHistoryCloseButton.click();
    await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
    ok(
      Services.prefs.getBoolPref(IMPORT_HISTORY_DISMISSED_PREF, true) &&
        !historyComponent.cards.length,
      "Import history banner has been dismissed."
    );
    // Reset profileAge to greater than 7 to avoid affecting other tests
    historyComponent.profileAge = 8;
    Services.prefs.setBoolPref(IMPORT_HISTORY_DISMISSED_PREF, false);

    gBrowser.removeTab(gBrowser.selectedTab);
  });
});

add_task(async function test_observers_removed_when_view_is_hidden() {
  await PlacesUtils.history.clear();
  const NEW_TAB_URL = "https://example.com";
  const tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    NEW_TAB_URL
  );
  await withFirefoxView({}, async browser => {
    const { document } = browser.contentWindow;
    await navigateToViewAndWait(document, "history");
    const historyComponent = document.querySelector("view-history");
    historyComponent.profileAge = 8;
    let visitList = await TestUtils.waitForCondition(() =>
      historyComponent.cards?.[0]?.querySelector("fxview-tab-list")
    );
    info("The list should show a visit from the new tab.");
    await TestUtils.waitForCondition(() => visitList.rowEls.length === 1);

    let promiseHidden = BrowserTestUtils.waitForEvent(
      document,
      "visibilitychange"
    );
    await BrowserTestUtils.switchTab(gBrowser, tab);
    await promiseHidden;
    const { date } = await PlacesUtils.history
      .fetch(NEW_TAB_URL, {
        includeVisits: true,
      })
      .then(({ visits }) => visits[0]);
    await addHistoryItems(date);
    is(
      visitList.rowEls.length,
      1,
      "The list does not update when Firefox View is hidden."
    );

    info("The list should update when Firefox View is visible.");
    await openFirefoxViewTab(browser.ownerGlobal);
    visitList = await TestUtils.waitForCondition(() =>
      historyComponent.cards?.[0]?.querySelector("fxview-tab-list")
    );
    await TestUtils.waitForCondition(() => visitList.rowEls.length > 1);

    BrowserTestUtils.removeTab(tab);
  });
});

add_task(async function test_show_all_history_telemetry() {
  await PlacesUtils.history.clear();
  const historyEntries = createHistoryEntries();
  await PlacesUtils.history.insertMany(historyEntries);
  await withFirefoxView({}, async browser => {
    const { document } = browser.contentWindow;

    await navigateToViewAndWait(document, "history");

    let historyComponent = document.querySelector("view-history");
    historyComponent.profileAge = 8;
    await historyComponentReady(historyComponent, historyEntries.length);

    await clearAllParentTelemetryEvents();
    let showAllHistoryBtn = historyComponent.showAllHistoryBtn;
    showAllHistoryBtn.scrollIntoView();
    await EventUtils.synthesizeMouseAtCenter(showAllHistoryBtn, {}, content);
    await showAllHistoryTelemetry();

    // Make sure library window is shown
    await TestUtils.waitForCondition(() =>
      Services.wm.getMostRecentWindow("Places:Organizer")
    );
    let library = Services.wm.getMostRecentWindow("Places:Organizer");
    await BrowserTestUtils.closeWindow(library);
    gBrowser.removeTab(gBrowser.selectedTab);
  });
});

add_task(async function test_search_history() {
  await PlacesUtils.history.clear();
  const historyEntries = createHistoryEntries();
  await PlacesUtils.history.insertMany(historyEntries);
  await withFirefoxView({}, async browser => {
    const { document } = browser.contentWindow;
    await navigateToViewAndWait(document, "history");
    const historyComponent = document.querySelector("view-history");
    historyComponent.profileAge = 8;
    await historyComponentReady(historyComponent, historyEntries.length);
    const searchTextbox = await TestUtils.waitForCondition(
      () => historyComponent.searchTextbox,
      "The search textbox is displayed."
    );

    info("Input a search query.");
    EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, content);
    EventUtils.sendString("Example Domain 1", content);
    await BrowserTestUtils.waitForMutationCondition(
      historyComponent.shadowRoot,
      { childList: true, subtree: true },
      () =>
        historyComponent.cards.length === 1 &&
        document.l10n.getAttributes(
          historyComponent.cards[0].querySelector("[slot=header]")
        ).id === "firefoxview-search-results-header"
    );
    await TestUtils.waitForCondition(() => {
      const { rowEls } = historyComponent.lists[0];
      return rowEls.length === 1 && rowEls[0].mainEl.href === URLs[1];
    }, "There is one matching search result.");

    info("Input a bogus search query.");
    EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, content);
    EventUtils.sendString("Bogus Query", content);
    await TestUtils.waitForCondition(() => {
      const tabList = historyComponent.lists[0];
      return tabList?.emptyState;
    }, "There are no matching search results.");

    info("Clear the search query.");
    EventUtils.synthesizeMouseAtCenter(searchTextbox.clearButton, {}, content);
    await BrowserTestUtils.waitForMutationCondition(
      historyComponent.shadowRoot,
      { childList: true, subtree: true },
      () =>
        historyComponent.cards.length ===
        historyComponent.controller.historyVisits.length
    );
    searchTextbox.blur();

    info("Input a bogus search query with keyboard.");
    EventUtils.synthesizeKey("f", { accelKey: true }, content);
    EventUtils.sendString("Bogus Query", content);
    await TestUtils.waitForCondition(() => {
      const tabList = historyComponent.lists[0];
      return tabList?.emptyState;
    }, "There are no matching search results.");

    info("Clear the search query with keyboard.");
    is(
      historyComponent.shadowRoot.activeElement,
      searchTextbox,
      "Search input is focused"
    );
    EventUtils.synthesizeKey("KEY_Tab", {}, content);
    ok(
      searchTextbox.clearButton.matches(":focus-visible"),
      "Clear Search button is focused"
    );
    EventUtils.synthesizeKey("KEY_Enter", {}, content);
    await BrowserTestUtils.waitForMutationCondition(
      historyComponent.shadowRoot,
      { childList: true, subtree: true },
      () =>
        historyComponent.cards.length ===
        historyComponent.controller.historyVisits.length
    );
  });
});

add_task(async function test_search_ignores_stale_queries() {
  await PlacesUtils.history.clear();
  const historyEntries = createHistoryEntries();
  await PlacesUtils.history.insertMany(historyEntries);

  let bogusQueryInProgress = false;
  const searchDeferred = Promise.withResolvers();
  const realDatabase = await PlacesUtils.promiseLargeCacheDBConnection();
  const mockDatabase = {
    executeCached: async (sql, options) => {
      if (options.query === "Bogus Query") {
        bogusQueryInProgress = true;
        await searchDeferred.promise;
      }
      return realDatabase.executeCached(sql, options);
    },
    interrupt: () => searchDeferred.reject(),
  };
  const stub = sinon
    .stub(PlacesUtils, "promiseLargeCacheDBConnection")
    .resolves(mockDatabase);

  await withFirefoxView({}, async browser => {
    const { document } = browser.contentWindow;
    await navigateToViewAndWait(document, "history");
    const historyComponent = document.querySelector("view-history");
    historyComponent.profileAge = 8;
    await historyComponentReady(historyComponent, historyEntries.length);
    const searchTextbox = await TestUtils.waitForCondition(
      () => historyComponent.searchTextbox,
      "The search textbox is displayed."
    );

    info("Input a bogus search query.");
    EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, content);
    EventUtils.sendString("Bogus Query", content);
    await TestUtils.waitForCondition(() => bogusQueryInProgress);

    info("Clear the bogus query.");
    EventUtils.synthesizeMouseAtCenter(searchTextbox.clearButton, {}, content);
    await searchTextbox.updateComplete;

    info("Input a real search query.");
    EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, content);
    EventUtils.sendString("Example Domain 1", content);
    await TestUtils.waitForCondition(() => {
      const { rowEls } = historyComponent.lists[0];
      return rowEls.length === 1 && rowEls[0].mainEl.href === URLs[1];
    }, "There is one matching search result.");
    searchDeferred.resolve();
    await TestUtils.waitForTick();
    const tabList = historyComponent.lists[0];
    ok(!tabList.emptyState, "Empty state should not be shown.");
  });

  stub.restore();
});

add_task(async function test_persist_collapse_card_after_view_change() {
  await PlacesUtils.history.clear();
  await addHistoryItems(today);
  await withFirefoxView({}, async browser => {
    const { document } = browser.contentWindow;
    await navigateToViewAndWait(document, "history");
    const historyComponent = document.querySelector("view-history");
    historyComponent.profileAge = 8;
    await TestUtils.waitForCondition(
      () => historyComponent.controller.totalVisitsCount === 4
    );
    let firstHistoryCard = historyComponent.cards[0];
    ok(
      firstHistoryCard.isExpanded,
      "The first history card is expanded initially."
    );

    // Collapse history card
    EventUtils.synthesizeMouseAtCenter(firstHistoryCard.summaryEl, {}, content);
    is(
      firstHistoryCard.detailsEl.hasAttribute("open"),
      false,
      "The first history card is now collapsed."
    );

    // Switch to a new view and then back to History
    await navigateToViewAndWait(document, "syncedtabs");
    await navigateToViewAndWait(document, "history");

    // Check that first history card is still collapsed after changing view
    ok(
      !firstHistoryCard.isExpanded,
      "The first history card is still collapsed after changing view."
    );

    await PlacesUtils.history.clear();
    gBrowser.removeTab(gBrowser.selectedTab);
  });
});
