/* 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";

/**
 * Tests that can show multiple auth prompts in different tabs and handle them
 * correctly.
 */

const ORIGIN1 = "https://example.com";
const ORIGIN2 = "https://example.org";

const AUTH_PATH =
  "/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs";

/**
 * Opens a tab and navigates to the auth test path.
 *
 * @param {string} origin - Origin to open with test path.
 * @param {object} authOptions - Authentication options to pass to server and
 * test for.
 * @param {string} authOptions.user - Expected username.
 * @param {string} authOptions.pass - Expected password.
 * @param {string} authOptions.realm - Realm to return on auth request.
 * @returns {object} - An object containing passed origin and authOptions,
 * opened tab, a promise which resolves once the tab loads, a promise which
 * resolves once the prompt has been opened.
 */
async function openTabWithAuthPrompt(origin, authOptions) {
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    "https://example.com"
  );

  let promptPromise = PromptTestUtils.waitForPrompt(tab.linkedBrowser, {
    modalType: Services.prompt.MODAL_TYPE_TAB,
    promptType: "promptUserAndPass",
  });
  let url = new URL(origin + AUTH_PATH);
  Object.entries(authOptions).forEach(([key, value]) =>
    url.searchParams.append(key, value)
  );
  let loadPromise = BrowserTestUtils.browserLoaded(
    tab.linkedBrowser,
    false,
    url.toString()
  );
  info("Loading " + url.toString());
  BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url.toString());
  return { origin, tab, authOptions, loadPromise, promptPromise };
}

/**
 * Waits for tab to load and tests for expected auth state.
 *
 * @param {boolean} expectAuthed - true: auth success, false otherwise.
 * @param {object} tabInfo - Information about the tab as generated by
 * openTabWithAuthPrompt.
 */
async function testTabAuthed(expectAuthed, { tab, loadPromise, authOptions }) {
  // Wait for tab to load after auth.
  await loadPromise;
  Assert.ok(true, "Tab loads after auth");

  // Fetch auth results from body (set by authenticate.sjs).
  let { loginResult, user, pass } = await SpecialPowers.spawn(
    tab.linkedBrowser,
    [],
    () => {
      let result = {};
      result.loginResult = content.document.getElementById("ok").innerText;
      result.user = content.document.getElementById("user").innerText;
      result.pass = content.document.getElementById("pass").innerText;
      return result;
    }
  );

  Assert.equal(
    loginResult == "PASS",
    expectAuthed,
    "Site has expected auth state"
  );
  Assert.equal(user, expectAuthed ? authOptions.user : "", "Sent correct user");
  Assert.equal(
    pass,
    expectAuthed ? authOptions.pass : "",
    "Sent correct password"
  );
}

add_task(async function test() {
  let tabA = await openTabWithAuthPrompt(ORIGIN1, {
    user: "userA",
    pass: "passA",
    realm: "realmA",
  });
  // Tab B and C share realm and credentials.
  // However, since the auth happens in separate tabs we should get two prompts.
  let tabB = await openTabWithAuthPrompt(ORIGIN2, {
    user: "userB",
    pass: "passB",
    realm: "realmB",
  });
  let tabC = await openTabWithAuthPrompt(ORIGIN2, {
    user: "userB",
    pass: "passB",
    realm: "realmB",
  });
  let tabs = [tabA, tabB, tabC];

  info(`Opening ${tabs.length} tabs with auth prompts`);
  let prompts = await Promise.all(tabs.map(tab => tab.promptPromise));

  Assert.equal(prompts.length, tabs.length, "Should have one prompt per tab");

  for (let i = 0; i < prompts.length; i++) {
    let titleEl = prompts[i].ui.prompt.document.querySelector("#titleText");
    Assert.equal(
      titleEl.textContent,
      new URL(tabs[i].origin).host,
      "Prompt matches the tab's host"
    );
  }

  // Interact with the prompts. This is deliberately done out of order
  // (no FIFO, LIFO).
  let [promptA, promptB, promptC] = prompts;

  // Accept prompt B with correct login details.
  await PromptTestUtils.handlePrompt(promptB, {
    loginInput: tabB.authOptions.user,
    passwordInput: tabB.authOptions.pass,
  });
  await testTabAuthed(true, tabB);

  // Accept prompt A with correct login details
  await PromptTestUtils.handlePrompt(promptA, {
    loginInput: tabA.authOptions.user,
    passwordInput: tabA.authOptions.pass,
  });
  await testTabAuthed(true, tabA);

  // Cancel prompt C
  await PromptTestUtils.handlePrompt(promptC, {
    buttonNumClick: 1,
  });
  await testTabAuthed(false, tabC);

  // Cleanup tabs
  tabs.forEach(({ tab }) => BrowserTestUtils.removeTab(tab));
});
