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

// Check that message persistence works - bug 705921 / bug 1307881

"use strict";

const TEST_FILE = "test-console.html";
const TEST_COM_URI = URL_ROOT_COM_SSL + TEST_FILE;
const TEST_ORG_URI = URL_ROOT_ORG_SSL + TEST_FILE;
// TEST_MOCHI_URI uses a non standart port and hence
// is not subject to https-first mode
const TEST_MOCHI_URI = URL_ROOT_MOCHI_8888 + TEST_FILE;

registerCleanupFunction(() => {
  Services.prefs.clearUserPref("devtools.webconsole.persistlog");
});

const INITIAL_LOGS_NUMBER = 5;

const {
  MESSAGE_TYPE,
} = require("resource://devtools/client/webconsole/constants.js");
const {
  WILL_NAVIGATE_TIME_SHIFT,
} = require("resource://devtools/server/actors/webconsole/listeners/document-events.js");

async function logAndAssertInitialMessages(hud) {
  await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [INITIAL_LOGS_NUMBER],
    count => {
      content.wrappedJSObject.doLogs(count);
    }
  );
  await waitFor(() => findAllMessages(hud).length === INITIAL_LOGS_NUMBER);
  ok(true, "Messages showed up initially");
}

add_task(async function () {
  info("Testing that messages disappear on a refresh if logs aren't persisted");
  const hud = await openNewTabAndConsole(TEST_COM_URI);

  await logAndAssertInitialMessages(hud);

  const onReloaded = hud.ui.once("reloaded");
  await reloadBrowser();
  await onReloaded;

  info("Wait for messages to be cleared");
  await waitFor(() => findAllMessages(hud).length === 0);
  ok(true, "Messages disappeared");

  await closeToolbox();
});

add_task(async function () {
  info(
    "Testing that messages disappear on a cross origin navigation if logs aren't persisted"
  );
  const hud = await openNewTabAndConsole(TEST_COM_URI);

  await logAndAssertInitialMessages(hud);

  await navigateTo(TEST_ORG_URI);
  await waitFor(() => findAllMessages(hud).length === 0);
  ok(true, "Messages disappeared");

  await closeToolbox();
});

add_task(async function () {
  info("Testing that messages disappear on bfcache navigations");
  const firstLocation =
    "data:text/html,<!DOCTYPE html><script>console.log('first document load');window.onpageshow=()=>console.log('first document show');</script>";
  const secondLocation =
    "data:text/html,<!DOCTYPE html><script>console.log('second document load');window.onpageshow=()=>console.log('second document show');</script>";
  const hud = await openNewTabAndConsole(firstLocation);

  info("Wait for first page messages");
  // Look into .message-body as the default selector also include the frame,
  // which is the document url, which also include the logged string...
  await waitFor(
    () =>
      findMessagePartsByType(hud, {
        text: "first document load",
        typeSelector: ".console-api",
        partSelector: ".message-body",
      }).length === 1 &&
      findMessagePartsByType(hud, {
        text: "first document show",
        typeSelector: ".console-api",
        partSelector: ".message-body",
      }).length === 1
  );
  const firstPageInnerWindowId =
    gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.innerWindowId;

  await navigateTo(secondLocation);

  const secondPageInnerWindowId =
    gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.innerWindowId;
  isnot(
    firstPageInnerWindowId,
    secondPageInnerWindowId,
    "The second page is having a distinct inner window id"
  );
  await waitFor(
    () =>
      findMessagePartsByType(hud, {
        text: "second",
        typeSelector: ".console-api",
        partSelector: ".message-body",
      }).length === 2
  );
  ok("Second page message appeared");
  is(
    findMessagePartsByType(hud, {
      text: "first",
      typeSelector: ".console-api",
      partSelector: ".message-body",
    }).length,
    0,
    "First page message disappeared"
  );

  info("Go back to the first page");
  gBrowser.selectedBrowser.goBack();

  // When going back, the page isn't reloaded, so in theory we should only get
  // the pageshow event.
  await waitFor(
    () =>
      findMessagePartsByType(hud, {
        text: "first document show",
        typeSelector: ".console-api",
        partSelector: ".message-body",
      }).length === 1
  );
  ok("First page message re-appeared");

  // However `clearMessagesCache` can fail on navigation, and in this case the
  // cache will still contain the two initial logs:
  // - "first document load"
  // - "first document show"
  let isLoadMessageDisplayed = findMessagePartsByType(hud, {
    text: "first document load",
    typeSelector: ".console-api",
    partSelector: ".message-body",
  }).length;

  // With the additional "first document show" from the new bfcache navigation
  // this means we should wait for "first document show" to have a repeated
  // count of 2.
  // Otherwise the second "first document show" resource might be throttled and
  // handled later in the test.
  if (isLoadMessageDisplayed) {
    await waitForRepeatedMessageByType(
      hud,
      "first document show",
      ".console-api",
      2
    );
  }

  is(
    gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.innerWindowId,
    firstPageInnerWindowId,
    "The first page is really a bfcache navigation, keeping the same WindowGlobal"
  );
  is(
    findMessagePartsByType(hud, {
      text: "second",
      typeSelector: ".console-api",
      partSelector: ".message-body",
    }).length,
    0,
    "Second page message disappeared"
  );

  info("Go forward to the original second page");
  gBrowser.selectedBrowser.goForward();
  await waitFor(
    () =>
      findMessagePartsByType(hud, {
        text: "second document show",
        typeSelector: ".console-api",
        partSelector: ".message-body",
      }).length === 1
  );
  ok("Second page message appeared");

  isLoadMessageDisplayed = findMessagePartsByType(hud, {
    text: "second document load",
    typeSelector: ".console-api",
    partSelector: ".message-body",
  }).length;
  if (isLoadMessageDisplayed) {
    await waitForRepeatedMessageByType(
      hud,
      "second document show",
      ".console-api",
      2
    );
  }
  is(
    gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.innerWindowId,
    secondPageInnerWindowId,
    "The second page is really a bfcache navigation, keeping the same WindowGlobal"
  );
  is(
    findMessagePartsByType(hud, {
      text: "first",
      typeSelector: ".console-api",
      partSelector: ".message-body",
    }).length,
    0,
    "First page message disappeared"
  );

  await closeToolbox();
});

