/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * This module provides the bridge between the "AppTestDelegate" helper in
 * mochitests and the supporting implementations in AppUiTestDelegate.sys.mjs.
 *
 * "AppTestDelegate" is documented in AppTestDelegate.sys.mjs and enables
 * mochitests to invoke common functionality whose implementation is different
 * (e.g. in browser/ and mobile/ instead of toolkit/).
 * Tests can use this common interface after importing AppTestDelegate.sys.mjs:
 *
 *     // head.js, in the scope of a plain mochitest:
 *     var { AppTestDelegate } = SpecialPowers.ChromeUtils.importESModule(
 *       "resource://specialpowers/AppTestDelegate.sys.mjs"
 *     );
 *
 *     // test usage example: open and close a tab.
 *     let tab = await AppTestDelegate.openNewForegroundTab(window, url);
 *     await AppTestDelegate.removeTab(window, tab);
 *
 * ## Overview of files supporting "AppTestDelegate":
 *
 * MOZ_BUILD_APP-specific AppUiTestDelegate.sys.mjs implementations:
 * - browser/components/extensions/test/AppUiTestDelegate.sys.mjs
 * - mobile/shared/modules/test/AppUiTestDelegate.sys.mjs
 * - mail/components/extensions/test/AppUiTestDelegate.sys.mjs (in comm-central)
 *
 * Glue between AppUiTestDelegate.sys.mjs in parent and test code in child:
 * - testing/specialpowers/content/AppTestDelegateParent.sys.mjs (this file)
 * - testing/specialpowers/content/AppTestDelegateChild.sys.mjs
 * - testing/specialpowers/content/AppTestDelegate.sys.mjs
 *
 * Setup for usage by test code in child (i.e. plain mochitests):
 * - Import AppTestDelegate.sys.mjs (e.g. in head.js or the test)
 *
 * Note: some browser-chrome tests import AppUiTestDelegate.sys.mjs directly,
 * but that is not part of this API contract. They merely reuse code.
 *
 * ## How to add new AppTestDelegate methods
 *
 * - Add the method to AppTestDelegate.sys.mjs
 * - Add a message forwarder in AppTestDelegateChild.sys.mjs
 * - Add a message handler in AppTestDelegateParent.sys.mjs
 * - Add an implementation in AppUiTestDelegate.sys.mjs for each MOZ_BUILD_APP,
 *   by defining the method on the exported AppUiTestDelegate object.
 *   All AppUiTestDelegate implementations must be kept in sync to have the
 *   same interface!
 *
 * You should use the same method name across all of these files for ease of
 * lookup and maintainability.
 */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  // Each app needs to implement this - see above comment.
  AppUiTestDelegate: "resource://testing-common/AppUiTestDelegate.sys.mjs",
});

export class AppTestDelegateParent extends JSWindowActorParent {
  constructor() {
    super();
    this._tabs = new Map();
  }

  get browser() {
    return this.browsingContext.top.embedderElement;
  }

  get window() {
    return this.browser.ownerGlobal;
  }

  async receiveMessage(message) {
    const { extensionId, url, waitForLoad, tabId } = message.data;
    switch (message.name) {
      case "DOMContentLoaded":
      case "load": {
        return this.browser?.dispatchEvent(
          new CustomEvent(`AppTestDelegate:${message.name}`, {
            detail: {
              browsingContext: this.browsingContext,
              ...message.data,
            },
          })
        );
      }
      case "clickPageAction":
        return lazy.AppUiTestDelegate.clickPageAction(this.window, extensionId);
      case "clickBrowserAction":
        return lazy.AppUiTestDelegate.clickBrowserAction(
          this.window,
          extensionId
        );
      case "closePageAction":
        return lazy.AppUiTestDelegate.closePageAction(this.window, extensionId);
      case "closeBrowserAction":
        return lazy.AppUiTestDelegate.closeBrowserAction(
          this.window,
          extensionId
        );
      case "awaitExtensionPanel":
        // The desktop delegate returns a <browser>, but that cannot be sent
        // over IPC, so just ignore it. The promise resolves when the panel and
        // its content is fully loaded.
        await lazy.AppUiTestDelegate.awaitExtensionPanel(
          this.window,
          extensionId
        );
        return null;
      case "openNewForegroundTab": {
        // We cannot send the tab object across process so let's store it with
        // a unique ID here.
        const uuid = Services.uuid.generateUUID().toString();
        const tab = await lazy.AppUiTestDelegate.openNewForegroundTab(
          this.window,
          url,
          waitForLoad
        );
        this._tabs.set(uuid, tab);
        return uuid;
      }
      case "removeTab": {
        const tab = this._tabs.get(tabId);
        this._tabs.delete(tabId);
        return lazy.AppUiTestDelegate.removeTab(tab);
      }

      default:
        throw new Error(`Unknown Test API: ${message.name}.`);
    }
  }
}
