
// Displays an error message to the UI. Any previous message will be erased.
function displayError(message) {
  // Clear the body of all children.
  while (document.body.firstChild) {
    document.body.removeChild(document.body.firstChild);
  }
  const element = document.createElement("p");
  element.innerText = "Error: " + message;
  element.style.color = "red";
  element.style.fontFamily = "Arial";
  element.style.fontWeight = "bold";
  element.style.margin = "5rem";
  document.body.appendChild(element);
}

// Reads `response` into an array of lines while calling `updateProgress` in between.
async function getBodyLinesWithProgress(response, updateProgress) {
  const utf8Decoder = new TextDecoder('utf-8');
  const reader = response.body.getReader();

  const lines = [];
  let pendingText = "";
  let readerDone = false;
  while (!readerDone) {
    // Read a chunk.
    const { value: chunk, done } = await reader.read();
    readerDone = done;
    if (!chunk) {
      continue;
    }
    // Notify the listener of progress.
    updateProgress(chunk.length);
    const decodedChunk = utf8Decoder.decode(chunk);

    const sublines = decodedChunk.split('\n');
    for (let i = 0; i < sublines.length - 1; i++) {
      const fullLine = pendingText + sublines[i];
      pendingText = "";
      if (fullLine !== "") {
        lines.push(fullLine);
      }
    }
    pendingText = sublines[sublines.length - 1];
  }

  // If there is any text remaining, append it.
  if (pendingText !== "") {
    lines.push(pendingText);
  }
  return lines;
}

// Determines whether `str` matches at least one value in `enumObject`.
function isValidEnumValue(enumObject, str) {
  for (const enumKey in enumObject) {
    if (enumObject[enumKey] === str) {
      return true;
    }
  }
  return false;
}

// Enum for test status.
const testStatus = {
  PASSED: "Passed",
  FAILED: "Failed",
  SKIPPED: "Skipped"
}

async function loadTestData(period) {
  const file = period === "last90" ? "data-last-90.csv" : "data.csv";
  const response = await fetch(file, {
    headers: {
      "Cache-Control": "max-age=3600,must-revalidate",
    }
  });
  if (!response.ok) {
    const responseText = await response.text();
    throw `Failed to fetch data from GCS bucket. Error: ${responseText}`;
  }

  const responseDate = new Date(response.headers.get("date").toString());

  const box = document.createElement("div");
  box.style.width = "100%";
  const innerBox = document.createElement("div");
  innerBox.style.margin = "5rem";
  box.appendChild(innerBox);
  const progressBarPrompt = document.createElement("h1");
  progressBarPrompt.style.fontFamily = "Arial";
  progressBarPrompt.style.textAlign = "center";
  progressBarPrompt.innerText = "Downloading data...";
  innerBox.appendChild(progressBarPrompt);
  const progressBar = document.createElement("progress");
  progressBar.setAttribute("max", Number(response.headers.get('Content-Length')));
  progressBar.style.width = "100%";
  innerBox.appendChild(progressBar);
  document.body.appendChild(box);

  let readBytes = 0;
  const lines = await getBodyLinesWithProgress(response, value => {
    readBytes += value;
    progressBar.setAttribute("value", readBytes);
  });
  // Consume the header to ensure the data has the right number of fields.
  const header = lines[0];
  if (header.split(",").length != 9) {
    document.body.removeChild(box);
    throw `Fetched CSV data contains wrong number of fields. Expected: 9. Actual Header: "${header}"`;
  }

  progressBarPrompt.textContent = "Parsing data...";
  progressBar.setAttribute("max", lines.length);

  const testData = [];
  let lineData = ["", "", "", "", "", "", "", "", ""];
  for (let i = 1; i < lines.length; i++) {
    if (i % 30000 === 0) {
      await new Promise(resolve => {
        setTimeout(() => {
          progressBar.setAttribute("value", i);
          resolve();
        });
      });
    }
    const line = lines[i];
    let splitLine = line.split(",");
    if (splitLine.length != 9) {
      console.warn(`Found line with wrong number of fields. Actual: ${splitLine.length} Expected: 9. Line: "${line}"`);
      continue;
    }
    splitLine = splitLine.map((value, index) => value === "" ? lineData[index] : value);
    lineData = splitLine;
    if (!isValidEnumValue(testStatus, splitLine[4])) {
      console.warn(`Invalid test status provided. Actual: ${splitLine[4]} Expected: One of ${Object.values(testStatus).join(", ")}`);
      continue;
    }
    // Skip unsafe dates.
    if (splitLine[1] === "0001-01-01") {
      continue;
    }
    testData.push({
      commit: splitLine[0],
      date: new Date(splitLine[1]),
      environment: splitLine[2],
      name: splitLine[3],
      status: splitLine[4],
      duration: Number(splitLine[5]),
      rootJob: splitLine[6],
      testCount: Number(splitLine[7]),
      totalDuration: Number(splitLine[8]),
    });
  }
  document.body.removeChild(box);
  if (testData.length == 0) {
    throw "Fetched CSV data is empty or poorly formatted.";
  }
  return [testData, responseDate];
}

