/* 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/. */

"use strict";

let proxyChannel;

function initProxy() {
  return new Promise(resolve => {
    let proxyChannel;

    let proxyCallback = {
      QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyCallback"]),

      onProxyAvailable(_req, _uri, _pi, _status) {
        class ProxyChannelListener {
          onStartRequest(_request) {
            resolve(proxyChannel);
          }
          onStopRequest(_request, _status) {}
        }
        // I'm cheating a bit here... We should probably do some magic foo to get
        // something implementing nsIProxiedProtocolHandler and then call
        // NewProxiedChannel(), so we have something that's definately a proxied
        // channel. But Mochitests use a proxy for a number of hosts, so just
        // requesting a normal channel will give us a channel that's proxied.
        // The proxyChannel needs to move to at least on-modify-request to
        // have valid ProxyInfo, but we use OnStartRequest during startup()
        // for simplicity.
        proxyChannel = Services.io.newChannel(
          "http://mochi.test:8888",
          null,
          null,
          null, // aLoadingNode
          Services.scriptSecurityManager.getSystemPrincipal(),
          null, // aTriggeringPrincipal
          Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
          Ci.nsIContentPolicy.TYPE_OTHER
        );
        proxyChannel.asyncOpen(new ProxyChannelListener());
      },
    };

    // Need to allow for arbitrary network servers defined in PAC instead of a hardcoded moz-proxy.
    let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();

    let channel = Services.io.newChannel(
      "https://example.com",
      null,
      null,
      null, // aLoadingNode
      Services.scriptSecurityManager.getSystemPrincipal(),
      null, // aTriggeringPrincipal
      Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
      Ci.nsIContentPolicy.TYPE_OTHER
    );
    pps.asyncResolve(channel, 0, proxyCallback);
  });
}

add_setup(async function () {
  proxyChannel = await initProxy();
});

/**
 * Create an object for consuming an nsIAuthPromptCallback.
 *
 * @returns result
 * @returns {nsIAuthPromptCallback} result.callback - Callback to be passed into
 * asyncPromptAuth.
 * @returns {Promise} result.promise - Promise which resolves with authInfo once
 * the callback has been called.
 */
function getAuthPromptCallback() {
  let callbackResolver;
  let promise = new Promise(resolve => {
    callbackResolver = resolve;
  });
  let callback = {
    onAuthAvailable(_context, authInfo) {
      callbackResolver(authInfo);
    },
  };
  return { callback, promise };
}

/**
 * Tests that if a window proxy auth prompt is open, subsequent auth calls with
 * matching realm will be merged into the existing prompt. This should work even
 * if the follwing auth call has browser parent.
 */
add_task(async function testProxyAuthPromptMerge() {
  let tabA = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    "https://example.com"
  );
  let tabB = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    "https://example.org"
  );

  const promptFac = Cc[
    "@mozilla.org/passwordmanager/authpromptfactory;1"
  ].getService(Ci.nsIPromptFactory);
  let prompter = promptFac.getPrompt(window, Ci.nsIAuthPrompt2);

  let level = Ci.nsIAuthPrompt2.LEVEL_NONE;
  let proxyAuthinfo = {
    username: "",
    password: "",
    domain: "",
    flags: Ci.nsIAuthInformation.AUTH_PROXY,
    authenticationScheme: "basic",
    realm: "",
  };

  // The next prompt call will result in window prompt, because the prompt does
  // not have a browser set yet. The browser is used as a parent for tab
  // prompts.
  let promptOpened = PromptTestUtils.waitForPrompt(null, {
    modalType: Services.prompt.MODAL_TYPE_WINDOW,
  });
  let cbWinPrompt = getAuthPromptCallback();
  info("asyncPromptAuth no parent");
  prompter.asyncPromptAuth(
    proxyChannel,
    cbWinPrompt.callback,
    null,
    level,
    proxyAuthinfo
  );
  let prompt = await promptOpened;

  // Set a browser so the next prompt would open as tab prompt.
  prompter.QueryInterface(Ci.nsILoginManagerAuthPrompter).browser =
    tabA.linkedBrowser;

  // Since we already have an open window prompts, subsequent calls with
  // matching realm should be merged into this prompt and no additional prompts
  // must be spawned.
  let cbNoPrompt = getAuthPromptCallback();
  info("asyncPromptAuth tabA parent");
  prompter.asyncPromptAuth(
    proxyChannel,
    cbNoPrompt.callback,
    null,
    level,
    proxyAuthinfo
  );

  // Switch to the next tabs browser.
  prompter.QueryInterface(Ci.nsILoginManagerAuthPrompter).browser =
    tabB.linkedBrowser;

  let cbNoPrompt2 = getAuthPromptCallback();
  info("asyncPromptAuth tabB parent");
  prompter.asyncPromptAuth(
    proxyChannel,
    cbNoPrompt2.callback,
    null,
    level,
    proxyAuthinfo
  );

  // Accept the prompt.
  PromptTestUtils.handlePrompt(prompt, {});

  // Accepting the window prompts should complete all auth requests.
  let authInfo1 = await cbWinPrompt.promise;
  Assert.ok(authInfo1, "Received callback from first proxy auth call.");
  let authInfo2 = await cbNoPrompt.promise;
  Assert.ok(authInfo2, "Received callback from second proxy auth call.");
  let authInfo3 = await cbNoPrompt2.promise;
  Assert.ok(authInfo3, "Received callback from third proxy auth call.");

  BrowserTestUtils.removeTab(tabA);
  BrowserTestUtils.removeTab(tabB);
});
