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

const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
  Ci.nsIProcessToolsService
);

let getStatsReports = async (filter = "") => {
  let { reports } = await new Promise(r =>
    WebrtcGlobalInformation.getAllStats(r, filter)
  );

  ok(Array.isArray(reports), "|reports| is an array");

  let sanityCheckReport = report => {
    isnot(report.pcid, "", "pcid is non-empty");
    if (filter.length) {
      is(report.pcid, filter, "pcid matches filter");
    }

    // Check for duplicates
    const checkForDuplicateId = statsArray => {
      ok(Array.isArray(statsArray), "|statsArray| is an array");
      const ids = new Set();
      statsArray.forEach(stat => {
        is(typeof stat.id, "string", "|stat.id| is a string");
        ok(
          !ids.has(stat.id),
          `Id ${stat.id} should appear only once. Stat was ${JSON.stringify(
            stat
          )}`
        );
        ids.add(stat.id);
      });
    };

    checkForDuplicateId(report.inboundRtpStreamStats);
    checkForDuplicateId(report.outboundRtpStreamStats);
    checkForDuplicateId(report.remoteInboundRtpStreamStats);
    checkForDuplicateId(report.remoteOutboundRtpStreamStats);
    checkForDuplicateId(report.rtpContributingSourceStats);
    checkForDuplicateId(report.iceCandidatePairStats);
    checkForDuplicateId(report.iceCandidateStats);
    checkForDuplicateId(report.trickledIceCandidateStats);
    checkForDuplicateId(report.dataChannelStats);
    checkForDuplicateId(report.codecStats);
  };

  reports.forEach(sanityCheckReport);
  return reports;
};

const getStatsHistoryPcIds = async () => {
  return new Promise(r => WebrtcGlobalInformation.getStatsHistoryPcIds(r));
};

const getStatsHistorySince = async (pcid, after, sdpAfter) => {
  return new Promise(r =>
    WebrtcGlobalInformation.getStatsHistorySince(r, pcid, after, sdpAfter)
  );
};

let getLogging = async () => {
  let logs = await new Promise(r => WebrtcGlobalInformation.getLogging("", r));
  ok(Array.isArray(logs), "|logs| is an array");
  return logs;
};

let checkStatsReportCount = async (count, filter = "") => {
  let reports = await getStatsReports(filter);
  is(reports.length, count, `|reports| should have length ${count}`);
  if (reports.length != count) {
    info(`reports = ${JSON.stringify(reports)}`);
  }
  return reports;
};

let checkLoggingEmpty = async () => {
  let logs = await getLogging();
  is(logs.length, 0, "Logging is empty");
  if (logs.length) {
    info(`logs = ${JSON.stringify(logs)}`);
  }
  return logs;
};

let checkLoggingNonEmpty = async () => {
  let logs = await getLogging();
  isnot(logs.length, 0, "Logging is not empty");
  return logs;
};

let clearAndCheck = async () => {
  WebrtcGlobalInformation.clearAllStats();
  WebrtcGlobalInformation.clearLogging();
  await checkStatsReportCount(0);
  await checkLoggingEmpty();
};

let openTabInNewProcess = async file => {
  let rootDir = getRootDirectory(gTestPath);
  rootDir = rootDir.replace(
    "chrome://mochitests/content/",
    "https://example.com/"
  );
  let absoluteURI = rootDir + file;

  return BrowserTestUtils.openNewForegroundTab({
    gBrowser,
    opening: absoluteURI,
    forceNewProcess: true,
  });
};

let killTabProcess = async tab => {
  await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
    ChromeUtils.privateNoteIntentionalCrash();
  });
  ProcessTools.kill(tab.linkedBrowser.frameLoader.remoteTab.osPid);
};

add_task(async () => {
  info("Test that clearAllStats is callable");
  WebrtcGlobalInformation.clearAllStats();
  ok(true, "clearAllStats returns");
});

add_task(async () => {
  info("Test that clearLogging is callable");
  WebrtcGlobalInformation.clearLogging();
  ok(true, "clearLogging returns");
});

add_task(async () => {
  info(
    "Test that getAllStats is callable, and returns 0 results when no RTCPeerConnections have existed"
  );
  await checkStatsReportCount(0);
});

add_task(async () => {
  info(
    "Test that getLogging is callable, and returns 0 results when no RTCPeerConnections have existed"
  );
  await checkLoggingEmpty();
});

add_task(async () => {
  info("Test that we can get stats/logging for a PC on the parent process");
  await clearAndCheck();
  let pc = new RTCPeerConnection();
  await pc.setLocalDescription(
    await pc.createOffer({ offerToReceiveAudio: true })
  );
  // Let ICE stack go quiescent
  await new Promise(r => {
    pc.onicegatheringstatechange = () => {
      if (pc.iceGatheringState == "complete") {
        r();
      }
    };
  });
  await checkStatsReportCount(1);
  await checkLoggingNonEmpty();
  pc.close();
  pc = null;
  // Closing a PC should not do anything to the ICE logging
  await checkLoggingNonEmpty();
  // There's just no way to get a signal that the ICE stack has stopped logging
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(r => setTimeout(r, 2000));
  await clearAndCheck();
});