Array.prototype.min = function() {
  return this.reduce((acc, val) => Math.min(acc, val), Number.MAX_VALUE)
}

Array.prototype.max = function() {
  return this.reduce((acc, val) => Math.max(acc, val), -Number.MAX_VALUE)
}

Array.prototype.sum = function() {
  return this.reduce((sum, value) => sum + value, 0);
};

// Computes the average of an array of numbers.
Array.prototype.average = function () {
  return this.length === 0 ? 0 : (this.sum() / this.length);
};

// Groups array elements by keys obtained through `keyGetter`.
Array.prototype.groupBy = function (keyGetter) {
  return Array.from(this.reduce((mapCollection, element) => {
    const key = keyGetter(element);
    if (mapCollection.has(key)) {
      mapCollection.get(key).push(element);
    } else {
      mapCollection.set(key, [element]);
    }
    return mapCollection;
  }, new Map()).values());
};

Array.prototype.oneOfEach = function (keyGetter) {
  return Array.from(new Map(this.map(value => [ keyGetter(value), value ])).values());
}

// Parse URL search `query` into [{key, value}].
function parseUrlQuery(query) {
  if (query[0] === '?') {
    query = query.substring(1);
  }
  return Object.fromEntries((query === "" ? [] : query.split("&")).map(element => {
    const keyValue = element.split("=");
    return [unescape(keyValue[0]), unescape(keyValue[1])];
  }));
}

// Takes a set of test runs (all of the same test), and aggregates them into one element per date.
function aggregateRuns(testRuns) {
  return testRuns
    // Group runs by the date it ran.
    .groupBy(run => run.date.getTime())
    // Sort by run date, past to future.
    .sort((a, b) => a[0].date - b[0].date)
    // Map each group to all variables need to format the rows.
    .map(tests => ({
      date: tests[0].date, // Get one of the dates from the tests (which will all be the same).
      flakeRate: tests.map(test => test.status === testStatus.FAILED ? 100 : 0).average(), // Compute average of runs where FAILED counts as 100%.
      duration: tests.map(test => test.duration).average(), // Compute average duration of runs.
      jobs: tests.map(test => ({ // Take all job ids, statuses, and durations of tests in this group.
        id: test.rootJob,
        status: test.status,
        duration: test.duration
      }))
    }));
}

// Takes a set of test runs (all of the same test), and aggregates them into one element per week.
function aggregateWeeklyRuns(testRuns, weekDates) {
  return testRuns
    // Group runs by the date it ran.
    .groupBy(run => weekDates.findRounded(run.date).getTime())
    // Sort by run date, past to future.
    .sort((a, b) => weekDates.findRounded(a[0].date) - weekDates.findRounded(b[0].date))
    // Map each group to all variables need to format the rows.
    .map(tests => ({
      date: weekDates.findRounded(tests[0].date), // Get one of the dates from the tests, and use it to get the rounded time (which will all be the same).
      flakeRate: tests.map(test => test.status === testStatus.FAILED ? 100 : 0).average(), // Compute average of runs where FAILED counts as 100%.
      duration: tests.map(test => test.duration).average(), // Compute average duration of runs.
      jobs: tests.map(test => ({ // Take all job ids, statuses, and durations of tests in this group.
        id: test.rootJob,
        status: test.status,
        duration: test.duration
      }))
    }));
}

