/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

// Like most of the mochitest-browser devtools test,
// on debug test machine, it takes about 50s to run the test.
requestLongerTimeout(4);

loadTestSubscript("head_devtools.js");

// Allow rejections related to closing the devtools toolbox too soon after the test
// has already verified the details that were relevant for that test case
// (e.g. this was triggering an intermittent failure in shippable optimized
// builds, tracked Bug 1707644).
PromiseTestUtils.allowMatchingRejectionsGlobally(
  /Connection closed, pending request to/
);

const TEST_ORIGIN = "http://mochi.test:8888";
const TEST_BASE = getRootDirectory(gTestPath).replace(
  "chrome://mochitests/content",
  ""
);
const TEST_PATH = `${TEST_BASE}file_inspectedwindow_reload_target.sjs`;

// Small helper which provides the common steps to the following reload test cases.
async function runReloadTestCase({
  urlParams,
  background,
  devtoolsPage,
  testCase,
  closeToolbox = true,
}) {
  const TEST_TARGET_URL = `${TEST_ORIGIN}/${TEST_PATH}?${urlParams}`;
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_TARGET_URL
  );

  let extension = ExtensionTestUtils.loadExtension({
    background,
    manifest: {
      devtools_page: "devtools_page.html",
      permissions: ["webNavigation", "<all_urls>"],
    },
    files: {
      "devtools_page.html": `<!DOCTYPE html>
      <html>
       <head>
         <meta charset="utf-8">
         <script type="text/javascript" src="devtools_page.js"></script>
       </head>
       <body>
       </body>
      </html>`,
      "devtools_page.js": devtoolsPage,
    },
  });

  await extension.startup();

  const toolbox = await openToolboxForTab(tab);

  // Wait the test extension to be ready.
  await extension.awaitMessage("devtools_inspected_window_reload.ready");

  info("devtools page ready");

  // Run the test case.
  await testCase(extension, tab, toolbox);

  if (closeToolbox) {
    await closeToolboxForTab(tab);
  }

  BrowserTestUtils.removeTab(tab);

  await extension.unload();
}

add_task(async function test_devtools_inspectedWindow_reload_ignore_cache() {
  function background() {
    // Wait until the devtools page is ready to run the test.
    browser.runtime.onMessage.addListener(async msg => {
      if (msg !== "devtools_page.ready") {
        browser.test.fail(`Unexpected message received: ${msg}`);
        return;
      }

      const tabs = await browser.tabs.query({ active: true });
      const activeTabId = tabs[0].id;
      let reloads = 0;

      browser.webNavigation.onCompleted.addListener(async details => {
        if (details.tabId == activeTabId && details.frameId == 0) {
          reloads++;

          // This test expects two `devtools.inspectedWindow.reload` calls:
          // the first one without any options and the second one with
          // `ignoreCache=true`.
          let expectedContent;
          let enabled;

          switch (reloads) {
            case 1:
              enabled = false;
              expectedContent = "empty cache headers";
              break;
            case 2:
              enabled = true;
              expectedContent = "no-cache:no-cache";
              break;
          }

          if (!expectedContent) {
            browser.test.fail(`Unexpected number of tab reloads: ${reloads}`);
          } else {
            try {
              const code = `document.body.textContent`;
              const [text] = await browser.tabs.executeScript(activeTabId, {
                code,
              });

              browser.test.assertEq(
                text,
                expectedContent,
                `Got the expected cache headers with ignoreCache=${enabled}`
              );
            } catch (err) {
              browser.test.fail(`Error: ${err.message} - ${err.stack}`);
            }
          }

          browser.test.sendMessage(
            "devtools_inspectedWindow_reload_checkIgnoreCache.done"
          );
        }
      });

      browser.test.sendMessage("devtools_inspected_window_reload.ready");
    });
  }

  async function devtoolsPage() {
    browser.test.onMessage.addListener(msg => {
      switch (msg) {
        case "no-ignore-cache":
          browser.devtools.inspectedWindow.reload();
          break;
        case "ignore-cache":
          browser.devtools.inspectedWindow.reload({ ignoreCache: true });
          break;
        default:
          browser.test.fail(`Unexpected test message received: ${msg}`);
      }
    });

    browser.runtime.sendMessage("devtools_page.ready");
  }

  await runReloadTestCase({
    urlParams: "test=cache",
    background,
    devtoolsPage,
    testCase: async function (extension) {
      for (const testMessage of ["no-ignore-cache", "ignore-cache"]) {
        extension.sendMessage(testMessage);
        await extension.awaitMessage(
          "devtools_inspectedWindow_reload_checkIgnoreCache.done"
        );
      }
    },
  });
});

