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

"use strict";

// Test the cache mechanism of the ResourceCommand.

const TEST_URI = "data:text/html;charset=utf-8,<!DOCTYPE html>Cache Test";

add_task(async function () {
  info("Test whether multiple listener can get same cached resources");

  const tab = await addTab(TEST_URI);

  const { client, resourceCommand, targetCommand } =
    await initResourceCommand(tab);

  info("Add messages as existing resources");
  const messages = ["a", "b", "c"];
  await logMessages(tab.linkedBrowser, messages);

  info("Register first listener");
  const cachedResources1 = [];
  await resourceCommand.watchResources(
    [resourceCommand.TYPES.CONSOLE_MESSAGE],
    {
      onAvailable(resources, { areExistingResources }) {
        ok(areExistingResources, "All resources are already existing ones");
        cachedResources1.push(...resources);
      },
    }
  );

  info("Register second listener");
  const cachedResources2 = [];
  await resourceCommand.watchResources(
    [resourceCommand.TYPES.CONSOLE_MESSAGE],
    {
      onAvailable(resources, { areExistingResources }) {
        ok(areExistingResources, "All resources are already existing ones");
        cachedResources2.push(...resources);
      },
    }
  );

  assertContents(cachedResources1, messages);
  assertResources(cachedResources2, cachedResources1);

  targetCommand.destroy();
  await client.close();
});

add_task(async function () {
  info(
    "Test whether the cache is reflecting existing resources and additional resources"
  );

  const tab = await addTab(TEST_URI);

  const { client, resourceCommand, targetCommand } =
    await initResourceCommand(tab);

  info("Add messages as existing resources");
  const existingMessages = ["a", "b", "c"];
  await logMessages(tab.linkedBrowser, existingMessages);

  info("Register first listener to get all available resources");
  const availableResources = [];
  // We first get notified about existing resources
  let shouldBeExistingResources = true;
  await resourceCommand.watchResources(
    [resourceCommand.TYPES.CONSOLE_MESSAGE],
    {
      onAvailable(resources, { areExistingResources }) {
        is(
          areExistingResources,
          shouldBeExistingResources,
          "areExistingResources flag is correct"
        );
        availableResources.push(...resources);
      },
    }
  );
  // Then, we are notified about, new, live ones
  shouldBeExistingResources = false;

  info("Add messages as additional resources");
  const additionalMessages = ["d", "e"];
  await logMessages(tab.linkedBrowser, additionalMessages);

  info("Wait until onAvailable is called expected times");
  const allMessages = [...existingMessages, ...additionalMessages];
  await waitUntil(() => availableResources.length === allMessages.length);

  info("Register second listener to get the cached resources");
  const cachedResources = [];
  await resourceCommand.watchResources(
    [resourceCommand.TYPES.CONSOLE_MESSAGE],
    {
      onAvailable(resources, { areExistingResources }) {
        ok(areExistingResources, "All resources are already existing ones");
        cachedResources.push(...resources);
      },
    }
  );

  assertContents(availableResources, allMessages);
  assertResources(cachedResources, availableResources);

  targetCommand.destroy();
  await client.close();
});

add_task(async function () {
  info("Test whether the cache is cleared when navigation");

  const tab = await addTab(TEST_URI);

  const { client, resourceCommand, targetCommand } =
    await initResourceCommand(tab);

  info("Add messages as existing resources");
  const existingMessages = ["a", "b", "c"];
  await logMessages(tab.linkedBrowser, existingMessages);

  info("Register first listener");
  await resourceCommand.watchResources(
    [resourceCommand.TYPES.CONSOLE_MESSAGE],
    {
      onAvailable: () => {},
    }
  );

  info("Reload the page");
  await BrowserTestUtils.reloadTab(tab);

  info("Register second listener");
  const cachedResources = [];
  await resourceCommand.watchResources(
    [resourceCommand.TYPES.CONSOLE_MESSAGE],
    {
      onAvailable: resources => cachedResources.push(...resources),
    }
  );

  is(cachedResources.length, 0, "The cache in ResourceCommand is cleared");

  targetCommand.destroy();
  await client.close();
});

add_task(async function () {
  info("Test with multiple resource types");

  const tab = await addTab(TEST_URI);

  const { client, resourceCommand, targetCommand } =
    await initResourceCommand(tab);

  info("Register first listener to get all available resources");
  const availableResources = [];
  await resourceCommand.watchResources(
    [
      resourceCommand.TYPES.CONSOLE_MESSAGE,
      resourceCommand.TYPES.ERROR_MESSAGE,
    ],
    {
      onAvailable: resources => availableResources.push(...resources),
    }
  );

  info("Add messages as console message");
  const consoleMessages1 = ["a", "b", "c"];
  await logMessages(tab.linkedBrowser, consoleMessages1);

  info("Add message as error message");
  const errorMessages = ["document.doTheImpossible();"];
  await triggerErrors(tab.linkedBrowser, errorMessages);

  info("Add messages as console message again");
  const consoleMessages2 = ["d", "e"];
  await logMessages(tab.linkedBrowser, consoleMessages2);

  info("Wait until the getting all available resources");
  const totalResourceCount =
    consoleMessages1.length + errorMessages.length + consoleMessages2.length;
  await waitUntil(() => {
    return availableResources.length === totalResourceCount;
  });

  info("Register listener to get the cached resources");
  const cachedResources = [];
  await resourceCommand.watchResources(
    [
      resourceCommand.TYPES.CONSOLE_MESSAGE,
      resourceCommand.TYPES.ERROR_MESSAGE,
    ],
    {
      onAvailable: resources => cachedResources.push(...resources),
    }
  );

  assertResources(cachedResources, availableResources);

  targetCommand.destroy();
  await client.close();
});