const testGopoghLink = (jobId, environment, testName) => `https://storage.googleapis.com/minikube-builds/logs/master/${jobId}/${environment}.html${testName ? `#fail_${testName}` : ``}`;

function displayTestAndEnvironmentChart(testData, testName, environmentName) {
  const testRuns = testData
    // Filter to only contain unskipped runs of the requested test and requested environment.
    .filter(test => test.name === testName && test.environment === environmentName && test.status !== testStatus.SKIPPED);
  const chartsContainer = document.getElementById('chart_div');
  {
    const data = new google.visualization.DataTable();
    data.addColumn('date', 'Date');
    data.addColumn('number', 'Flake Percentage');
    data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });
    data.addColumn('number', 'Duration');
    data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });

    data.addRows(
      aggregateRuns(testRuns)
        .map(groupData => [
          groupData.date,
          groupData.flakeRate,
          `<div style="padding: 1rem; font-family: 'Arial'; font-size: 14">
            <b>Date:</b> ${groupData.date.toLocaleString([], {dateStyle: 'medium'})}<br>
            <b>Flake Percentage:</b> ${groupData.flakeRate.toFixed(2)}%<br>
            <b>Jobs:</b><br>
            ${groupData.jobs.map(({ id, status }) => `  - <a href="${testGopoghLink(id, environmentName, testName)}">${id}</a> (${status})`).join("<br>")}
          </div>`,
          groupData.duration,
          `<div style="padding: 1rem; font-family: 'Arial'; font-size: 14">
            <b>Date:</b> ${groupData.date.toLocaleString([], {dateStyle: 'medium'})}<br>
            <b>Average Duration:</b> ${groupData.duration.toFixed(2)}s<br>
            <b>Jobs:</b><br>
            ${groupData.jobs.map(({ id, duration }) => `  - <a href="${testGopoghLink(id, environmentName, testName)}">${id}</a> (${duration}s)`).join("<br>")}
          </div>`,
        ])
    );

    const options = {
      title: `Flake rate and duration by day of ${testName} on ${environmentName}`,
      width: window.innerWidth,
      height: window.innerHeight,
      pointSize: 10,
      pointShape: "circle",
      series: {
        0: { targetAxisIndex: 0 },
        1: { targetAxisIndex: 1 },
      },
      vAxes: {
        0: { title: "Flake rate", minValue: 0, maxValue: 100 },
        1: { title: "Duration (seconds)" },
      },
      colors: ['#dc3912', '#3366cc'],
      tooltip: { trigger: "selection", isHtml: true }
    };
    const flakeRateContainer = document.createElement("div");
    flakeRateContainer.style.width = "100vw";
    flakeRateContainer.style.height = "100vh";
    chartsContainer.appendChild(flakeRateContainer);
    const chart = new google.visualization.LineChart(flakeRateContainer);
    chart.draw(data, options);
  }
  {
    const dates = testRuns.map(run => run.date.getTime());
    const startDate = new Date(dates.min());
    const endDate = new Date(dates.max());
  
    const weekDates = [];
    let currentDate = startDate;
    while (currentDate < endDate) {
      weekDates.push(currentDate);
      currentDate = new Date(currentDate);
      currentDate.setDate(currentDate.getDate() + 7);
    }
    weekDates.push(currentDate);
    weekDates.findRounded = function (value) {
      let index = this.findIndex(v => value < v);
      if (index == 0) {
        return this[index];
      }
      if (index < 0) {
        index = this.length;
      }
      return this[index - 1];
    }

    const data = new google.visualization.DataTable();
    data.addColumn('date', 'Date');
    data.addColumn('number', 'Flake Percentage');
    data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });
    data.addColumn('number', 'Duration');
    data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });

    data.addRows(
      aggregateWeeklyRuns(testRuns, weekDates)
        .map(groupData => [
          groupData.date,
          groupData.flakeRate,
          `<div style="padding: 1rem; font-family: 'Arial'; font-size: 14">
            <b>Date:</b> ${groupData.date.toLocaleString([], {dateStyle: 'medium'})}<br>
            <b>Flake Percentage:</b> ${groupData.flakeRate.toFixed(2)}%<br>
            <b>Jobs:</b><br>
            ${groupData.jobs.map(({ id, status }) => `  - <a href="${testGopoghLink(id, environmentName, testName)}">${id}</a> (${status})`).join("<br>")}
          </div>`,
          groupData.duration,
          `<div style="padding: 1rem; font-family: 'Arial'; font-size: 14">
            <b>Date:</b> ${groupData.date.toLocaleString([], {dateStyle: 'medium'})}<br>
            <b>Average Duration:</b> ${groupData.duration.toFixed(2)}s<br>
            <b>Jobs:</b><br>
            ${groupData.jobs.map(({ id, duration }) => `  - <a href="${testGopoghLink(id, environmentName, testName)}">${id}</a> (${duration}s)`).join("<br>")}
          </div>`,
        ])
    );

    const options = {
      title: `Flake rate and duration by week of ${testName} on ${environmentName}`,
      width: window.innerWidth,
      height: window.innerHeight,
      pointSize: 10,
      pointShape: "circle",
      series: {
        0: { targetAxisIndex: 0 },
        1: { targetAxisIndex: 1 },
      },
      vAxes: {
        0: { title: "Flake rate", minValue: 0, maxValue: 100 },
        1: { title: "Duration (seconds)" },
      },
      colors: ['#dc3912', '#3366cc'],
      tooltip: { trigger: "selection", isHtml: true }
    };
    const flakeRateContainer = document.createElement("div");
    flakeRateContainer.style.width = "100vw";
    flakeRateContainer.style.height = "100vh";
    chartsContainer.appendChild(flakeRateContainer);
    const chart = new google.visualization.LineChart(flakeRateContainer);
    chart.draw(data, options);
  }
}

