// META: script=/resources/testdriver.js
// META: script=/resources/testdriver-vendor.js
// META: script=/common/utils.js
// META: script=resources/fledge-util.sub.js
// META: script=/common/subset-tests.js
// META: script=third_party/cbor-js/cbor.js
// META: timeout=long
// META: variant=?1-5
// META: variant=?6-10
// META: variant=?11-15
// META: variant=?16-20

'use strict';

// To better isolate from private aggregation tests run in parallel,
// don't use the usual origin here.
const MAIN_ORIGIN = OTHER_ORIGIN1;
const ALT_ORIGIN = OTHER_ORIGIN4;

const MAIN_PATH = '/.well-known/private-aggregation/report-protected-audience';
const DEBUG_PATH =
    '/.well-known/private-aggregation/debug/report-protected-audience';

const ADDITIONAL_BID_PUBLIC_KEY =
    '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=';

const enableDebugMode = 'privateAggregation.enableDebugMode();';

// The next 3 methods are for interfacing with the test handler for
// Private Aggregation reports; adopted wholesale from Chrome-specific
// wpt_internal/private-aggregation/resources/utils.js
const resetReports = url => {
  url = `${url}?clear_stash=true`;
  const options = {
    method: 'POST',
    mode: 'no-cors',
  };
  return fetch(url, options);
};

const delay = ms => new Promise(resolve => step_timeout(resolve, ms));

async function pollReports(path, wait_for = 1, timeout = 5000 /*ms*/) {
  const targetUrl = new URL(path, MAIN_ORIGIN);
  const endTime = performance.now() + timeout;
  const outReports = [];

  do {
    const response = await fetch(targetUrl);
    assert_true(response.ok, 'pollReports() fetch response should be OK.');
    const reports = await response.json();
    outReports.push(...reports);
    if (outReports.length >= wait_for) {
      break;
    }
    await delay(/*ms=*/ 100);
  } while (performance.now() < endTime);

  return outReports.length ? outReports : null;
};

function decodeBase64(inStr) {
  let strBytes = atob(inStr);
  let arrBytes = new Uint8Array(strBytes.length);
  for (let i = 0; i < strBytes.length; ++i) {
    arrBytes[i] = strBytes.codePointAt(i);
  }
  return arrBytes.buffer;
}

function byteArrayToBigInt(inArray) {
  let out = 0n;
  for (let byte of inArray) {
    out = out * 256n + BigInt(byte);
  }
  return out;
}

async function getDebugSamples(path) {
  const debugReports = await pollReports(path);

  let samplesDict = new Map();

  // Extract samples for debug reports, and aggregate them, so we are not
  // reliant on how aggregation happens.
  for (let jsonReport of debugReports) {
    let report = JSON.parse(jsonReport);
    for (let payload of report.aggregation_service_payloads) {
      let decoded = CBOR.decode(decodeBase64(payload.debug_cleartext_payload));
      assert_equals(decoded.operation, 'histogram');
      for (let sample of decoded.data) {
        let convertedSample = {
          bucket: byteArrayToBigInt(sample.bucket),
          value: byteArrayToBigInt(sample.value)
        };
        if (convertedSample.value !== 0n) {
          let oldCount = 0n;
          if (samplesDict.has(convertedSample.bucket)) {
            oldCount = samplesDict.get(convertedSample.bucket);
          }

          samplesDict.set(
              convertedSample.bucket, oldCount + convertedSample.value);
        }
      }
    }
  }

  return samplesDict;
}

function stringifySamples(samplesDict) {
  let samplesArray = [];
  for (let [bucket, value] of samplesDict.entries()) {
    // Stringify these so we can use assert_array_equals on them.
    samplesArray.push(bucket + ' => ' + value);
  }
  samplesArray.sort();
  return samplesArray;
}

function maybeDelay(delayParam) {
  if (delayParam) {
    return `&pipe=trickle(d${delayParam / 1000})`
  } else {
    return '';
  }
}

