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

const fs = require("fs");
const http = require("http");
const { logTest, logTask } = require("./utils/profiling");

const URL = "/secrets/v1/secret/project/perftest/gecko/level-";
const SECRET = "/perftest-login";
const DEFAULT_SERVER = "https://firefox-ci-tc.services.mozilla.com";

const SCM_LOGIN_SITES = ["facebook", "netflix"];

/**
 * This function obtains the perftest secret from Taskcluster.
 *
 * It will NOT work locally. Please see the get_logins function, you
 * will need to define a JSON file and set the RAPTOR_LOGINS
 * env variable to its path.
 */
async function get_tc_secrets(context) {
  const MOZ_AUTOMATION = process.env.MOZ_AUTOMATION;
  if (!MOZ_AUTOMATION) {
    throw Error(
      "Not running in CI. Set RAPTOR_LOGINS to a JSON file containing the logins."
    );
  }

  let TASKCLUSTER_PROXY_URL = process.env.TASKCLUSTER_PROXY_URL
    ? process.env.TASKCLUSTER_PROXY_URL
    : DEFAULT_SERVER;

  let MOZ_SCM_LEVEL = process.env.MOZ_SCM_LEVEL ? process.env.MOZ_SCM_LEVEL : 1;

  const url = TASKCLUSTER_PROXY_URL + URL + MOZ_SCM_LEVEL + SECRET;

  const data = await new Promise((resolve, reject) => {
    context.log.info("Obtaining secrets for login...");

    http.get(
      url,
      {
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
        },
      },
      res => {
        let data = "";
        context.log.info(`Secret status code: ${res.statusCode}`);

        res.on("data", d => {
          data += d.toString();
        });

        res.on("end", () => {
          resolve(data);
        });

        res.on("error", error => {
          context.log.error(error);
          reject(error);
        });
      }
    );
  });

  return JSON.parse(data);
}

/**
 * This function gets the login information required.
 *
 * It starts by looking for a local file whose path is defined
 * within RAPTOR_LOGINS. If we don't find this file, then we'll
 * attempt to get the login information from our Taskcluster secret.
 * If MOZ_AUTOMATION is undefined, then the test will fail, Taskcluster
 * secrets can only be obtained in CI.
 */
async function get_logins(context) {
  let logins;

  let RAPTOR_LOGINS = process.env.RAPTOR_LOGINS;
  if (RAPTOR_LOGINS) {
    // Get logins from a local file
    if (!RAPTOR_LOGINS.endsWith(".json")) {
      throw Error(
        `File given for logins does not end in '.json': ${RAPTOR_LOGINS}`
      );
    }

    let logins_file = null;
    try {
      logins_file = await fs.readFileSync(RAPTOR_LOGINS, "utf8");
    } catch (err) {
      throw Error(`Failed to read the file ${RAPTOR_LOGINS}: ${err}`);
    }

    logins = await JSON.parse(logins_file);
  } else {
    // Get logins from a perftest Taskcluster secret
    logins = await get_tc_secrets(context);
  }

  return logins;
}

/**
 * This function returns the type of login to do.
 *
 * This function returns "single-form" when we find a single form. If we only
 * find a single input field, we assume that there is one page per input
 * and return "multi-page". Otherwise, we return null.
 */
async function get_login_type(context, commands) {
  /*
    Determine if there's a password field visible with this
    query selector. Some sites use `tabIndex` to hide the password
    field behind other elements. In this case, we are searching
    for any password-type field that has a tabIndex of 0 or undefined and
    is not hidden.
  */
  let input_length = await commands.js.run(`
    return document.querySelectorAll(
      "input[type=password][tabIndex='0']:not([type=hidden])," +
      "input[type=password]:not([tabIndex]):not([type=hidden])"
    ).length;
  `);

  if (input_length == 0) {
    context.log.info("Found a multi-page login");
    return multi_page_login;
  } else if (input_length == 1) {
    context.log.info("Found a single-page login");
    return single_page_login;
  }

  if (
    (await commands.js.run(
      `return document.querySelectorAll("form").length;`
    )) >= 1
  ) {
    context.log.info("Found a single-form login");
    return single_form_login;
  }

  return null;
}