function createRecentFlakePercentageTable(recentFlakePercentage, previousFlakePercentageMap, environmentName, period) {
  const createCell = (elementType, text) => {
    const element = document.createElement(elementType);
    element.innerHTML = text;
    return element;
  }

  const table = document.createElement("table");
  const tableHeaderRow = document.createElement("tr");
  tableHeaderRow.appendChild(createCell("th", "Rank"));
  tableHeaderRow.appendChild(createCell("th", "Test Name")).style.textAlign = "left";
  tableHeaderRow.appendChild(createCell("th", "Recent Flake Percentage"));
  tableHeaderRow.appendChild(createCell("th", "Growth (since last 15 days)"));
  table.appendChild(tableHeaderRow);
  const tableBody = document.createElement("tbody");
  for (let i = 0; i < recentFlakePercentage.length; i++) {
    const {testName, flakeRate} = recentFlakePercentage[i];
    const row = document.createElement("tr");
    row.appendChild(createCell("td", "" + (i + 1))).style.textAlign = "center";
    row.appendChild(createCell("td", `<a href="${window.location.pathname}?env=${environmentName}&test=${testName}${period === 'last90' ? '&period=last90' : ''}">${testName}</a>`));
    row.appendChild(createCell("td", `${flakeRate.toFixed(2)}%`)).style.textAlign = "right";
    const growth = previousFlakePercentageMap.has(testName) ?
      flakeRate - previousFlakePercentageMap.get(testName) : 0;
    row.appendChild(createCell("td", `<span style="color: ${growth === 0 ? "black" : (growth > 0 ? "red" : "green")}">${growth > 0 ? '+' + growth.toFixed(2) : growth.toFixed(2)}%</span>`));
    tableBody.appendChild(row);
  }
  table.appendChild(tableBody);
  new Tablesort(table);
  return table;
}