add_task(async () => {
  info("Test that we can get stats/logging for a PC on a content process");
  await clearAndCheck();
  let tab = await openTabInNewProcess("single_peerconnection.html");
  await checkStatsReportCount(1);
  await checkLoggingNonEmpty();
  await killTabProcess(tab);
  BrowserTestUtils.removeTab(tab);
  await clearAndCheck();
});

add_task(async () => {
  info(
    "Test that we can get stats/logging for two connected PCs on a content process"
  );
  await clearAndCheck();
  let tab = await openTabInNewProcess("peerconnection_connect.html");
  await checkStatsReportCount(2);
  await checkLoggingNonEmpty();
  await killTabProcess(tab);
  BrowserTestUtils.removeTab(tab);
  await clearAndCheck();
});

add_task(async () => {
  info("Test filtering for stats reports (parent process)");
  await clearAndCheck();
  let pc1 = new RTCPeerConnection();
  let pc2 = new RTCPeerConnection();
  let allReports = await checkStatsReportCount(2);
  await checkStatsReportCount(1, allReports[0].pcid);
  pc1.close();
  pc2.close();
  pc1 = null;
  pc2 = null;
  await checkStatsReportCount(1, allReports[0].pcid);
  await clearAndCheck();
});

add_task(async () => {
  info("Test filtering for stats reports (content process)");
  await clearAndCheck();
  let tab1 = await openTabInNewProcess("single_peerconnection.html");
  let tab2 = await openTabInNewProcess("single_peerconnection.html");
  let allReports = await checkStatsReportCount(2);
  await checkStatsReportCount(1, allReports[0].pcid);
  await killTabProcess(tab1);
  BrowserTestUtils.removeTab(tab1);
  await killTabProcess(tab2);
  BrowserTestUtils.removeTab(tab2);
  await checkStatsReportCount(1, allReports[0].pcid);
  await clearAndCheck();
});

add_task(async () => {
  info("Test that stats/logging persists when PC is closed (parent process)");
  await clearAndCheck();
  let pc = new RTCPeerConnection();
  // This stuff will generate logging
  await pc.setLocalDescription(
    await pc.createOffer({ offerToReceiveAudio: true })
  );
  // Once gathering is done, the ICE stack should go quiescent
  await new Promise(r => {
    pc.onicegatheringstatechange = () => {
      if (pc.iceGatheringState == "complete") {
        r();
      }
    };
  });
  let reports = await checkStatsReportCount(1);
  isnot(
    window.browsingContext.browserId,
    undefined,
    "browserId is defined for parent process"
  );
  is(
    reports[0].browserId,
    window.browsingContext.browserId,
    "browserId for stats report matches parent process"
  );
  await checkLoggingNonEmpty();
  pc.close();
  pc = null;
  await checkStatsReportCount(1);
  await checkLoggingNonEmpty();
  await clearAndCheck();
});

add_task(async () => {
  info("Test that stats/logging persists when PC is closed (content process)");
  await clearAndCheck();
  let tab = await openTabInNewProcess("single_peerconnection.html");
  let { browserId } = tab.linkedBrowser;
  let reports = await checkStatsReportCount(1);
  is(reports[0].browserId, browserId, "browserId for stats report matches tab");
  isnot(
    browserId,
    window.browsingContext.browserId,
    "tab browser id is not the same as parent process browser id"
  );
  await checkLoggingNonEmpty();
  await killTabProcess(tab);
  BrowserTestUtils.removeTab(tab);
  await checkStatsReportCount(1);
  await checkLoggingNonEmpty();
  await clearAndCheck();
});

const set_int_pref_returning_unsetter = (pref, num) => {
  const value = Services.prefs.getIntPref(pref);
  Services.prefs.setIntPref(pref, num);
  return () => Services.prefs.setIntPref(pref, value);
};

const stats_history_is_enabled = () => {
  return Services.prefs.getBoolPref("media.aboutwebrtc.hist.enabled");
};

const set_max_histories_to_retain = num =>
  set_int_pref_returning_unsetter(
    "media.aboutwebrtc.hist.closed_stats_to_retain",
    num
  );

const set_history_storage_window_s = num =>
  set_int_pref_returning_unsetter(
    "media.aboutwebrtc.hist.storage_window_s",
    num
  );