add_task(async function () {
  info("Test multiple listeners with/without ignoreExistingResources");
  await testIgnoreExistingResources(true);
  await testIgnoreExistingResources(false);
});

async function testIgnoreExistingResources(isFirstListenerIgnoreExisting) {
  const tab = await addTab(TEST_URI);

  const { client, resourceCommand, targetCommand } =
    await initResourceCommand(tab);

  info("Add messages as existing resources");
  const existingMessages = ["a", "b", "c"];
  await logMessages(tab.linkedBrowser, existingMessages);

  info("Register first listener");
  const cachedResources1 = [];
  await resourceCommand.watchResources(
    [resourceCommand.TYPES.CONSOLE_MESSAGE],
    {
      onAvailable: resources => cachedResources1.push(...resources),
      ignoreExistingResources: isFirstListenerIgnoreExisting,
    }
  );

  info("Register second listener");
  const cachedResources2 = [];
  await resourceCommand.watchResources(
    [resourceCommand.TYPES.CONSOLE_MESSAGE],
    {
      onAvailable: resources => cachedResources2.push(...resources),
      ignoreExistingResources: !isFirstListenerIgnoreExisting,
    }
  );

  const cachedResourcesWithFlag = isFirstListenerIgnoreExisting
    ? cachedResources1
    : cachedResources2;
  const cachedResourcesWithoutFlag = isFirstListenerIgnoreExisting
    ? cachedResources2
    : cachedResources1;

  info("Check the existing resources both listeners got");
  assertContents(cachedResourcesWithFlag, []);
  assertContents(cachedResourcesWithoutFlag, existingMessages);

  info("Add messages as additional resources");
  const additionalMessages = ["d", "e"];
  await logMessages(tab.linkedBrowser, additionalMessages);

  info("Wait until onAvailable is called expected times");
  await waitUntil(
    () => cachedResourcesWithFlag.length === additionalMessages.length
  );
  const allMessages = [...existingMessages, ...additionalMessages];
  await waitUntil(
    () => cachedResourcesWithoutFlag.length === allMessages.length
  );

  info("Check the resources after adding messages");
  assertContents(cachedResourcesWithFlag, additionalMessages);
  assertContents(cachedResourcesWithoutFlag, allMessages);

  targetCommand.destroy();
  await client.close();
}

add_task(async function () {
  info("Test that onAvailable is not called with an empty resources array");

  const tab = await addTab(TEST_URI);

  const { client, resourceCommand, targetCommand } =
    await initResourceCommand(tab);

  info("Register first listener to get all available resources");
  const availableResources = [];
  let onAvailableCallCount = 0;
  const onAvailable = resources => {
    ok(
      !!resources.length,
      "onAvailable is called with a non empty resources array"
    );
    availableResources.push(...resources);
    onAvailableCallCount++;
  };

  await resourceCommand.watchResources(
    [resourceCommand.TYPES.CONSOLE_MESSAGE],
    { onAvailable }
  );
  is(availableResources.length, 0, "availableResources array is empty");
  is(onAvailableCallCount, 0, "onAvailable was never called");

  info("Add messages as console message");
  await logMessages(tab.linkedBrowser, ["expected message"]);

  await waitUntil(() => availableResources.length === 1);
  is(availableResources.length, 1, "availableResources array has one item");
  is(onAvailableCallCount, 1, "onAvailable was called only once");
  is(
    availableResources[0].arguments[0],
    "expected message",
    "onAvailable was called with the expected resource"
  );

  resourceCommand.unwatchResources([resourceCommand.TYPES.CONSOLE_MESSAGE], {
    onAvailable,
  });
  targetCommand.destroy();
  await client.close();
});

function assertContents(resources, expectedMessages) {
  is(
    resources.length,
    expectedMessages.length,
    "Number of the resources is correct"
  );

  for (let i = 0; i < expectedMessages.length; i++) {
    const resource = resources[i];
    const message = resource.arguments[0];
    const expectedMessage = expectedMessages[i];
    is(message, expectedMessage, `The ${i}th content is correct`);
  }
}

function assertResources(resources, expectedResources) {
  is(
    resources.length,
    expectedResources.length,
    "Number of the resources is correct"
  );

  for (let i = 0; i < resources.length; i++) {
    const resource = resources[i];
    const expectedResource = expectedResources[i];
    Assert.strictEqual(
      resource,
      expectedResource,
      `The ${i}th resource is correct`
    );
  }
}

function logMessages(browser, messages) {
  return ContentTask.spawn(browser, { messages }, args => {
    for (const message of args.messages) {
      content.console.log(message);
    }
  });
}

async function triggerErrors(browser, errorScripts) {
  for (const errorScript of errorScripts) {
    await ContentTask.spawn(browser, errorScript, expr => {
      const document = content.document;
      const container = document.createElement("script");
      document.body.appendChild(container);
      container.textContent = expr;
      container.remove();
    });
  }
}