add_task(
  async function test_devtools_inspectedWindow_reload_custom_user_agent() {
    const CUSTOM_USER_AGENT = "CustomizedUserAgent";

    function background() {
      browser.runtime.onMessage.addListener(async msg => {
        if (msg !== "devtools_page.ready") {
          browser.test.fail(`Unexpected message received: ${msg}`);
          return;
        }

        browser.test.sendMessage("devtools_inspected_window_reload.ready");
      });
    }

    function devtoolsPage() {
      browser.test.onMessage.addListener(async msg => {
        switch (msg) {
          case "no-custom-user-agent":
            await browser.devtools.inspectedWindow.reload({});
            break;
          case "custom-user-agent":
            await browser.devtools.inspectedWindow.reload({
              userAgent: "CustomizedUserAgent",
            });
            break;
          default:
            browser.test.fail(`Unexpected test message received: ${msg}`);
        }
      });

      browser.runtime.sendMessage("devtools_page.ready");
    }

    async function checkUserAgent(expectedUA) {
      const contexts =
        gBrowser.selectedBrowser.browsingContext.getAllBrowsingContextsInSubtree();

      const uniqueRemoteTypes = new Set();
      for (const context of contexts) {
        uniqueRemoteTypes.add(context.currentRemoteType);
      }

      info(
        `Got ${contexts.length} with remoteTypes: ${Array.from(
          uniqueRemoteTypes
        )}`
      );
      Assert.greaterOrEqual(
        contexts.length,
        2,
        "There should be at least 2 browsing contexts"
      );

      if (Services.appinfo.fissionAutostart) {
        Assert.greaterOrEqual(
          uniqueRemoteTypes.size,
          2,
          "Expect at least one cross origin sub frame"
        );
      }

      for (const context of contexts) {
        const url = context.currentURI?.spec?.replace(
          context.currentURI?.query,
          "…"
        );
        info(
          `Checking user agent on ${url} (remoteType: ${context.currentRemoteType})`
        );
        await SpecialPowers.spawn(context, [expectedUA], async _expectedUA => {
          is(
            content.navigator.userAgent,
            _expectedUA,
            `expected navigator.userAgent value`
          );
          is(
            content.wrappedJSObject.initialUserAgent,
            _expectedUA,
            `expected navigator.userAgent value at startup`
          );
          if (content.wrappedJSObject.userAgentHeader) {
            is(
              content.wrappedJSObject.userAgentHeader,
              _expectedUA,
              `user agent header has expected value`
            );
          }
        });
      }
    }

    await runReloadTestCase({
      urlParams: "test=user-agent",
      background,
      devtoolsPage,
      closeToolbox: false,
      testCase: async function (extension, tab) {
        info("Get the initial user agent");
        const initialUserAgent = await SpecialPowers.spawn(
          gBrowser.selectedBrowser,
          [],
          () => {
            return content.navigator.userAgent;
          }
        );

        info(
          "Check that calling inspectedWindow.reload without userAgent does not change the user agent of the page"
        );
        let onPageLoaded = BrowserTestUtils.browserLoaded(
          gBrowser.selectedBrowser,
          true
        );
        extension.sendMessage("no-custom-user-agent");
        await onPageLoaded;

        await checkUserAgent(initialUserAgent);

        info(
          "Check that calling inspectedWindow.reload with userAgent does change the user agent of the page"
        );
        onPageLoaded = BrowserTestUtils.browserLoaded(
          gBrowser.selectedBrowser,
          true
        );
        extension.sendMessage("custom-user-agent");
        await onPageLoaded;

        await checkUserAgent(CUSTOM_USER_AGENT);

        info("Check that the user agent persists after a reload");
        await BrowserTestUtils.reloadTab(tab, {
          includeSubFrames: true,
        });
        await checkUserAgent(CUSTOM_USER_AGENT);

        info(
          "Check that the user agent persists after navigating to a new browsing context"
        );
        const previousBrowsingContextId =
          gBrowser.selectedBrowser.browsingContext.id;

        // Navigate to a different origin
        await navigateToWithDevToolsOpen(
          tab,
          `https://example.com/${TEST_PATH}?test=user-agent&crossOriginIsolated=true`
        );

        isnot(
          gBrowser.selectedBrowser.browsingContext.id,
          previousBrowsingContextId,
          "A new browsing context was created"
        );

        await checkUserAgent(CUSTOM_USER_AGENT);

        info(
          "Check that closing DevTools resets the user agent of the page to its initial value"
        );

        await closeToolboxForTab(tab);

        is(
          gBrowser.selectedBrowser.browsingContext.customUserAgent,
          "",
          "The flag on the browsing context was reset"
        );

        // We can not use `checkUserAgent` here because `wrappedJSObject.initialUserAgent` and
        // `wrappedJSObject.userAgentHeader` are not updated, since there was no reload
        // after resetting the override.
        const contexts =
          gBrowser.selectedBrowser.browsingContext.getAllBrowsingContextsInSubtree();

        for (const context of contexts) {
          const url = context.currentURI?.spec?.replace(
            context.currentURI?.query,
            "…"
          );
          info(
            `Checking user agent on ${url} (remoteType: ${context.currentRemoteType}), (${context.id})`
          );
          await SpecialPowers.spawn(
            context,
            [initialUserAgent],
            async _expectedUA => {
              is(
                content.navigator.userAgent,
                _expectedUA,
                `expected navigator.userAgent value`
              );
            }
          );
        }
        await BrowserTestUtils.reloadTab(tab, {
          includeSubFrames: true,
        });
        await checkUserAgent(initialUserAgent);
      },
    });
  }
);

