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

"use strict";

// Test the ResourceCommand API around CSS_MESSAGE
// Reproduces the CSS message assertions from devtools/shared/webconsole/test/chrome/test_page_errors.html

const { MESSAGE_CATEGORY } = require("resource://devtools/shared/constants.js");

// Create a simple server so we have a nice sourceName in the resources packets.
const httpServer = createTestHTTPServer();
httpServer.registerPathHandler(`/test_css_messages.html`, (req, res) => {
  res.setStatusLine(req.httpVersion, 200, "OK");
  res.setHeader("Content-Type", "text/html");
  res.write(`<meta charset=utf8>
    <style>
      html {
        body {
          color: bloup;
        }
      }
    </style>Test CSS Messages`);
});

const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/test_css_messages.html`;

add_task(async function () {
  await testWatchingCssMessages();
  await testWatchingCachedCssMessages();
});

async function testWatchingCssMessages() {
  // Disable the preloaded process as it creates processes intermittently
  // which forces the emission of RDP requests we aren't correctly waiting for.
  await pushPref("dom.ipc.processPrelaunch.enabled", false);

  // Open a test tab
  const tab = await addTab(TEST_URI);

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

  const receivedMessages = [];
  const { onAvailable, onAllMessagesReceived } = setupOnAvailableFunction(
    targetCommand,
    receivedMessages,
    false
  );
  await resourceCommand.watchResources([resourceCommand.TYPES.CSS_MESSAGE], {
    onAvailable,
  });

  info(
    "Now log CSS warning *after* the call to ResourceCommand.watchResources and after " +
      "having received the existing message"
  );
  // We need to wait for the first CSS Warning as it is not a cached message; when we
  // start watching, the `cssErrorReportingEnabled` is checked on the target docShell, and
  // if it is false, we re-parse the stylesheets to get the messages.
  await BrowserTestUtils.waitForCondition(() => receivedMessages.length === 1);

  info("Trigger a CSS Warning");
  triggerCSSWarning(tab);

  info("Waiting for all expected CSS messages to be received");
  await onAllMessagesReceived;
  ok(true, "All the expected CSS messages were received");

  Services.console.reset();
  targetCommand.destroy();
  await client.close();
}

async function testWatchingCachedCssMessages() {
  // Disable the preloaded process as it creates processes intermittently
  // which forces the emission of RDP requests we aren't correctly waiting for.
  await pushPref("dom.ipc.processPrelaunch.enabled", false);

  // Open a test tab
  const tab = await addTab(TEST_URI);

  // By default, the CSS Parser does not emit warnings at all, for performance matter.
  // Since we actually want the Parser to emit those messages _before_ we start listening
  // for CSS messages, we need to set the cssErrorReportingEnabled flag on the docShell.
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
    content.docShell.cssErrorReportingEnabled = true;
    ChromeUtils.clearResourceCache({
      types: ["stylesheet"],
    });
  });

  // Setting the docShell flag only indicates to the Parser that from now on, it should
  // emit warnings. But it does not automatically emit warnings for the existing CSS
  // errors in the stylesheets. So here we reload the tab, which will make the Parser
  // parse the stylesheets again, this time emitting warnings.
  await reloadSelectedTab();
  // and trigger more CSS warnings
  await triggerCSSWarning(tab);

  // At this point, all messages should be in the ConsoleService cache, and we can begin
  // to watch and check that we do retrieve those messages.
  const { client, resourceCommand, targetCommand } =
    await initResourceCommand(tab);

  const receivedMessages = [];
  const { onAvailable } = setupOnAvailableFunction(
    targetCommand,
    receivedMessages,
    true
  );
  await resourceCommand.watchResources([resourceCommand.TYPES.CSS_MESSAGE], {
    onAvailable,
  });
  is(receivedMessages.length, 3, "Cached messages were retrieved as expected");

  Services.console.reset();
  targetCommand.destroy();
  await client.close();
}

function setupOnAvailableFunction(
  targetCommand,
  receivedMessages,
  isAlreadyExistingResource
) {
  // timeStamp are the result of a number in microsecond divided by 1000.
  // so we can't expect a precise number of decimals, or even if there would
  // be decimals at all.
  const FRACTIONAL_NUMBER_REGEX = /^\d+(\.\d{1,3})?$/;

  // The expected messages are the CSS warnings:
  // - one for the rule in the style element
  // - two for the JS modified style we're doing in the test.
  const expectedMessages = [
    {
      pageError: {
        errorMessage: /Expected color but found ‘bloup’/,
        sourceName: /test_css_messages/,
        category: MESSAGE_CATEGORY.CSS_PARSER,
        timeStamp: FRACTIONAL_NUMBER_REGEX,
        error: false,
        warning: true,
      },
      cssSelectors: ":is(html) body",
      isAlreadyExistingResource,
    },
    {
      pageError: {
        errorMessage: /Error in parsing value for ‘width’/,
        sourceName: /test_css_messages/,
        category: MESSAGE_CATEGORY.CSS_PARSER,
        timeStamp: FRACTIONAL_NUMBER_REGEX,
        error: false,
        warning: true,
      },
      isAlreadyExistingResource,
    },
    {
      pageError: {
        errorMessage: /Error in parsing value for ‘height’/,
        sourceName: /test_css_messages/,
        category: MESSAGE_CATEGORY.CSS_PARSER,
        timeStamp: FRACTIONAL_NUMBER_REGEX,
        error: false,
        warning: true,
      },
      isAlreadyExistingResource,
    },
  ];

  let done;
  const onAllMessagesReceived = new Promise(resolve => (done = resolve));
  const onAvailable = resources => {
    for (const resource of resources) {
      const { pageError } = resource;

      is(
        resource.targetFront,
        targetCommand.targetFront,
        "The targetFront property is the expected one"
      );

      if (!pageError.sourceName.includes("test_css_messages")) {
        info(`Ignore error from unknown source: "${pageError.sourceName}"`);
        continue;
      }

      const index = receivedMessages.length;
      receivedMessages.push(resource);

      info(
        `checking received css message #${index}: ${pageError.errorMessage}`
      );
      ok(pageError, "The resource has a pageError attribute");
      checkObject(resource, expectedMessages[index]);

      if (receivedMessages.length == expectedMessages.length) {
        done();
      }
    }
  };
  return { onAvailable, onAllMessagesReceived };
}

/**
 * Sets invalid values for width and height on the document's body style attribute.
 */
function triggerCSSWarning(tab) {
  return ContentTask.spawn(tab.linkedBrowser, null, function frameScript() {
    content.document.body.style.width = "red";
    content.document.body.style.height = "blue";
  });
}