function createIgOverrides(nameAndBid, fragments, originOverride = null) {
  let originToUse = originOverride ? originOverride : MAIN_ORIGIN;
  return {
    name: nameAndBid,
    biddingLogicURL: createBiddingScriptURL({
                       origin: originToUse,
                       generateBid:
                           enableDebugMode + fragments.generateBidFragment,
                       reportWin: enableDebugMode + fragments.reportWinFragment,
                       bid: nameAndBid,
                       allowComponentAuction: true
                     }) +
        maybeDelay(fragments.bidderDelayFactor ?
                       fragments.bidderDelayFactor * nameAndBid :
                       null)
  };
}

function expectAndConsume(samplesDict, bucket, val) {
  assert_equals(samplesDict.get(bucket), val, 'sample in bucket ' + bucket);
  samplesDict.delete(bucket);
}

function createAuctionConfigOverrides(
    uuid, fragments, moreAuctionConfigOverrides = {}) {
  return {
    decisionLogicURL:
        createDecisionScriptURL(uuid, {
          origin: MAIN_ORIGIN,
          scoreAd: enableDebugMode + fragments.scoreAdFragment,
          reportResult: enableDebugMode + fragments.reportResultFragment
        }) +
        maybeDelay(fragments.sellerDelay),
    seller: MAIN_ORIGIN,
    interestGroupBuyers: [MAIN_ORIGIN],
    privateAggregationConfig:
      { aggregationCoordinatorOrigin: window.location.origin },
    ...moreAuctionConfigOverrides
  };
}

