/* 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 { logTest, logTask } = require("./utils/profiling");

const fs = require("fs");
const path = require("path");

async function waitForH3(maxRetries, browser, url, commands, context) {
  let attempt = 0;
  while (attempt < maxRetries) {
    await commands.navigate(url);

    // Get performance entries to check the protocol
    let protocolInfo = await commands.js.run(
      `
      // Get all performance entries
      const entries = performance.getEntries();

      // Create an array to store the results
      const protocolInfo = entries.map(entry => ({
          name: entry.name,
          protocol: entry.nextHopProtocol,
      }));

      return protocolInfo;
      `
    );

    // Log the protocol information
    context.log.info("protocolInfo: " + JSON.stringify(protocolInfo));

    // Check if the main document is using h3 protocol
    const normalizeUrl = url => url.replace(/\/+$/, "");
    const isH3 = protocolInfo.some(
      entry =>
        normalizeUrl(entry.name) === normalizeUrl(url) &&
        entry.protocol === "h3"
    );

    if (isH3) {
      context.log.info("Protocol h3 detected!");
      return; // Exit the function if h3 is found
    }

    // Increment the attempt counter and retry
    attempt++;
    context.log.info(
      `Retry attempt ${attempt} - Protocol is not h3. Retrying...`
    );
    if (browser === "firefox") {
      const script = `
      Services.obs.notifyObservers(null, "net:cancel-all-connections");
      Services.obs.notifyObservers(null, "network:reset-http3-excluded-list");
    `;
      commands.js.runPrivileged(script);
    }
    context.log.info("Waiting 3s to close connection...");
    await commands.wait.byTime(3000);
  }
}

async function waitForComplete(timeout, commands, context, id) {
  let starttime = performance.now();
  let status = "";
  let goodput = 0;

  while (performance.now() - starttime < timeout) {
    status = await commands.js.run(
      `return document.getElementById('${id}').innerHTML;`
    );

    if (status.startsWith("success")) {
      goodput = parseFloat(status.split(":")[1]);
      status = "success";
      break;
    } else if (status.startsWith("error")) {
      break;
    }

    await commands.wait.byTime(1000);
  }

  let endtime = performance.now();

  return {
    start: starttime,
    end: endtime,
    status,
    goodput,
  };
}

module.exports = logTest(
  "download/upload test",
  async function (context, commands) {
    let serverUrl = `${context.options.browsertime.server_url}`;
    let iterations = `${context.options.browsertime.iterations}`;
    const [protocol, testType] =
      `${context.options.browsertime.test_type}`.split("_");

    await commands.measure.start(serverUrl);
    let accumulatedResults = [];
    let browserName = "";
    let file_size = parseInt(context.options.browsertime.test_file_size, 10);
    if (Number.isNaN(file_size)) {
      // default is 32MB
      file_size = 32000000;
    }
    for (let iteration = 0; iteration < iterations; iteration++) {
      await logTask(context, "cycle " + iteration, async function () {
        const driver = context.selenium.driver;
        const webdriver = context.selenium.webdriver;
        let capabilities = await driver.getCapabilities();
        browserName = capabilities.get("browserName");

        if (protocol === "h3") {
          await waitForH3(10, browserName, serverUrl, commands, context);
        } else {
          await commands.navigate(serverUrl);
        }

        if (testType === "download") {
          const downloadItem = await driver.findElement(
            webdriver.By.id("downloadBtn")
          );

          const actions = driver.actions({ async: true });
          await actions.move({ origin: downloadItem }).click().perform();

          // Start the test and wait for the upload to complete
          let results = await waitForComplete(
            1200000,
            commands,
            context,
            "download_status"
          );
          let downloadTime = results.end - results.start;
          // Store result in megabit/seconds
          let downloadGoodput = (file_size * 8) / ((downloadTime / 1000) * 1e6);
          context.log.info(
            "download results: " +
              results.status +
              " duration: " +
              downloadTime +
              "ms, downloadGoodput: " +
              downloadGoodput +
              "Mbit/s"
          );
          accumulatedResults.push(downloadGoodput);
        } else if (testType === "upload") {
          const uploadItem = await driver.findElement(
            webdriver.By.id("fileUpload")
          );

          let tagName = await uploadItem.getTagName();
          if (tagName === "input") {
            if (context.options.browsertime.moz_fetch_dir == "None") {
              context.log.error(
                "This test depends on the fetch task. Download the file, 'https://github.com/mozilla/perf-automation/raw/master/test_files/upload-test-32MB.dat' and set the os environment variable MOZ_FETCHES_DIR to that directory."
              );
            }

            let localFilePath = path.join(
              `${context.options.browsertime.moz_fetch_dir}`,
              `${context.options.browsertime.test_file_name}`
            );
            if (!fs.existsSync(localFilePath)) {
              localFilePath = path.join(
                `${context.options.browsertime.moz_fetch_dir}`,
                "upload-test-32MB.dat"
              );
            }

            context.log.info("Sending file path: " + localFilePath);
            await uploadItem.sendKeys(localFilePath);
          } else {
            const actions = driver.actions({ async: true });
            await actions.move({ origin: uploadItem }).click().perform();
          }

          // Start the test and wait for the upload to complete
          let results = await waitForComplete(
            1200000,
            commands,
            context,
            "upload_status"
          );
          let uploadTime = results.end - results.start;

          // Store result in megabit/seconds
          let uploadGoodput = Number.isNaN(results.goodput)
            ? (file_size * 8) / ((uploadTime / 1000) * 1e6)
            : results.goodput;
          context.log.info(
            "upload results: " +
              results.status +
              " duration: " +
              uploadTime +
              "ms, uploadGoodput: " +
              uploadGoodput +
              "Mbit/s"
          );
          if (results.status === "success") {
            accumulatedResults.push(uploadGoodput);
          }
        } else {
          context.log.error("Unsupported test type:" + testType);
        }
      });

      if (browserName === "firefox" && protocol === "h3") {
        const statsScript = `
        const gDashboard = Cc["@mozilla.org/network/dashboard;1"].getService(
          Ci.nsIDashboard
        );
        let promise = new Promise((resolve, reject) => {
          gDashboard.requestHttp3ConnectionStats((data) => {
            resolve(data);
          });
        });
        let done = false;
        let result = null;
        let error = null;
        promise
          .catch(e => {
            error = e;
          })
          .then(r => {
            result = r;
            done = true;
          });
        // Spin the event loop until done becomes true.
        Services.tm.spinEventLoopUntil(
          "requestHttpConnections",
          () => done
        );
        return result;
        `;
        let res = await commands.js.runPrivileged(statsScript);
        context.log.info("HTTP/3 Connection Stats" + JSON.stringify(res));
      }

      // No need to close the connection at the last run.
      if (iteration != iterations - 1) {
        await commands.navigate("about:blank");
        if (browserName === "firefox") {
          const script = `
      Services.obs.notifyObservers(null, "net:cancel-all-connections");
    `;
          commands.js.runPrivileged(script);
          context.log.info("Waiting 3s to close connection...");
          await commands.wait.byTime(3000);
        } else {
          // TODO: configiure a shorter idle timeout at server side, so we
          // don't have to wait that long.
          context.log.info("Waiting 35s to close connection...");
          await commands.wait.byTime(35000);
        }
      }
    }

    if (testType === "download") {
      commands.measure.addObject({
        custom_data: { "download-goodput": accumulatedResults },
      });
    } else if (testType === "upload") {
      commands.measure.addObject({
        custom_data: { "upload-goodput": accumulatedResults },
      });
    }
  }
);