add_task(async () => {
  if (!stats_history_is_enabled()) {
    return;
  }
  info(
    "Test that stats history is available after close until clearLongTermStats is called"
  );
  await clearAndCheck();
  const pc = new RTCPeerConnection();

  const ids = await getStatsHistoryPcIds();
  is(ids.length, 1, "There is a single PeerConnection Id for stats history.");

  let firstLen = 0;
  // I "don't love" this but we don't have a anything we can await on ... yet.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(r => setTimeout(r, 2000));
  {
    const history = await getStatsHistorySince(ids[0]);
    firstLen = history.reports.length;
    ok(
      history.reports.length,
      "There is at least a single PeerConnection stats history before close."
    );
  }
  // I "don't love" this but we don't have a anything we can await on ... yet.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(r => setTimeout(r, 2000));
  {
    const history = await getStatsHistorySince(ids[0]);
    const secondLen = history.reports.length;
    ok(
      secondLen > firstLen,
      "After waiting there are more history entries available."
    );
  }
  pc.close();
  // After close for final stats and pc teardown to settle
  // I "don't love" this but we don't have a anything we can await on ... yet.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(r => setTimeout(r, 2000));
  {
    const history = await getStatsHistorySince(ids[0]);
    ok(
      history.reports.length,
      "There is at least a single PeerConnection stats history after close."
    );
  }
  await clearAndCheck();
  {
    const history = await getStatsHistorySince(ids[0]);
    is(
      history.reports.length,
      0,
      "After PC.close and clearing the stats there are no history reports"
    );
  }
  {
    const ids1 = await getStatsHistoryPcIds();
    is(
      ids1.length,
      0,
      "After PC.close and clearing the stats there are no history pcids"
    );
  }
  {
    const pc2 = new RTCPeerConnection();
    const pc3 = new RTCPeerConnection();
    let idsN = await getStatsHistoryPcIds();
    is(
      idsN.length,
      2,
      "There are two pcIds after creating two PeerConnections"
    );
    pc2.close();
    // After close for final stats and pc teardown to settle
    // I "don't love" this but we don't have a anything we can await on ... yet.
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(r => setTimeout(r, 2000));
    await WebrtcGlobalInformation.clearAllStats();
    idsN = await getStatsHistoryPcIds();
    is(
      idsN.length,
      1,
      "There is one pcIds after closing one of two PeerConnections and clearing stats"
    );
    pc3.close();
    // After close for final stats and pc teardown to settle
    // I "don't love" this but we don't have a anything we can await on ... yet.
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(r => setTimeout(r, 2000));
  }
});

add_task(async () => {
  if (!stats_history_is_enabled()) {
    return;
  }
  const restoreHistRetainPref = set_max_histories_to_retain(7);
  info("Test that the proper number of pcIds are available");
  await clearAndCheck();
  const pc01 = new RTCPeerConnection();
  const pc02 = new RTCPeerConnection();
  const pc03 = new RTCPeerConnection();
  const pc04 = new RTCPeerConnection();
  const pc05 = new RTCPeerConnection();
  const pc06 = new RTCPeerConnection();
  const pc07 = new RTCPeerConnection();
  const pc08 = new RTCPeerConnection();
  const pc09 = new RTCPeerConnection();
  const pc10 = new RTCPeerConnection();
  const pc11 = new RTCPeerConnection();
  const pc12 = new RTCPeerConnection();
  const pc13 = new RTCPeerConnection();
  // I "don't love" this but we don't have a anything we can await on ... yet.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(r => setTimeout(r, 2000));
  {
    const ids = await getStatsHistoryPcIds();
    is(ids.length, 13, "There is are 13 PeerConnection Ids for stats history.");
  }
  pc01.close();
  pc02.close();
  pc03.close();
  pc04.close();
  pc05.close();
  pc06.close();
  pc07.close();
  pc08.close();
  pc09.close();
  pc10.close();
  pc11.close();
  pc12.close();
  pc13.close();
  // I "don't love" this but we don't have a anything we can await on ... yet.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(r => setTimeout(r, 5000));
  {
    const ids = await getStatsHistoryPcIds();
    is(
      ids.length,
      7,
      "After closing 13 PCs there are no more than the max closed (7) PeerConnection Ids for stats history."
    );
  }
  restoreHistRetainPref();
  await clearAndCheck();
});

add_task(async () => {
  if (!stats_history_is_enabled()) {
    return;
  }
  // If you change this, please check if the setTimeout should be updated.
  // NOTE: the unit here is _integer_ seconds.
  const STORAGE_WINDOW_S = 1;
  const restoreStorageWindowPref =
    set_history_storage_window_s(STORAGE_WINDOW_S);
  info("Test that history items are being aged out");
  await clearAndCheck();
  const pc = new RTCPeerConnection();
  // I "don't love" this but we don't have a anything we can await on ... yet.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(r => setTimeout(r, STORAGE_WINDOW_S * 2 * 1000));
  const ids = await getStatsHistoryPcIds();
  const { reports } = await getStatsHistorySince(ids[0]);
  const first = reports[0];
  const last = reports.at(-1);
  ok(
    last.timestamp - first.timestamp <= STORAGE_WINDOW_S * 1000,
    "History reports should be aging out according to the storage window pref"
  );
  pc.close();
  restoreStorageWindowPref();
  await clearAndCheck();
});