// Runs an auction with numGroups interest groups, "1" and "2", etc., with
// fragments.generateBidFragment/fragments.reportWinFragment/
// fragments.scoreAdFragment/fragments.reportResultFragment
// expected to make some Private Aggregation contributions.
// Returns the collected samples.
async function runPrivateAggregationTest(
    test, uuid, fragments, numGroups = 2, moreAuctionConfigOverrides = {}) {
  await resetReports(MAIN_ORIGIN + MAIN_PATH);
  await resetReports(MAIN_ORIGIN + DEBUG_PATH);

  for (let i = 1; i <= numGroups; ++i) {
    await joinCrossOriginInterestGroup(
        test, uuid, MAIN_ORIGIN, createIgOverrides(i, fragments));
  }

  const auctionConfigOverrides =
      createAuctionConfigOverrides(uuid, fragments, moreAuctionConfigOverrides);

  await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
  return await getDebugSamples(DEBUG_PATH);
}

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogram({ bucket: 1n, value: 2 });`,

    reportWinFragment:
        `privateAggregation.contributeToHistogram({ bucket: 2n, value: 3 });`,

    scoreAdFragment:
        `privateAggregation.contributeToHistogram({ bucket: 3n, value: 4 });`,

    reportResultFragment:
        `privateAggregation.contributeToHistogram({ bucket: 4n, value: 5 });`
  };

  const samples = await runPrivateAggregationTest(test, uuid, fragments);
  assert_array_equals(
      stringifySamples(samples),
      [
        '1 => 4',  // doubled since it's reported twice.
        '2 => 3',
        '3 => 8',  // doubled since it's reported twice.
        '4 => 5'
      ]);
}, 'Basic contributions');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always',
          { bucket: 1n, value: 2 });`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always',
          { bucket: 2n, value: 3 });`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always',
          { bucket: 3n, value: 4 });`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always',
          { bucket: 4n, value: 5 });`
  };

  const samples = await runPrivateAggregationTest(test, uuid, fragments);
  assert_array_equals(
      stringifySamples(samples),
      [
        '1 => 4',  // doubled since it's reported twice.
        '2 => 3',
        '3 => 8',  // doubled since it's reported twice.
        '4 => 5'
      ]);
}, 'reserved.always');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.win',
          { bucket: 1n, value: interestGroup.name });`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.win',
          { bucket: 2n, value: 3 });`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.win',
          { bucket: 3n, value: bid });`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.win',
          { bucket: 4n, value: 5 });`
  };

  const samples = await runPrivateAggregationTest(test, uuid, fragments);
  assert_array_equals(
      stringifySamples(samples),
      [
        '1 => 2',  // winning IG name
        '2 => 3',
        '3 => 2',  // winning bid
        '4 => 5'
      ]);
}, 'reserved.win');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.loss',
          { bucket: 1n, value: interestGroup.name });`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.loss',
          { bucket: 2n, value: 3 });`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.loss',
          { bucket: 3n, value: bid });`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.loss',
          { bucket: 4n, value: 5 });`
  };

  const samples = await runPrivateAggregationTest(test, uuid, fragments);

  // No reserved.loss from reporting since they only run for winners.
  assert_array_equals(
      stringifySamples(samples),
      [
        '1 => 1',  // losing IG name
        '3 => 1',  // losing bid
      ]);
}, 'reserved.loss');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once',
          { bucket: 1n, value: interestGroup.name });`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once',
          { bucket: 2n, value: 3 });`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once',
          { bucket: 3n, value: bid });`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once',
          { bucket: 4n, value: 5 });`
  };

  const samples =
      stringifySamples(await runPrivateAggregationTest(test, uuid, fragments));

  // No reserved.once from reporting since it throws an exception.
  // bidder/scorer just pick one.
  assert_equals(samples.length, 2, 'samples array length');
  assert_in_array(samples[0], ['1 => 1', '1 => 2'], 'samples[0]');
  assert_in_array(samples[1], ['3 => 1', '3 => 2'], 'samples[1]');
}, 'reserved.once');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once',
          { bucket: 1n, value: 1 });`,

    reportWinFragment: `
      try {
        privateAggregation.contributeToHistogramOnEvent(
            'reserved.once',
            { bucket: 2n, value: 2 });
      } catch (e) {
        privateAggregation.contributeToHistogramOnEvent(
            'reserved.always',
            { bucket: 2n, value: (e instanceof TypeError ? 3 : 4) });
      }`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once',
          { bucket: 3n, value: 4 });`,

    reportResultFragment: `
      try {
        privateAggregation.contributeToHistogramOnEvent(
            'reserved.once',
            { bucket: 4n, value: 5 });
      } catch (e) {
        privateAggregation.contributeToHistogramOnEvent(
            'reserved.always',
            { bucket: 4n, value: (e instanceof TypeError ? 6 : 7) });
      }`
  };

  const samples =
      stringifySamples(await runPrivateAggregationTest(test, uuid, fragments));

  assert_array_equals(samples, [
    '1 => 1',
    '2 => 3',
    '3 => 4',
    '4 => 6',
  ]);
}, 'no reserved.once in reporting');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await resetReports(ALT_ORIGIN + DEBUG_PATH);
  await resetReports(ALT_ORIGIN + MAIN_PATH);

  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: 'average-code-fetch-time', offset: 0n},
              value: 1});`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: 'average-code-fetch-time', offset: 100000n},
              value: 1});`,

    bidderDelayFactor: 200,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: 'average-code-fetch-time', offset: 200000n},
              value: 1});`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: 'average-code-fetch-time', offset: 300000n},
              value: 1});`,

    sellerDelay: 500
  };

  const altFragments = {
    generateBidFragment: fragments.generateBidFragment,
    bidderDelayFactor: 1000
  };

  await joinCrossOriginInterestGroup(
      test, uuid, ALT_ORIGIN, createIgOverrides('1', altFragments, ALT_ORIGIN));
  const auctionConfigOverrides = {
    interestGroupBuyers: [MAIN_ORIGIN, ALT_ORIGIN]
  };

  const samples = await runPrivateAggregationTest(
      test, uuid, fragments, 3, auctionConfigOverrides);

  let generateBidVal = -1;
  let reportWinVal = -1;
  let scoreAdVal = -1;
  let reportResultVal = -1;
  assert_equals(samples.size, 4, 'main domain samples');

  for (let [bucket, val] of samples.entries()) {
    assert_equals(val, 1n, 'bucket val');
    if (0n <= bucket && bucket < 100000n) {
      generateBidVal = Number(bucket - 0n);
    } else if (100000n <= bucket && bucket < 200000n) {
      reportWinVal = Number(bucket - 100000n);
    } else if (200000n <= bucket && bucket < 300000n) {
      scoreAdVal = Number(bucket - 200000n);
    } else if (300000n <= bucket && bucket < 400000n) {
      reportResultVal = Number(bucket - 300000n);
    } else {
      assert_unreached('Unexpected bucket number ' + bucket);
    }
  }

  assert_greater_than_equal(generateBidVal, 400, 'generateBid code fetch time');
  assert_greater_than_equal(reportWinVal, 600, 'reportWin code fetch time');
  assert_greater_than_equal(scoreAdVal, 500, 'scoreAd code fetch time');
  assert_greater_than_equal(
      reportResultVal, 500, 'reportResult code fetch time');

  let otherSamples = await getDebugSamples(ALT_ORIGIN + DEBUG_PATH);
  assert_equals(otherSamples.size, 1, 'alt domain samples');
  let otherGenerateBidVal = -1;
  for (let [bucket, val] of otherSamples.entries()) {
    assert_equals(val, 1n, 'other bucket val');
    if (0n <= bucket && bucket < 100000n) {
      otherGenerateBidVal = Number(bucket - 0n);
    } else {
      assert_unreached('Unexpected other bucket number ' + bucket);
    }
  }
  assert_greater_than_equal(
      otherGenerateBidVal, 1000, 'other generateBid code fetch time');
}, 'average-code-fetch-time');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: 'percent-scripts-timeout', offset: 0n},
              value: 1});
      if (interestGroup.name === '1') {
        while (true) {}
      }
      `,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: 'percent-scripts-timeout', offset: 200n},
              value: 1});
      while(true) {}`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: 'percent-scripts-timeout', offset: 400n},
              value: 1});
      if (bid == 2) {
        while (true) {}
      }
      `,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: 'percent-scripts-timeout', offset: 600n},
              value: 1});`
  };

  const samples = await runPrivateAggregationTest(test, uuid, fragments, 3);

  let expected = [
    '33 => 1',   // 33% of generateBid  (base bucket 0)
    '300 => 1',  // 100% of reportWin   (base bucket 200)
    '450 => 1',  // 50% of scoreAd      (base bucket 400)
    '600 => 1',  // 0% of reportResult  (base bucket 600)
  ].sort();

  assert_array_equals(stringifySamples(samples), expected);
}, 'percent-scripts-timeout');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await resetReports(ALT_ORIGIN + DEBUG_PATH);
  await resetReports(ALT_ORIGIN + MAIN_PATH);

  const ADDITIONAL_BID_PUBLIC_KEY =
      '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=';

  // Join a negative group, one without ads.
  // These shouldn't count towards participant number.
  await joinNegativeInterestGroup(
      test, MAIN_ORIGIN, 'some negative group', ADDITIONAL_BID_PUBLIC_KEY);
  await joinCrossOriginInterestGroup(test, uuid, MAIN_ORIGIN, {ads: []});

  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: 'participating-ig-count', offset: 0n},
              value: 1});`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: 'participating-ig-count', offset: 200n},
              value: 1});`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: 'participating-ig-count', offset: 400n},
              value: 1});`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: 'participating-ig-count', offset: 600n},
              value: 1});`
  };

  // ... and a different participant should get their own samples.
  await joinCrossOriginInterestGroup(
      test, uuid, ALT_ORIGIN, createIgOverrides('1', fragments, ALT_ORIGIN));
  await joinCrossOriginInterestGroup(
      test, uuid, ALT_ORIGIN, createIgOverrides('2', fragments, ALT_ORIGIN));
  const auctionConfigOverrides = {
    interestGroupBuyers: [MAIN_ORIGIN, ALT_ORIGIN]
  };

  const samples = await runPrivateAggregationTest(
      test, uuid, fragments, 5, auctionConfigOverrides);

  let expected = [
    '5 => 1',    // 5 in generateBid  (base bucket 0)
    '205 => 1',  // 5 in reportWin    (base bucket 200)
    '400 => 1',  // 0 in scoreAd      (base bucket 400)
    '600 => 1',  // 0 in reportResult (base bucket 600)
  ].sort();

  assert_array_equals(stringifySamples(samples), expected);

  let otherSamples = await getDebugSamples(ALT_ORIGIN + DEBUG_PATH);
  assert_array_equals(stringifySamples(otherSamples), ['2 => 1']);
}, 'participating-ig-count');


subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {
                  baseValue: 'percent-igs-cumulative-timeout',
                  offset: 0n
              },
              value: 1});
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {
                  baseValue: 'cumulative-buyer-time',
                  offset: 10000n
              },
              value: 1});
      setBid({bid: interestGroup.name, render: interestGroup.ads[0].renderURL});
      while (true) {}
      `,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {
                  baseValue: 'percent-igs-cumulative-timeout',
                  offset: 200n
              },
              value: 1});
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {
                  baseValue: 'cumulative-buyer-time',
                  offset: 20000n
              },
              value: 1});
      `,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {
                  baseValue: 'percent-igs-cumulative-timeout',
                  offset: 400n
              },
              value: 1});
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {
                  baseValue: 'cumulative-buyer-time',
                  offset: 40000n
              },
              value: 1});
      `,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {
                  baseValue: 'percent-igs-cumulative-timeout',
                  offset: 600n
              },
              value: 1});
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {
                  baseValue: 'cumulative-buyer-time',
                  offset: 60000n
              },
              value: 1});`
  };

  const auctionConfigOverrides = {
    perBuyerTimeouts: {
      '*': 500  // max.
    },
    perBuyerCumulativeTimeouts: {'*': 2000}
  };

  const samples = await runPrivateAggregationTest(
      test, uuid, fragments, 15, auctionConfigOverrides);

  // Timeout is reported as 3000 (limit + 1000) for generateBid
  // and reportWin, as 0 for the seller methods.
  expectAndConsume(samples, 13000n, 1n);  // base is 10,000
  expectAndConsume(samples, 23000n, 1n);
  expectAndConsume(samples, 40000n, 1n);
  expectAndConsume(samples, 60000n, 1n);

  // percent time is 0 on the seller side.
  expectAndConsume(samples, 400n, 1n);
  expectAndConsume(samples, 600n, 1n);

  assert_equals(samples.size, 2, 'buyer samples');

  let percentGenerateBid = -1;
  let percentReportWin = -1;

  for (let [bucket, val] of samples.entries()) {
    assert_equals(val, 1n, 'bucket val');
    if (0n <= bucket && bucket <= 110n) {
      percentGenerateBid = bucket;
    } else if (200n <= bucket && bucket <= 310n) {
      percentReportWin = bucket - 200n;
    } else {
      assert_unreached('Unexpected bucket number ' + bucket);
    }
  }

  assert_equals(
      percentGenerateBid, percentReportWin,
      'same % in generateBid and reportWin');

  // This assumes that at least some time out; which may not be guaranteed with
  // sufficient level of parallelism. At any rate, the denominator is 15,
  // however, so only some percentages are possible.
  assert_in_array(
      percentGenerateBid,
      [6n, 13n, 20n, 26n, 33n, 40n, 46n, 53n, 60n, 66n, 73n, 80n, 86n, 93n],
      'percent timeout is as expected');
}, 'percent-igs-cumulative-timeout, and cumulative-buyer-time when hit');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: 'cumulative-buyer-time', offset: 0n},
              value: 1});`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: 'cumulative-buyer-time', offset: 200n},
              value: 1});`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: 'cumulative-buyer-time', offset: 400n},
              value: 1});`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: 'cumulative-buyer-time', offset: 600n},
              value: 1});`
  };

  const samples = await runPrivateAggregationTest(test, uuid, fragments, 5);

  // 0s for all the bases.
  let expected = ['0 => 1', '200 => 1', '400 => 1', '600 => 1'].sort();

  assert_array_equals(stringifySamples(samples), expected);
}, 'cumulative-buyer-time when not configured');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: 'cumulative-buyer-time', offset: 0n},
              value: 1});`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: 'cumulative-buyer-time', offset: 10000n},
              value: 1});`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: 'cumulative-buyer-time', offset: 20000n},
              value: 1});`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: 'cumulative-buyer-time', offset: 30000n},
              value: 1});`
  };

  const auctionConfigOverrides = {perBuyerCumulativeTimeouts: {'*': 4000}};

  const samples = await runPrivateAggregationTest(
      test, uuid, fragments, 5, auctionConfigOverrides);

  // Sellers stuff is just 0s (so 1 to the base bucket offset).
  expectAndConsume(samples, 20000n, 1n);
  expectAndConsume(samples, 30000n, 1n);

  assert_equals(samples.size, 2, 'buyer samples');

  let timeGenerateBid = -1;
  let timeReportWin = -1;

  for (let [bucket, val] of samples.entries()) {
    assert_equals(val, 1n, 'bucket val');
    if (0n <= bucket && bucket <= 5000n) {
      timeGenerateBid = bucket;
    } else if (10000n <= bucket && bucket <= 15000n) {
      timeReportWin = bucket - 10000n;
    } else {
      assert_unreached('Unexpected bucket number');
    }
  }

  assert_equals(
      timeGenerateBid, timeReportWin, 'same time in generateBid and reportWin');

  // This assume this takes more than 0ms to run; it's not really required to
  // be the case, but feels like a realistic assumption that makes the test
  // more useful.
  assert_true(
      1n <= timeGenerateBid && timeGenerateBid <= 4000n,
      'time ' + timeGenerateBid + ' is reasonable and non-zero');
}, 'cumulative-buyer-time when configured');


async function testStorageQuotaMetric(test, name) {
  const uuid = generateUuid(test);
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: '${name}', offset: 0n},
              value: 1});`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: '${name}', offset: 10000n},
              value: 1});`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: '${name}', offset: 20000n},
              value: 1});`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: '${name}', offset: 30000n},
              value: 1});`
  };

  const samples = await runPrivateAggregationTest(test, uuid, fragments, 5);

  // Sellers stuff is just 0s (so 1 to the base bucket offset).
  expectAndConsume(samples, 20000n, 1n);
  expectAndConsume(samples, 30000n, 1n);

  assert_equals(samples.size, 2, 'buyer samples');

  let generateBidVal = -1;
  let reportWinVal = -1;

  for (let [bucket, val] of samples.entries()) {
    assert_equals(val, 1n, 'bucket val');
    if (0n <= bucket && bucket < 10000n) {
      generateBidVal = Number(bucket);
    } else if (10000n <= bucket && bucket <= 20000n) {
      reportWinVal = Number(bucket - 10000n);
    } else {
      assert_unreached('Unexpected bucket number ' + bucket);
    }
  }

  assert_equals(
      generateBidVal, reportWinVal, 'same value in generateBid and reportWin');

  // We don't know what the impls quota is, or even how much we are using,
  // but at least make sure it's in range.
  assert_between_inclusive(
      generateBidVal, 0, 110, 'reported percent value is in expected range');
}

subsetTest(promise_test, async test => {
  await testStorageQuotaMetric(test, 'percent-regular-ig-count-quota-used');
}, 'percent-regular-ig-count-quota-used');

subsetTest(promise_test, async test => {
  await testStorageQuotaMetric(test, 'percent-negative-ig-count-quota-used');
}, 'percent-negative-ig-count-quota-used');

subsetTest(promise_test, async test => {
  await testStorageQuotaMetric(test, 'percent-ig-storage-quota-used');
}, 'percent-ig-storage-quota-used');


async function testStorageUsageMetric(test, name, min) {
  const uuid = generateUuid(test);
  const spacing = 1000000000n;
  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: '${name}', offset: 0n},
              value: 1});`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: '${name}', offset: ${spacing}n},
              value: 1});`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once', {
              bucket: {baseValue: '${name}', offset: 2n * ${spacing}n},
              value: 1});`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always', {
              bucket: {baseValue: '${name}', offset: 3n * ${spacing}n},
              value: 1});`
  };

  await joinNegativeInterestGroup(
      test, MAIN_ORIGIN, 'some negative group', ADDITIONAL_BID_PUBLIC_KEY);
  await joinNegativeInterestGroup(
      test, MAIN_ORIGIN, 'some negative group 2', ADDITIONAL_BID_PUBLIC_KEY);
  await joinCrossOriginInterestGroup(
      test, uuid, MAIN_ORIGIN,
      {ads: [], name: 'Big group w/o ads'.padEnd(50000)});

  const samples = await runPrivateAggregationTest(test, uuid, fragments, 5);

  // Sellers stuff is just 0s (so 1 to the base bucket offset).
  expectAndConsume(samples, 2n * spacing, 1n);
  expectAndConsume(samples, 3n * spacing, 1n);

  assert_equals(samples.size, 2, 'buyer samples');

  let generateBidVal = -1;
  let reportWinVal = -1;

  for (let [bucket, val] of samples.entries()) {
    assert_equals(val, 1n, 'bucket val');
    if (0n <= bucket && bucket < spacing) {
      generateBidVal = bucket;
    } else if (spacing <= bucket && bucket < 2n * spacing) {
      reportWinVal = bucket - spacing;
    } else {
      assert_unreached('Unexpected bucket number ' + bucket);
    }
  }

  assert_equals(
      generateBidVal, reportWinVal, 'same value in generateBid and reportWin');

  assert_true(
      generateBidVal >= BigInt(min),
      'reported value should be at least ' + min + ' but is ' + generateBidVal);
}

subsetTest(promise_test, async test => {
  // 5 regular Igs + one ad less.
  await testStorageUsageMetric(test, 'regular-igs-count', 6);
}, 'regular-igs-count');

subsetTest(promise_test, async test => {
  // 2 negative IGs
  await testStorageUsageMetric(test, 'negative-igs-count', 2);
}, 'negative-igs-count');

subsetTest(promise_test, async test => {
  // The big group has a 50,000 character name
  await testStorageUsageMetric(test, 'ig-storage-used', 50000);
}, 'ig-storage-used');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await resetReports(MAIN_ORIGIN + MAIN_PATH);
  await resetReports(MAIN_ORIGIN + DEBUG_PATH);
  await resetReports(ALT_ORIGIN + MAIN_PATH);
  await resetReports(ALT_ORIGIN + DEBUG_PATH);

  const fragments = {
    generateBidFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once',
          { bucket: 1n, value: 2 });`,

    reportWinFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always',
          { bucket: 2n, value: 3 });`,

    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once',
          { bucket: 3n, value: 4 });`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always',
          { bucket: 4n, value: 5 });`
  };

  // 4 IGs in main origin, 2 in alt origin.
  for (let i = 1; i <= 4; ++i) {
    await joinCrossOriginInterestGroup(
        test, uuid, MAIN_ORIGIN, createIgOverrides(i, fragments));
  }

  for (let i = 1; i <= 2; ++i) {
    await joinCrossOriginInterestGroup(
        test, uuid, ALT_ORIGIN, createIgOverrides(i, fragments, ALT_ORIGIN));
  }

  // Both groups in component auction 1, only alt group in component auction 2.
  const subAuction1 = createAuctionConfigOverrides(
      uuid, fragments, {interestGroupBuyers: [MAIN_ORIGIN, ALT_ORIGIN]});
  const subAuction2 = createAuctionConfigOverrides(
      uuid, fragments, {interestGroupBuyers: [ALT_ORIGIN]});

  const topFragments = {
    scoreAdFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.once',
          { bucket: 5n, value: 6 });`,

    reportResultFragment: `
      privateAggregation.contributeToHistogramOnEvent(
          'reserved.always',
          { bucket: 6n, value: 7 });`
  };
  const mainAuction = createAuctionConfigOverrides(
      uuid, topFragments,
      {interestGroupBuyers: [], componentAuctions: [subAuction1, subAuction2]});

  await runBasicFledgeAuctionAndNavigate(test, uuid, mainAuction);
  let samples = await getDebugSamples(DEBUG_PATH);
  let otherSamples = await getDebugSamples(ALT_ORIGIN + DEBUG_PATH);
  let expected = [
    '1 => 2',  // generateBid only in first component, so happens 1.
    '2 => 3',  // reportWin once.
    '3 => 8',  // Once per each component auction (out of total 6 scored).
    '4 => 5',  // component reportResult once.
    '5 => 6',  // top-level scoreAd once.
    '6 => 7',  // top-level reportResult.
  ].sort();
  let otherExpected = [
    '1 => 4',  // generateBid in each components, so twice, out of 4 executions.
  ].sort();
  assert_array_equals(stringifySamples(samples), expected);
  assert_array_equals(stringifySamples(otherSamples), otherExpected);
}, 'report.once in a component auction');