/**
 * This function sets up the login for a single form.
 *
 * The username field is defined as the field which immediately precedes
 * the password field. We have to do this in two steps because we need
 * to make sure that the event we emit from the change has the `isTrusted`
 * field set to `true`. Otherwise, some websites will ignore the input and
 * the form submission.
 */
async function single_page_login(login_info, context, commands, prefix = "") {
  // Get the first input field in the form that is not hidden and add the
  // username. Assumes that email/username is always the first input field.
  await commands.addText.bySelector(
    login_info.username,
    `${prefix}input:not([type=hidden]):not([type=password])`
  );

  // Get the password field and ensure it's not hidden.
  await commands.addText.bySelector(
    login_info.password,
    `${prefix}input[type=password]:not([type=hidden])`
  );

  return undefined;
}

/**
 * See single_page_login.
 */
async function single_form_login(login_info, context, commands) {
  return single_page_login(login_info, context, commands, "form ");
}

/**
 * Login to a website that uses multiple pages for the login.
 *
 * WARNING: Assumes that the first page is for the username.
 */
// TODO cleanup comments
async function multi_page_login(login_info, context, commands) {
  const driver = context.selenium.driver;
  const webdriver = context.selenium.webdriver;
  //TODO fails here in netflix for Try...
  const username_field = await driver.findElement(
    webdriver.By.css(`input:not([type=hidden]):not([type=password])`)
  );
  await username_field.sendKeys(login_info.username);
  await username_field.sendKeys(webdriver.Key.ENTER);
  await commands.wait.byTime(5000);
  let password_field;
  try {
    password_field = await driver.findElement(
      webdriver.By.css(`input[type=password]:not([type=hidden])`)
    );
  } catch (err) {
    if (err.toString().includes("NoSuchElementError")) {
      // Sometimes we're suspicious (i.e. they think we're a bot/bad-actor)
      let name_field = await driver.findElement(
        webdriver.By.css(`input:not([type=hidden]):not([type=password])`)
      );
      await name_field.sendKeys(login_info.suspicious_answer);
      await name_field.sendKeys(webdriver.Key.ENTER);
      await commands.wait.byTime(5000);

      // Try getting the password field again
      password_field = await driver.findElement(
        webdriver.By.css(`input[type=password]:not([type=hidden])`)
      );
    } else {
      throw err;
    }
  }

  await password_field.sendKeys(login_info.password);

  return async function () {
    password_field.sendKeys(webdriver.Key.ENTER);
    await commands.wait.byTime(5000);
  };
}

/**
 * This function sets up the login.
 *
 * This is done by first the login type, and then performing the
 * actual login setup. The return is a possible button to click
 * to perform the login.
 */
async function setup_login(login_info, context, commands) {
  let login_func = await get_login_type(context, commands);
  if (!login_func) {
    throw Error("Could not determine the type of login page.");
  }

  try {
    return await login_func(login_info, context, commands);
  } catch (err) {
    throw Error(`Could not setup login information: ${err}`);
  }
}

/**
 * This function performs the login.
 *
 * It does this by either clicking on a button with a type
 * of "sumbit", or running a final_button function that was
 * obtained from the setup_login function. Some pages also ask
 * questions about setting up 2FA or other information. Generally,
 * these contain the "skip" text.
 */
async function login(context, commands, final_button) {
  try {
    if (!final_button) {
      // The mouse double click emits an event with `evt.isTrusted=true`
      await commands.mouse.doubleClick.bySelector("button[type=submit]");
      await commands.wait.byTime(10000);
    } else {
      // In some cases, it's preferable to be given a function for the final button
      await final_button();
    }

    // Some pages ask to setup 2FA, skip this based on the text
    const XPATHS = [
      "//a[contains(text(), 'skip')]",
      "//button[contains(text(), 'skip')]",
      "//input[contains(text(), 'skip')]",
      "//div[contains(text(), 'skip')]",
    ];

    for (let xpath of XPATHS) {
      try {
        await commands.mouse.doubleClick.byXpath(xpath);
      } catch (err) {
        if (err.toString().includes("not double click")) {
          context.log.info(`Can't find a button with the text: ${xpath}`);
        } else {
          throw err;
        }
      }
    }
  } catch (err) {
    throw Error(
      `Could not login to website as we could not find the submit button/input: ${err}`
    );
  }
}

