// META: script=/resources/testdriver.js
// META: script=/resources/testdriver-vendor.js
// META: script=/common/get-host-info.sub.js
// META: script=/common/utils.js
// META: script=resources/ba-fledge-util.sub.js
// META: script=resources/fledge-util.sub.js
// META: script=third_party/cbor-js/cbor.js
// META: script=/common/subset-tests.js
// META: timeout=long
// META: variant=?1-6
// META: variant=?7-last

"use strict";

// These tests focus on the paggResponse field in AuctionConfig's
// serverResponse, i.e. auctions involving private aggregation reporting. NOTE:
// Due to debug mode being disabled for B&A's Private Aggregation reports, these
// tests just exercise the code paths and ensure that correct number of reports
// are sent -- they don't otherwise verify report content.

// To better isolate from private aggregation tests run in parallel,
// don't use the usual origin here.
const MAIN_ORIGIN = OTHER_ORIGIN1;
const MAIN_PATH = '/.well-known/private-aggregation/report-protected-audience';

const BUCKET_ONE = new Uint8Array([
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x01
]);

function BigEndianInteger128ToUint8Array(val) {
  let buffer = new Uint8Array(16);
  for (let i = 15; i >= 0; i--) {
    buffer[i] = Number(val & 0xFFn);
    val >>= 8n;
  }
  return buffer;
}

function createSimplePerOriginPAggResponse(
    reportingOrigin = MAIN_ORIGIN, igIndex = 0, event = 'reserved.win',
    bucket = BUCKET_ONE, value = 10, filteringId = null) {
  let contribution = {};
  if (bucket !== null) {
    contribution.bucket = bucket;
  }
  if (value !== null) {
    contribution.value = value;
  }
  if (filteringId !== null) {
    contribution.filteringId = filteringId;
  }
  return {
    'reportingOrigin': reportingOrigin,
    'igContributions': [{
      'igIndex': igIndex,
      'eventContributions': [{'event': event, 'contributions': [contribution]}]
    }]
  };
}

async function privateAggregationTestWithMutatedServerResponse(
    test, expectWin, paggResponse, timeout = 5000 /*ms*/,
    ownerOverride = null) {
  await resetReports(MAIN_ORIGIN + MAIN_PATH);
  let result = await BA.testWithMutatedServerResponse(
      test, expectWin,
      (msg, uuid) => {
        msg.paggResponse = paggResponse;
      },
      (ig, uuid) => {
        ig.ads[0].renderURL = createRenderURL(uuid);
      },
      ownerOverride);
  createAndNavigateFencedFrame(test, result);
  const reports = await pollReports(MAIN_PATH, timeout);
  return reports;
}

async function testInvalidPAggResponseFields(
    test, reportingOrigin = MAIN_ORIGIN, igIndex = 0, event = 'reserved.win',
    bucket = '1', value = 10, filteringId = null) {
  const paggResponse = [createSimplePerOriginPAggResponse(
      reportingOrigin, igIndex, event, bucket, value, filteringId)];

  let reports = await privateAggregationTestWithMutatedServerResponse(
      test,
      /*expectWin=*/ true, paggResponse, /*timeout=*/ 5000, MAIN_ORIGIN);
  assert_equals(reports, null);
}

// The next few methods are modified 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;
};

/**
 * Verifies that a report's aggregation_service_payloads has the expected
 * fields. Currently for B&A's PAgg reports, debug mode is disabled, so we
 * cannot check contributions in payload.
 */
const verifyAggregationServicePayloads = (aggregation_service_payloads) => {
  assert_equals(aggregation_service_payloads.length, 1);
  const payload_obj = aggregation_service_payloads[0];

  assert_own_property(payload_obj, 'key_id');
  assert_own_property(payload_obj, 'payload');
  // Check the payload is base64 encoded. We do not decrypt the payload to
  // test its contents.
  atob(payload_obj.payload);

  // Check there are no extra keys
  assert_equals(Object.keys(payload_obj).length, expected_payload ? 3 : 2);
};

