/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js", this);

// Test for Bug 835896
// WalkerSearch specific tests.  This is to make sure search results are
// coming back as expected.
// See also test_inspector-search-front.html.

add_task(async function() {
  const { walker } =
    await initInspectorFront(MAIN_DOMAIN + "inspector-search-data.html");

  await ContentTask.spawn(gBrowser.selectedBrowser,
    [walker.actorID],
    async function(actorID) {
      const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
      const { DebuggerServer } = require("devtools/server/main");
      const {DocumentWalker: _documentWalker} = require("devtools/server/actors/inspector/document-walker");

      // Convert actorID to current compartment string otherwise
      // searchAllConnectionsForActor is confused and won't find the actor.
      actorID = String(actorID);
      const walkerActor = DebuggerServer.searchAllConnectionsForActor(actorID);
      const walkerSearch = walkerActor.walkerSearch;
      const {WalkerSearch, WalkerIndex} =
        require("devtools/server/actors/utils/walker-search");

      info("Testing basic index APIs exist.");
      const index = new WalkerIndex(walkerActor);
      ok(index.data.size > 0, "public index is filled after getting");

      index.clearIndex();
      ok(!index._data, "private index is empty after clearing");
      ok(index.data.size > 0, "public index is filled after getting");

      index.destroy();

      info("Testing basic search APIs exist.");

      ok(walkerSearch, "walker search exists on the WalkerActor");
      ok(walkerSearch.search, "walker search has `search` method");
      ok(walkerSearch.index, "walker search has `index` property");
      is(walkerSearch.walker, walkerActor, "referencing the correct WalkerActor");

      const walkerSearch2 = new WalkerSearch(walkerActor);
      ok(walkerSearch2, "a new search instance can be created");
      ok(walkerSearch2.search, "new search instance has `search` method");
      ok(walkerSearch2.index, "new search instance has `index` property");
      isnot(walkerSearch2, walkerSearch,
        "new search instance differs from the WalkerActor's");

      walkerSearch2.destroy();

      info("Testing search with an empty query.");
      let results = walkerSearch.search("");
      is(results.length, 0, "No results when searching for ''");

      results = walkerSearch.search(null);
      is(results.length, 0, "No results when searching for null");

      results = walkerSearch.search(undefined);
      is(results.length, 0, "No results when searching for undefined");

      results = walkerSearch.search(10);
      is(results.length, 0, "No results when searching for 10");

      const inspectee = content.document;
      const testData = [
      {
        desc: "Search for tag with one result.",
        search: "body",
        expected: [
          {node: inspectee.body, type: "tag"},
        ],
      },
      {
        desc: "Search for tag with multiple results",
        search: "h2",
        expected: [
          {node: inspectee.querySelectorAll("h2")[0], type: "tag"},
          {node: inspectee.querySelectorAll("h2")[1], type: "tag"},
          {node: inspectee.querySelectorAll("h2")[2], type: "tag"},
        ],
      },
      {
        desc: "Search for selector with multiple results",
        search: "body > h2",
        expected: [
          {node: inspectee.querySelectorAll("h2")[0], type: "selector"},
          {node: inspectee.querySelectorAll("h2")[1], type: "selector"},
          {node: inspectee.querySelectorAll("h2")[2], type: "selector"},
        ],
      },
      {
        desc: "Search for selector with multiple results",
        search: ":root h2",
        expected: [
          {node: inspectee.querySelectorAll("h2")[0], type: "selector"},
          {node: inspectee.querySelectorAll("h2")[1], type: "selector"},
          {node: inspectee.querySelectorAll("h2")[2], type: "selector"},
        ],
      },
      {
        desc: "Search for selector with multiple results",
        search: "* h2",
        expected: [
          {node: inspectee.querySelectorAll("h2")[0], type: "selector"},
          {node: inspectee.querySelectorAll("h2")[1], type: "selector"},
          {node: inspectee.querySelectorAll("h2")[2], type: "selector"},
        ],
      },
      {
        desc: "Search with multiple matches in a single tag expecting a single result",
        search: "💩",
        expected: [
          {node: inspectee.getElementById("💩"), type: "attributeValue"},
        ],
      },
      {
        desc: "Search that has tag and text results",
        search: "h1",
        expected: [
          {node: inspectee.querySelector("h1"), type: "tag"},
          {node: inspectee.querySelector("h1 + p").childNodes[0], type: "text"},
          {node: inspectee.querySelector("h1 + p > strong").childNodes[0], type: "text"},
        ],
      },
      ];

      const isDeeply = (a, b, msg) => {
        return is(JSON.stringify(a), JSON.stringify(b), msg);
      };
      for (const {desc, search, expected} of testData) {
        info("Running test: " + desc);
        results = walkerSearch.search(search);
        isDeeply(results, expected,
          "Search returns correct results with '" + search + "'");
      }

      info("Testing ::before and ::after element matching");

      const beforeElt = new _documentWalker(inspectee.querySelector("#pseudo"),
                                          inspectee.defaultView).firstChild();
      const afterElt = new _documentWalker(inspectee.querySelector("#pseudo"),
                                         inspectee.defaultView).lastChild();
      const styleText = inspectee.querySelector("style").childNodes[0];

      // ::before
      results = walkerSearch.search("::before");
      isDeeply(results, [ {node: beforeElt, type: "tag"} ],
               "Tag search works for pseudo element");

      results = walkerSearch.search("_moz_generated_content_before");
      is(results.length, 0, "No results for anon tag name");

      results = walkerSearch.search("before element");
      isDeeply(results, [
        {node: styleText, type: "text"},
        {node: beforeElt, type: "text"},
      ], "Text search works for pseudo element");

      // ::after
      results = walkerSearch.search("::after");
      isDeeply(results, [ {node: afterElt, type: "tag"} ],
               "Tag search works for pseudo element");

      results = walkerSearch.search("_moz_generated_content_after");
      is(results.length, 0, "No results for anon tag name");

      results = walkerSearch.search("after element");
      isDeeply(results, [
        {node: styleText, type: "text"},
        {node: afterElt, type: "text"},
      ], "Text search works for pseudo element");

      info("Testing search before and after a mutation.");
      const expected = [
        {node: inspectee.querySelectorAll("h3")[0], type: "tag"},
        {node: inspectee.querySelectorAll("h3")[1], type: "tag"},
        {node: inspectee.querySelectorAll("h3")[2], type: "tag"},
      ];

      results = walkerSearch.search("h3");
      isDeeply(results, expected, "Search works with tag results");

      function mutateDocumentAndWaitForMutation(mutationFn) {
        // eslint-disable-next-line new-cap
        return new Promise(resolve => {
          info("Listening to markup mutation on the inspectee");
          const observer = new inspectee.defaultView.MutationObserver(resolve);
          observer.observe(inspectee, {childList: true, subtree: true});
          mutationFn();
        });
      }
      await mutateDocumentAndWaitForMutation(() => {
        expected[0].node.remove();
      });

      results = walkerSearch.search("h3");
      isDeeply(results, [
        expected[1],
        expected[2],
      ], "Results are updated after removal");

      // eslint-disable-next-line new-cap
      await new Promise(resolve => {
        info("Waiting for a mutation to happen");
        const observer = new inspectee.defaultView.MutationObserver(() => {
          resolve();
        });
        observer.observe(inspectee, {attributes: true, subtree: true});
        inspectee.body.setAttribute("h3", "true");
      });

      results = walkerSearch.search("h3");
      isDeeply(results, [
        {node: inspectee.body, type: "attributeName"},
        expected[1],
        expected[2],
      ], "Results are updated after addition");

      // eslint-disable-next-line new-cap
      await new Promise(resolve => {
        info("Waiting for a mutation to happen");
        const observer = new inspectee.defaultView.MutationObserver(() => {
          resolve();
        });
        observer.observe(inspectee, {attributes: true, childList: true, subtree: true});
        inspectee.body.removeAttribute("h3");
        expected[1].node.remove();
        expected[2].node.remove();
      });

      results = walkerSearch.search("h3");
      is(results.length, 0, "Results are updated after removal");
    });
});