/**
 * Grab the base URL from the browsertime url.
 *
 * This is a necessary step for getting the login values from the Taskcluster
 * secrets, which are hashed by the base URL.
 *
 * The first entry is the protocal, third is the top-level domain (or host)
 */
function get_base_URL(fullUrl) {
  let pathAsArray = fullUrl.split("/");
  return pathAsArray[0] + "//" + pathAsArray[2];
}

/**
 * This function attempts the login-login sequence for a live pageload test
 */
async function perform_live_login(context, commands) {
  let testUrl = context.options.browsertime.url;

  let logins = await get_logins(context);
  const baseUrl = get_base_URL(testUrl);

  await commands.navigate("about:blank");

  let login_info = logins.secret[baseUrl];
  try {
    await commands.navigate(login_info.login_url);
  } catch (err) {
    context.log.info("Unable to acquire login information");
    throw err;
  }
  await commands.wait.byTime(10000);

  let final_button = await setup_login(login_info, context, commands);
  await login(context, commands, final_button);
}

module.exports = logTest(
  "browsertime pageload",
  async function (context, commands) {
    context.log.info("Starting a browsertime pageload");
    let test_url = context.options.browsertime.url;
    let secondary_url = context.options.browsertime.secondary_url;
    let page_cycles = context.options.browsertime.page_cycles;
    let page_cycle_delay = context.options.browsertime.page_cycle_delay;
    let post_startup_delay = context.options.browsertime.post_startup_delay;
    let chimera_mode = context.options.browsertime.chimera;
    let test_bytecode_cache = context.options.browsertime.test_bytecode_cache;
    let login_required = context.options.browsertime.loginRequired;
    let live_site = context.options.browsertime.liveSite;
    let test_name = context.options.browsertime.testName;

    context.log.info(
      "Waiting for %d ms (post_startup_delay)",
      post_startup_delay
    );
    await commands.wait.byTime(post_startup_delay);
    let cached = false;

    // Login once before testing the test_url/secondary_url cycles
    // If the user has RAPTOR_LOGINS configured correctly, a local login pageload
    // test can be attempted. Otherwise if attempting it in CI, only sites with the
    // associated MOZ_SCM_LEVEL will be attempted (e.g. Try = 1, autoland = 3).
    // In addition, ensure login sequence is only attempted on live sites and for sites
    // that we have Taskcluster secrets for.
    if (
      login_required == "True" &&
      live_site == "True" &&
      SCM_LOGIN_SITES.includes(test_name)
    ) {
      await perform_live_login(context, commands);
    }

    for (let count = 0; count < page_cycles; count++) {
      await logTask(context, "cycle " + count, async function () {
        if (count !== 0 && secondary_url !== undefined) {
          context.log.info("Navigating to secondary url:" + secondary_url);
          await commands.navigate(secondary_url);
          await commands.wait.byTime(1000);
          await commands.js.runAndWait(`
        (function() {
          const white = document.createElement('div');
          white.id = 'raptor-white';
          white.style.position = 'absolute';
          white.style.top = '0';
          white.style.left = '0';
          white.style.width = Math.max(document.documentElement.clientWidth, document.body.clientWidth) + 'px';
          white.style.height = Math.max(document.documentElement.clientHeight,document.body.clientHeight) + 'px';
          white.style.backgroundColor = 'white';
          white.style.zIndex = '2147483647';
          document.body.appendChild(white);
          document.body.style.display = '';
        })();`);
          await commands.wait.byTime(1000);
        } else {
          context.log.info("Navigating to about:blank, count: " + count);
          await commands.navigate("about:blank");
        }

        context.log.info("Navigating to primary url:" + test_url);
        context.log.info(
          "Cycle %d, waiting for %d ms",
          count,
          page_cycle_delay
        );
        await commands.wait.byTime(page_cycle_delay);

        context.log.info("Cycle %d, starting the measure", count);
        await commands.measure.start(test_url);

        // Wait 20 seconds to populate bytecode cache
        if (
          test_bytecode_cache == "true" &&
          chimera_mode == "true" &&
          !cached
        ) {
          context.log.info("Waiting 20s to populate bytecode cache...");
          await commands.wait.byTime(20000);
          cached = true;
        }
      });
    }

    context.log.info("Browsertime pageload ended.");
    return true;
  }
);