function displayEnvironmentChart(testData, environmentName, period) {
  // Number of days to use to look for "flaky-est" tests.
  const dateRange = 15;
  // Number of tests to display in chart.
  const topFlakes = 10;

  testData = testData
    // Filter to only contain unskipped runs of the requested environment.
    .filter(test => test.environment === environmentName && test.status !== testStatus.SKIPPED);

  const testRuns = testData
    .groupBy(test => test.name);

  const aggregatedRuns = new Map(testRuns.map(test => [
    test[0].name,
    new Map(aggregateRuns(test)
      .map(runDate => [ runDate.date.getTime(), runDate ]))]));
  const uniqueDates = new Set();
  for (const [_, runDateMap] of aggregatedRuns) {
    for (const [dateTime, _] of runDateMap) {
      uniqueDates.add(dateTime);
    }
  }
  const orderedDates = Array.from(uniqueDates).sort();
  const recentDates = orderedDates.slice(-dateRange);
  const previousDates = orderedDates.slice(-2 * dateRange, -dateRange);

  const computeFlakePercentage = (runs, dates) => Array.from(runs).map(([testName, data]) => {
    const {flakeCount, totalCount} = dates.map(date => {
      const dateInfo = data.get(date);
      return dateInfo === undefined ? null : {
        flakeRate: dateInfo.flakeRate,
        runs: dateInfo.jobs.length
      };
    }).filter(dateInfo => dateInfo != null)
      .reduce(({flakeCount, totalCount}, {flakeRate, runs}) => ({
        flakeCount: flakeRate * runs + flakeCount,
        totalCount: runs + totalCount
      }), {flakeCount: 0, totalCount: 0});
    return {
      testName,
      flakeRate: totalCount === 0 ? 0 : flakeCount / totalCount,
    };
  });
  
  const recentFlakePercentage = computeFlakePercentage(aggregatedRuns, recentDates)
    .sort((a, b) => b.flakeRate - a.flakeRate);
  const previousFlakePercentageMap = new Map(
    computeFlakePercentage(aggregatedRuns, previousDates)
      .map(({testName, flakeRate}) => [testName, flakeRate]));

  const recentTopFlakes = recentFlakePercentage
    .slice(0, topFlakes)
    .map(({testName}) => testName);

  const chartsContainer = document.getElementById('chart_div');
  {
    const data = new google.visualization.DataTable();
    data.addColumn('date', 'Date');
    for (const name of recentTopFlakes) {
      data.addColumn('number', `Flake Percentage - ${name}`);
      data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });
    }
    data.addRows(
      orderedDates.map(dateTime => [new Date(dateTime)].concat(recentTopFlakes.map(name => {
        const data = aggregatedRuns.get(name).get(dateTime);
        return data !== undefined ? [
          data.flakeRate,
          `<div style="padding: 1rem; font-family: 'Arial'; font-size: 14">
            <b style="display: block">${name}</b><br>
            <b>Date:</b> ${data.date.toLocaleString([], {dateStyle: 'medium'})}<br>
            <b>Flake Percentage:</b> ${data.flakeRate.toFixed(2)}%<br>
            <b>Jobs:</b><br>
            ${data.jobs.map(({ id, status }) => `  - <a href="${testGopoghLink(id, environmentName, name)}">${id}</a> (${status})`).join("<br>")}
          </div>`
        ] : [null, null];
      })).flat())
    );
    const options = {
      title: `Flake rate by day of top ${topFlakes} of recent test flakiness (past ${dateRange} days) on ${environmentName}`,
      width: window.innerWidth,
      height: window.innerHeight,
      pointSize: 10,
      pointShape: "circle",
      vAxes: {
        0: { title: "Flake rate", minValue: 0, maxValue: 100 },
      },
      tooltip: { trigger: "selection", isHtml: true }
    };
    const flakeRateContainer = document.createElement("div");
    flakeRateContainer.style.width = "100vw";
    flakeRateContainer.style.height = "100vh";
    chartsContainer.appendChild(flakeRateContainer);
    const chart = new google.visualization.LineChart(flakeRateContainer);
    chart.draw(data, options);
  }
  {
    const dates = testData.map(run => run.date.getTime());
    const startDate = new Date(dates.min());
    const endDate = new Date(dates.max());
  
    const weekDates = [];
    let currentDate = startDate;
    while (currentDate < endDate) {
      weekDates.push(currentDate);
      currentDate = new Date(currentDate);
      currentDate.setDate(currentDate.getDate() + 7);
    }
    weekDates.push(currentDate);
    weekDates.findRounded = function (value) {
      let index = this.findIndex(v => value < v);
      if (index == 0) {
        return this[index];
      }
      if (index < 0) {
        index = this.length;
      }
      return this[index - 1];
    }
    const aggregatedWeeklyRuns = new Map(testRuns.map(test => [
      test[0].name,
      new Map(aggregateWeeklyRuns(test, weekDates)
        .map(runDate => [ weekDates.findRounded(runDate.date).getTime(), runDate ]))]));
    const uniqueWeekDates = new Set();
    for (const [_, runDateMap] of aggregatedWeeklyRuns) {
      for (const [dateTime, _] of runDateMap) {
        uniqueWeekDates.add(dateTime);
      }
    }
    const orderedWeekDates = Array.from(uniqueWeekDates).sort();
    const recentWeeklyTopFlakes = computeFlakePercentage(aggregatedWeeklyRuns, [orderedWeekDates[orderedWeekDates.length - 1]])
      .sort((a, b) => b.flakeRate - a.flakeRate)
      .slice(0, topFlakes)
      .map(({testName}) => testName);
    const data = new google.visualization.DataTable();
    data.addColumn('date', 'Date');
    for (const name of recentWeeklyTopFlakes) {
      data.addColumn('number', `Flake Percentage - ${name}`);
      data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });
    }
    data.addRows(
      orderedWeekDates.map(dateTime => [new Date(dateTime)].concat(recentTopFlakes.map(name => {
        const data = aggregatedWeeklyRuns.get(name).get(dateTime);
        return data !== undefined ? [
          data.flakeRate,
          `<div style="padding: 1rem; font-family: 'Arial'; font-size: 14">
            <b style="display: block">${name}</b><br>
            <b>Date:</b> ${data.date.toLocaleString([], {dateStyle: 'medium'})}<br>
            <b>Flake Percentage:</b> ${data.flakeRate.toFixed(2)}%<br>
            <b>Jobs:</b><br>
            ${data.jobs.map(({ id, status }) => `  - <a href="${testGopoghLink(id, environmentName, name)}">${id}</a> (${status})`).join("<br>")}
          </div>`
        ] : [null, null];
      })).flat())
    );
    const options = {
      title: `Flake rate by week of top ${topFlakes} of recent test flakiness (past week) on ${environmentName}`,
      width: window.innerWidth,
      height: window.innerHeight,
      pointSize: 10,
      pointShape: "circle",
      vAxes: {
        0: { title: "Flake rate", minValue: 0, maxValue: 100 },
      },
      tooltip: { trigger: "selection", isHtml: true }
    };
    const flakeRateContainer = document.createElement("div");
    flakeRateContainer.style.width = "100vw";
    flakeRateContainer.style.height = "100vh";
    chartsContainer.appendChild(flakeRateContainer);
    const chart = new google.visualization.LineChart(flakeRateContainer);
    chart.draw(data, options);
  }
  {
    const jobData = 
      testData
        .groupBy(run => run.date.getTime())
        .map(runDate => ({
          date: runDate[0].date,
          runInfo: runDate
            .oneOfEach(run => run.rootJob)
            .map(run => ({
              commit: run.commit,
              rootJob: run.rootJob,
              testCount: run.testCount,
              totalDuration: run.totalDuration
            }))
        }))
        .sort((a, b) => a.date - b.date)
        .map(({date, runInfo}) => ({
          date,
          runInfo,
          testCount: runInfo.map(job => job.testCount).average(),
          totalDuration: runInfo.map(job => job.totalDuration).average(),
        }));

    const data = new google.visualization.DataTable();
    data.addColumn('date', 'Date');
    data.addColumn('number', 'Test Count');
    data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });
    data.addColumn('number', 'Duration');
    data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });
    data.addRows(
      jobData.map(dateInfo => [
        dateInfo.date,
        dateInfo.testCount,
        `<div style="padding: 1rem; font-family: 'Arial'; font-size: 14">
          <b>Date:</b> ${dateInfo.date.toLocaleString([], {dateStyle: 'medium'})}<br>
          <b>Test Count (averaged): </b> ${+dateInfo.testCount.toFixed(2)}<br>
          <b>Jobs:</b><br>
          ${dateInfo.runInfo.map(job => `  - <a href="${testGopoghLink(job.rootJob, environmentName)}">${job.rootJob}</a> Test count: ${job.testCount}`).join("<br>")}
        </div>`,
        dateInfo.totalDuration,
        `<div style="padding: 1rem; font-family: 'Arial'; font-size: 14">
          <b>Date:</b> ${dateInfo.date.toLocaleString([], {dateStyle: 'medium'})}<br>
          <b>Total Duration (averaged): </b> ${+dateInfo.totalDuration.toFixed(2)}<br>
          <b>Jobs:</b><br>
          ${dateInfo.runInfo.map(job => `  - <a href="${testGopoghLink(job.rootJob, environmentName)}">${job.rootJob}</a> Total Duration: ${+job.totalDuration.toFixed(2)}s`).join("<br>")}
        </div>`,
      ]));
    const options = {
      title: `Test count and total duration by day on ${environmentName}`,
      width: window.innerWidth,
      height: window.innerHeight,
      pointSize: 10,
      pointShape: "circle",
      series: {
        0: { targetAxisIndex: 0 },
        1: { targetAxisIndex: 1 },
      },
      vAxes: {
        0: { title: "Test Count", minValue: 0 },
        1: { title: "Duration (seconds)", minValue: 0 },
      },
      tooltip: { trigger: "selection", isHtml: true }
    };
    const testCountContainer = document.createElement("div");
    testCountContainer.style.width = "100vw";
    testCountContainer.style.height = "100vh";
    chartsContainer.appendChild(testCountContainer);
    const chart = new google.visualization.LineChart(testCountContainer);
    chart.draw(data, options);
  }

  chartsContainer.appendChild(
    createRecentFlakePercentageTable(
      recentFlakePercentage,
      previousFlakePercentageMap,
      environmentName,
      period));
}