/**
 * Verifies that a report has the expected fields. The `expected_payload` should
 * be undefined.
 */
const verifyReport = (report, reporting_origin) => {
  assert_own_property(report, 'shared_info');
  let shared_info = JSON.parse(report.shared_info);
  assert_own_property(shared_info, 'reporting_origin');
  assert_equals(shared_info.reporting_origin, reporting_origin);
  assert_own_property(report, 'aggregation_service_payloads');
  assert_own_property(report, 'aggregation_coordinator_origin');
  // TODO(qingxinwu): Maybe add tests for coordinator origin.

  assert_not_own_property(report, 'debug_key');

  // Check there are no extra keys
  let expected_length = 3;
  assert_equals(Object.keys(report).length, expected_length);
};

subsetTest(promise_test, async test => {
  await testInvalidPAggResponseFields(test, 'http://non-https.com');
}, 'Private aggregation - invalid reporting origin');

subsetTest(
    promise_test,
    async test => {await testInvalidPAggResponseFields(test, MAIN_ORIGIN, 100)},
    'Private aggregation - invalid index');

subsetTest(promise_test, async test => {
  await testInvalidPAggResponseFields(
      test, MAIN_ORIGIN, 0, 'reserved.not-supported');
}, 'Private aggregation - invalid event');

subsetTest(promise_test, async test => {
  await testInvalidPAggResponseFields(
      test, MAIN_ORIGIN, 0, 'reserved.win', /*bucket=*/ null);
}, 'Private aggregation - missing required bucket');

subsetTest(promise_test, async test => {
  await testInvalidPAggResponseFields(
      test, MAIN_ORIGIN, 0, 'reserved.win', new Uint8Array([
        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x01
      ]));
}, 'Private aggregation - bucket is bigger than 128 bits');

subsetTest(promise_test, async test => {
  await testInvalidPAggResponseFields(
      test, MAIN_ORIGIN, 0, 'reserved.win', BUCKET_ONE, /*value=*/ null);
}, 'Private aggregation - missing required value');

subsetTest(promise_test, async test => {
  await testInvalidPAggResponseFields(
      test, MAIN_ORIGIN, 0, 'reserved.win', BUCKET_ONE, 10, 10000);
}, 'Private aggregation - invalid filteringId');

subsetTest(promise_test, async test => {
  const paggResponse = [createSimplePerOriginPAggResponse()];

  let reports = await privateAggregationTestWithMutatedServerResponse(
      test,
      /*expectWin=*/ true, paggResponse, /*timeout=*/ 6000, MAIN_ORIGIN);
  assert_equals(reports.length, 1);
  let report = JSON.parse(reports[0]);
  verifyReport(report, MAIN_ORIGIN);
}, 'Private aggregation - successfully sent report');

// TODO(qingxinwu): may add a test for custom event type if possible.

subsetTest(promise_test, async test => {
  const paggResponse = [{
    'reportingOrigin': MAIN_ORIGIN,
    'igContributions': [{
      'igIndex': 0,
      'eventContributions': [
        {
          'event': 'reserved.win',
          'contributions': [{'value': 10}, {'bucket': BUCKET_ONE, 'value': 11}]
        },
        {
          'event': 'reserved.not-supported',
          'contributions':
              [{'bucket': BigEndianInteger128ToUint8Array(2n), 'value': 22}]
        },
      ]
    }]
  }];

  let reports = await privateAggregationTestWithMutatedServerResponse(
      test,
      /*expectWin=*/ true, paggResponse, /*timeout=*/ 6000, MAIN_ORIGIN);
  assert_equals(reports.length, 1);
  let report = JSON.parse(reports[0]);
  verifyReport(report, MAIN_ORIGIN);
}, 'Private aggregation - invalid contributions do not affect valid ones');

// TODO(qingxinwu): privateAggregation multi-seller.