add_task(async function () {
  info("Testing that messages persist on a refresh if logs are persisted");

  const hud = await openNewTabAndConsole(TEST_COM_URI);

  await toggleConsoleSetting(
    hud,
    ".webconsole-console-settings-menu-item-persistentLogs"
  );

  await logAndAssertInitialMessages(hud);

  const onNavigatedMessage = waitForMessageByType(
    hud,
    "Navigated to " + TEST_COM_URI,
    ".navigationMarker"
  );
  const onReloaded = hud.ui.once("reloaded");
  // Because will-navigate DOCUMENT_EVENT timestamp is shifted to workaround some other limitation,
  // the reported time of navigation may actually be slightly off and be older than the real navigation start
  let timeBeforeNavigation = Date.now() - WILL_NAVIGATE_TIME_SHIFT;
  reloadBrowser();
  await onNavigatedMessage;
  await onReloaded;

  ok(true, "Navigation message appeared as expected");
  is(
    findAllMessages(hud).length,
    INITIAL_LOGS_NUMBER + 1,
    "Messages logged before navigation are still visible"
  );

  assertLastMessageIsNavigationMessage(hud, timeBeforeNavigation, TEST_COM_URI);

  info(
    "Testing that messages also persist when doing a cross origin navigation if logs are persisted"
  );
  const onNavigatedMessage2 = waitForMessageByType(
    hud,
    "Navigated to " + TEST_ORG_URI,
    ".navigationMarker"
  );
  timeBeforeNavigation = Date.now() - WILL_NAVIGATE_TIME_SHIFT;
  await navigateTo(TEST_ORG_URI);
  await onNavigatedMessage2;

  ok(true, "Second navigation message appeared as expected");
  is(
    findAllMessages(hud).length,
    INITIAL_LOGS_NUMBER + 2,
    "Messages logged before the second navigation are still visible"
  );

  assertLastMessageIsNavigationMessage(hud, timeBeforeNavigation, TEST_ORG_URI);

  info(
    "Test doing a second cross origin navigation in order to triger a target switching with a target following the window global lifecycle"
  );
  const onNavigatedMessage3 = waitForMessageByType(
    hud,
    "Navigated to " + TEST_MOCHI_URI,
    ".navigationMarker"
  );
  timeBeforeNavigation = Date.now() - WILL_NAVIGATE_TIME_SHIFT;
  await navigateTo(TEST_MOCHI_URI);
  await onNavigatedMessage3;

  ok(true, "Third navigation message appeared as expected");
  is(
    findAllMessages(hud).length,
    INITIAL_LOGS_NUMBER + 3,
    "Messages logged before the third navigation are still visible"
  );

  assertLastMessageIsNavigationMessage(
    hud,
    timeBeforeNavigation,
    TEST_MOCHI_URI
  );

  await closeToolbox();
});

add_task(async function consoleClearPersist() {
  info("Testing that messages persist on console.clear if logs are persisted");

  await pushPref("devtools.webconsole.persistlog", true);
  const hud = await openNewTabAndConsole(TEST_COM_URI);

  await logAndAssertInitialMessages(hud);

  info("Send a console.clear() and another log from the content page");
  const onConsoleClearPrevented = waitForMessageByType(
    hud,
    "console.clear() was prevented",
    ".console-api"
  );
  SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    content.wrappedJSObject.console.clear();
    content.wrappedJSObject.console.log("after clear");
  });

  await waitForMessageByType(hud, "after clear", ".log");
  await onConsoleClearPrevented;
  ok(true, "console.clear was handled by the client");

  Assert.strictEqual(
    findAllMessages(hud).length,
    INITIAL_LOGS_NUMBER + 2,
    "All initial messages are still displayed, with the 2 new ones"
  );

  await closeToolbox();
});

function assertLastMessageIsNavigationMessage(hud, timeBeforeNavigation, url) {
  const { visibleMessages, mutableMessagesById } = hud.ui.wrapper
    .getStore()
    .getState().messages;
  const lastMessageId = visibleMessages.at(-1);
  const lastMessage = mutableMessagesById.get(lastMessageId);

  is(
    lastMessage.type,
    MESSAGE_TYPE.NAVIGATION_MARKER,
    "The last message is a navigation marker"
  );
  is(
    lastMessage.messageText,
    "Navigated to " + url,
    "The navigation message is correct"
  );
  // It is surprising, but the navigation may be timestamped at the same exact time
  // as timeBeforeNavigation time record.
  Assert.greaterOrEqual(
    lastMessage.timeStamp,
    timeBeforeNavigation,
    "The navigation message has a timestamp newer (or equal) than the time before the navigation..."
  );
  Assert.less(
    lastMessage.timeStamp,
    Date.now(),
    "...and older than current time"
  );
}