async function init() {
  const query = parseUrlQuery(window.location.search);
  const desiredTest = query.test, desiredEnvironment = query.env || "", desiredPeriod = query.period || "";

  google.charts.load('current', { 'packages': ['corechart'] });
  let testData, responseDate;
  try {
    // Wait for Google Charts to load, and for test data to load.
    // Only store the test data (at index 1) into `testData`.
    [testData, responseDate] = (await Promise.all([
      new Promise(resolve => google.charts.setOnLoadCallback(resolve)),
      loadTestData(desiredPeriod)
    ]))[1];
  } catch (err) {
    displayError(err);
    return;
  }

  if (desiredTest === undefined) {
    displayEnvironmentChart(testData, desiredEnvironment, desiredPeriod);
  } else {
    displayTestAndEnvironmentChart(testData, desiredTest, desiredEnvironment);
  }
  document.querySelector('#data_date_container').style.display = 'block';
  document.querySelector('#data_date').innerText = responseDate.toLocaleString();
  let periodDisplay, newURL;

  // we're going to take the current page URL (desiredPeriod) and modify it to create the link to the other page
  if (desiredPeriod === 'last90') {
    // remove '&period=last90' to make a link to the all-time data page
    otherPeriodURL = window.location.href.replace(/&?period=last90/gi, '');
    periodDisplay = `Currently viewing last 90 days of data: <a href="` + otherPeriodURL + `">View all-time data</a>`;
  } else {
    // add '&period=last90' to make a link to the last 90 days page
    otherPeriodURL = window.location.href + '&period=last90';
    periodDisplay = `Currently viewing all-time data: <a href="` + otherPeriodURL + `">View last 90 days of data</a>`;
  }
  document.querySelector('#period_display').innerHTML = periodDisplay;
}

init();
