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

"use strict";

const URL = "about:blank";

async function getBrowsingContextId(browser, id) {
  return SpecialPowers.spawn(browser, [id], async function (id) {
    let contextId = content.window.docShell.browsingContext.id;

    let frames = [content.window];
    while (frames.length) {
      let frame = frames.pop();
      let target = frame.document.getElementById(id);
      if (target) {
        contextId = target.docShell.browsingContext.id;
        break;
      }

      frames = frames.concat(Array.from(frame.frames));
    }

    return contextId;
  });
}

async function addFrame(browser, id, parentId) {
  return SpecialPowers.spawn(
    browser,
    [{ parentId, id }],
    async function ({ parentId, id }) {
      let parent = null;
      if (parentId) {
        let frames = [content.window];
        while (frames.length) {
          let frame = frames.pop();
          let target = frame.document.getElementById(parentId);
          if (target) {
            parent = target.contentWindow.document.body;
            break;
          }
          frames = frames.concat(Array.from(frame.frames));
        }
      } else {
        parent = content.document.body;
      }

      let frame = await new Promise(resolve => {
        let frame = content.document.createElement("iframe");
        frame.id = id || "";
        frame.url = "about:blank";
        frame.onload = () => resolve(frame);
        parent.appendChild(frame);
      });

      return frame.contentWindow.docShell.browsingContext.id;
    }
  );
}

async function removeFrame(browser, id) {
  return SpecialPowers.spawn(browser, [id], async function (id) {
    let frames = [content.window];
    while (frames.length) {
      let frame = frames.pop();
      let target = frame.document.getElementById(id);
      if (target) {
        target.remove();
        break;
      }

      frames = frames.concat(Array.from(frame.frames));
    }
  });
}

function getBrowsingContextById(id) {
  return BrowsingContext.get(id);
}

add_task(async function () {
  await BrowserTestUtils.withNewTab(
    { gBrowser, url: URL },
    async function (browser) {
      let topId = await getBrowsingContextId(browser, "");
      let topContext = getBrowsingContextById(topId);
      isnot(topContext, null);
      is(topContext.parent, null);
      is(
        topId,
        browser.browsingContext.id,
        "<browser> has the correct browsingContext"
      );
      is(
        browser.browserId,
        topContext.browserId,
        "browsing context should have a correct <browser> id"
      );

      let id0 = await addFrame(browser, "frame0");
      let browsingContext0 = getBrowsingContextById(id0);
      isnot(browsingContext0, null);
      is(browsingContext0.parent, topContext);

      await removeFrame(browser, "frame0");

      is(topContext.children.indexOf(browsingContext0), -1);

      // TODO(farre): Handle browsingContext removal [see Bug 1486719].
      todo_isnot(browsingContext0.parent, topContext);
    }
  );
});

add_task(async function () {
  // If Fission is disabled, the pref is no-op.
  await SpecialPowers.pushPrefEnv({ set: [["fission.bfcacheInParent", true]] });

  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url:
        getRootDirectory(gTestPath).replace(
          "chrome://mochitests/content",
          // eslint-disable-next-line @microsoft/sdl/no-insecure-url
          "http://example.com"
        ) + "dummy_page.html",
    },
    async function (browser) {
      let path = getRootDirectory(gTestPath).replace(
        "chrome://mochitests/content",
        // eslint-disable-next-line @microsoft/sdl/no-insecure-url
        "http://example.com"
      );
      await SpecialPowers.spawn(browser, [path], async function (path) {
        var bc = new content.BroadcastChannel("browser_browsingContext");
        function waitForMessage(command) {
          let p = new Promise(resolve => {
            bc.addEventListener("message", e => resolve(e), { once: true });
          });
          command();
          return p;
        }

        // Open a new window and wait for the message.
        let e1 = await waitForMessage(_ =>
          content.window.open(path + "onpageshow_message.html", "", "noopener")
        );

        is(e1.data, "pageshow", "Got page show");

        let e2 = await waitForMessage(_ => bc.postMessage("createiframe"));
        is(e2.data.framesLength, 1, "Here we should have an iframe");

        let e3 = await waitForMessage(_ => bc.postMessage("nextpage"));

        is(e3.data.event, "load");
        is(e3.data.framesLength, 0, "Here there shouldn't be an iframe");

        // Return to the previous document. N.B. we expect to trigger
        // BFCache here, hence we wait for pageshow.
        let e4 = await waitForMessage(_ => bc.postMessage("back"));

        is(e4.data, "pageshow");

        let e5 = await waitForMessage(_ => bc.postMessage("queryframes"));
        is(e5.data.framesLength, 1, "And again there should be an iframe");

        is(e5.outerWindowId, e2.outerWindowId, "BF cache cached outer window");
        is(e5.browsingContextId, e2.browsingContextId, "BF cache cached BC");

        let e6 = await waitForMessage(_ => bc.postMessage("close"));
        is(e6.data, "closed");

        bc.close();
      });
    }
  );
});