add_task(async function test_devtools_inspectedWindow_reload_injected_script() {
  function background() {
    function getIframesTextContent() {
      let docs = [];
      for (
        let iframe, doc = document;
        doc;
        doc = iframe && iframe.contentDocument
      ) {
        docs.push(doc);
        iframe = doc.querySelector("iframe");
      }

      return docs.map(doc => doc.querySelector("pre").textContent);
    }

    browser.runtime.onMessage.addListener(async msg => {
      if (msg !== "devtools_page.ready") {
        browser.test.fail(`Unexpected message received: ${msg}`);
        return;
      }

      const tabs = await browser.tabs.query({ active: true });
      const activeTabId = tabs[0].id;
      let reloads = 0;

      browser.webNavigation.onCompleted.addListener(async details => {
        if (details.tabId == activeTabId && details.frameId == 0) {
          reloads++;

          let expectedContent;
          let enabled;

          switch (reloads) {
            case 1:
              enabled = false;
              expectedContent = "injected script NOT executed";
              break;
            case 2:
              enabled = true;
              expectedContent = "injected script executed first";
              break;
            default:
              browser.test.fail(`Unexpected number of tab reloads: ${reloads}`);
          }

          if (!expectedContent) {
            browser.test.fail(`Unexpected number of tab reloads: ${reloads}`);
          } else {
            let expectedResults = new Array(4).fill(expectedContent);
            let code = `(${getIframesTextContent})()`;

            try {
              let [results] = await browser.tabs.executeScript(activeTabId, {
                code,
              });

              browser.test.assertEq(
                JSON.stringify(expectedResults),
                JSON.stringify(results),
                `Got the expected result with injectScript=${enabled}`
              );
            } catch (err) {
              browser.test.fail(`Error: ${err.message} - ${err.stack}`);
            }
          }

          browser.test.sendMessage(
            `devtools_inspectedWindow_reload_injectedScript.done`
          );
        }
      });

      browser.test.sendMessage("devtools_inspected_window_reload.ready");
    });
  }

  function devtoolsPage() {
    function injectedScript() {
      if (!window.pageScriptExecutedFirst) {
        window.addEventListener(
          "DOMContentLoaded",
          function listener() {
            document.querySelector("pre").textContent =
              "injected script executed first";
          },
          { once: true }
        );
      }
    }

    browser.test.onMessage.addListener(msg => {
      switch (msg) {
        case "no-injected-script":
          browser.devtools.inspectedWindow.reload({});
          break;
        case "injected-script":
          browser.devtools.inspectedWindow.reload({
            injectedScript: `new ${injectedScript}`,
          });
          break;
        default:
          browser.test.fail(`Unexpected test message received: ${msg}`);
      }
    });

    browser.runtime.sendMessage("devtools_page.ready");
  }

  await runReloadTestCase({
    urlParams: "test=injected-script&frames=3",
    background,
    devtoolsPage,
    testCase: async function (extension) {
      extension.sendMessage("no-injected-script");

      await extension.awaitMessage(
        "devtools_inspectedWindow_reload_injectedScript.done"
      );

      extension.sendMessage("injected-script");

      await extension.awaitMessage(
        "devtools_inspectedWindow_reload_injectedScript.done"
      );
    },
  });
});
