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

"use strict";

// Creates a tracker URL for a component ad. These are fetched from component ad URLs.
function createComponentAdTrackerURL(uuid, id) {
  return createTrackerURL(window.location.origin, uuid, 'track_get',
                          `component_ad_${id}`)
}

// Returns a component ad render URL that fetches the corresponding component ad
// tracker URL.
function createComponentAdRenderURL(uuid, id) {
  return createRenderURL(
      uuid,
      `fetch("${createComponentAdTrackerURL(uuid, id)}");`);
}

// Runs a generic component ad loading test. It joins an interest group with a
// "numComponentAdsInInterestGroup" component ads. The IG will make a bid that
// potentially includes some of them. Then an auction will be run, component
// ads potentially will be loaded in nested fenced frame within the main frame,
// and the test will make sure that each component ad render URL that should have
// been loaded in an iframe was indeed loaded.
//
// Joins an interest group that has "numComponentAdsInInterestGroup" component ads.
//
// "componentAdsInBid" is a list of 0-based indices of which of those ads will be
// included in the bid. It may contain duplicate component ads. If it's null then the
// bid will have no adComponents field, while if it is empty, the bid will have an empty
// adComponents field.
//
// "componentAdsToLoad" is another list of 0-based ad components, but it's the index of
// fenced frame configs in the top frame ad's getNestedConfigs(). It may also contain
// duplicates to load a particular ad twice.
//
// If "adMetadata" is true, metadata is added to each component ad. Only integer metadata
// is used, relying on renderURL tests to cover other types of renderURL metadata.
//
// If "deprecatedRenderURLReplacements" is passed, the matches and replacements will be
// used in the trackingURLs and the object will be passed into the auctionConfig, to
// replace matching macros within the renderURLs.
async function runComponentAdLoadingTest(test, uuid, numComponentAdsInInterestGroup,
  componentAdsInBid, componentAdsToLoad,
  adMetadata = false, deprecatedRenderURLReplacements = null) {
  let interestGroupAdComponents = [];
  // These are used within the URLs for deprecatedRenderURLReplacement tests.
  const renderURLReplacementsStrings = createStringBeforeAndAfterReplacements(deprecatedRenderURLReplacements);
  const beforeReplacementsString= renderURLReplacementsStrings.beforeReplacements;
  const afterReplacementsString = renderURLReplacementsStrings.afterReplacements;

  for (let i = 0; i < numComponentAdsInInterestGroup; ++i) {
    let componentRenderURL = createComponentAdRenderURL(uuid, i);
    if (deprecatedRenderURLReplacements !== null) {
      componentRenderURL = createTrackerURL(window.location.origin, uuid, 'track_get', beforeReplacementsString);
    }
    let adComponent = { renderURL: componentRenderURL };
    if (adMetadata)
      adComponent.metadata = i;
    interestGroupAdComponents.push(adComponent);
  }

  const renderURL = createRenderURL(
      uuid,
      `// "status" is passed to the beacon URL, to be verified by waitForObservedRequests().
       let status = "ok";
       const componentAds = window.fence.getNestedConfigs()
       if (componentAds.length !== 40)
         status = "unexpected getNestedConfigs() length";
       for (let i of ${JSON.stringify(componentAdsToLoad)}) {
         let fencedFrame = document.createElement("fencedframe");
         fencedFrame.mode = "opaque-ads";
         fencedFrame.config = componentAds[i];
         document.body.appendChild(fencedFrame);
       }

       window.fence.reportEvent({eventType: "beacon",
                                 eventData: status,
                                 destination: ["buyer"]});`);

  let bid = {bid:1, render:renderURL};
  if (componentAdsInBid) {
    bid.adComponents = [];
    for (let index of componentAdsInBid) {
      bid.adComponents.push(interestGroupAdComponents[index].renderURL);
    }
  }

  // In these tests, the bidder should always request a beacon URL.
  let expectedTrackerURLs = [`${createBidderBeaconURL(uuid)}, body: ok`];
  // Figure out which, if any, elements of "componentAdsToLoad" correspond to
  // component ads listed in bid.adComponents, and for those ads, add a tracker URL
  // to "expectedTrackerURLs".
  if (componentAdsToLoad && bid.adComponents) {
    for (let index of componentAdsToLoad) {
      let expectedURL = createComponentAdTrackerURL(uuid, componentAdsInBid[index]);
      if (deprecatedRenderURLReplacements != null) {
        expectedURL = createTrackerURL(window.location.origin, uuid, 'track_get',
                                       afterReplacementsString);
      }
      if (index < componentAdsInBid.length)
        expectedTrackerURLs.push(expectedURL);
    }
  }

  await joinInterestGroup(
      test, uuid,
      { biddingLogicURL:
          createBiddingScriptURL({
              generateBid:
                  `let expectedAdComponents = ${JSON.stringify(interestGroupAdComponents)};
                   let adComponents = interestGroup.adComponents;
                   if (adComponents.length !== expectedAdComponents.length)
                     throw "Unexpected adComponents";
                   for (let i = 0; i < adComponents.length; ++i) {
                    if (adComponents[i].renderURL !== expectedAdComponents[i].renderURL ||
                        adComponents[i].metadata !== expectedAdComponents[i].metadata) {
                      throw "Unexpected adComponents";
                    }
                   }
                   return ${JSON.stringify(bid)}`,
              reportWin:
                  `registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});` }),
        ads: [{renderURL: renderURL}],
        adComponents: interestGroupAdComponents});

  if (!bid.adComponents || bid.adComponents.length === 0) {
    await runBasicFledgeAuctionAndNavigate(
      test, uuid,
      {decisionLogicURL: createDecisionScriptURL(
        uuid,
        { scoreAd: `if (browserSignals.adComponents !== undefined)
                      throw "adComponents should be undefined"`})});
  } else {
    await runBasicFledgeAuctionAndNavigate(
      test, uuid,
      {decisionLogicURL: createDecisionScriptURL(
        uuid,
          { scoreAd:
              `if (JSON.stringify(browserSignals.adComponents) !==
                       '${JSON.stringify(bid.adComponents)}') {
                 throw "Unexpected adComponents: " + JSON.stringify(browserSignals.adComponents);
               }`}),
        deprecatedRenderURLReplacements: deprecatedRenderURLReplacements
      });
  }

  await waitForObservedRequests(uuid, expectedTrackerURLs);
}

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);

  const renderURL = createRenderURL(uuid, `let status = "ok";
     const nestedConfigsLength = window.fence.getNestedConfigs().length
     // "getNestedConfigs()" should return a list of 40 configs, to avoid leaking
     // whether there were any component URLs to the page.
     if (nestedConfigsLength !== 40)
       status = "unexpected getNestedConfigs() length: " + nestedConfigsLength;
     window.fence.reportEvent({eventType: "beacon",
                               eventData: status,
                               destination: ["buyer"]});`);
  await joinInterestGroup(
      test, uuid,
      { biddingLogicURL:
          createBiddingScriptURL({
              generateBid:
                  'if (interestGroup.componentAds !== undefined) throw "unexpected componentAds"',
              reportWin:
                  `registerAdBeacon({beacon: "${createBidderBeaconURL(uuid)}"});` }),
        ads: [{renderUrl: renderURL}]});
  await runBasicFledgeAuctionAndNavigate(
      test, uuid,
      {decisionLogicURL: createDecisionScriptURL(
        uuid,
        { scoreAd: `if (browserSignals.adComponents !== undefined)
                      throw "adComponents should be undefined"`})});
  await waitForObservedRequests(uuid, [`${createBidderBeaconURL(uuid)}, body: ok`]);
}, 'Group has no component ads, no adComponents in bid.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);

  await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
      test,
      {uuid: uuid,
       interestGroupOverrides: {
           biddingLogicURL:
           createBiddingScriptURL({
               generateBid:
                   `return {bid: 1,
                            render: interestGroup.ads[0].renderUrl,
                            adComponents: []};`})}});
}, 'Group has no component ads, adComponents in bid is empty array.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(
      test, uuid, /*numComponentAdsInInterestGroup=*/2, /*componentAdsInBid=*/null,
      // Try to load ad components, even though there are none. This should load
      // about:blank in those frames, though that's not testible.
      // The waitForObservedRequests() call may see extra requests, racily, if
      // component ads not found in the bid are used.
      /*componentAdsToLoad=*/[0, 1]);
}, 'Group has component ads, but not used in bid (no adComponents field).');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(
      test, uuid, /*numComponentAdsInInterestGroup=*/2, /*componentAdsInBid=*/[],
      // Try to load ad components, even though there are none. This should load
      // about:blank in those frames, though that's not testible.
      // The waitForObservedRequests() call may see extra requests, racily, if
      // component ads not found in the bid are used.
      /*componentAdsToLoad=*/[0, 1]);
}, 'Group has component ads, but not used in bid (adComponents field empty array).');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(
      test, uuid, /*numComponentAdsInInterestGroup=*/2, /*componentAdsInBid=*/null,
      // Try to load ad components, even though there are none. This should load
      // about:blank in those frames, though that's not testible.
      // The waitForObservedRequests() call may see extra requests, racily, if
      // component ads not found in the bid are used.
      /*componentAdsToLoad=*/[0, 1], /*adMetadata=*/true);
}, 'Unused component ads with metadata.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);

  await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
      test,
      { uuid: uuid,
        interestGroupOverrides: {
            biddingLogicURL:
                createBiddingScriptURL({
                    generateBid:
                        `return {bid: 1,
                                 render: interestGroup.ads[0].renderUrl,
                                 adComponents: ["https://random.url.test/"]};`}),
            adComponents: [{renderURL: createComponentAdRenderURL(uuid, 0)}]}});
}, 'Unknown component ad URL in bid.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);

  await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
      test,
      { uuid: uuid,
        interestGroupOverrides: {
            biddingLogicURL:
                createBiddingScriptURL({
                    generateBid:
                        `return {bid: 1,
                                 render: interestGroup.ads[0].renderUrl,
                                 adComponents: [interestGroup.ads[0].renderUrl]};`}),
            adComponents: [{renderURL: createComponentAdRenderURL(uuid, 0)}]}});
}, 'Render URL used as component ad URL in bid.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);

  await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
      test,
      { uuid: uuid,
        interestGroupOverrides: {
            biddingLogicURL:
                createBiddingScriptURL({
                    generateBid:
                        `return {bid: 1, render: interestGroup.adComponents[0].renderURL};`}),
            adComponents: [{renderURL: createComponentAdRenderURL(uuid, 0)}]}});
}, 'Component ad URL used as render URL.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/2,
                                  /*componentAdsInBid=*/[0, 1], /*componentAdsToLoad=*/[0, 1]);
}, '2 of 2 component ads in bid and then shown.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/2,
                                  /*componentAdsInBid=*/[0, 1], /*componentAdsToLoad=*/[0, 1],
                                  /*adMetadata=*/true);
}, '2 of 2 component ads in bid and then shown, with metadata.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/20,
                                  /*componentAdsInBid=*/[3, 10], /*componentAdsToLoad=*/[0, 1]);
}, '2 of 20 component ads in bid and then shown.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const intsUpTo19 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/20,
                                  /*componentAdsInBid=*/intsUpTo19,
                                  /*componentAdsToLoad=*/intsUpTo19);
}, '20 of 20 component ads in bid and then shown.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const intsUpTo39 = [];
  for (let i = 0; i < 40; ++i) {
    intsUpTo39.push(i);
  }
  await runComponentAdLoadingTest(
      test, uuid, /*numComponentAdsInInterestGroup=*/ 40,
      /*componentAdsInBid=*/ intsUpTo39,
      /*componentAdsToLoad=*/ intsUpTo39);
}, '40 of 40 component ads in bid and then shown.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/20,
                                  /*componentAdsInBid=*/[1, 2, 3, 4, 5, 6],
                                  /*componentAdsToLoad=*/[1, 3]);
}, '6 of 20 component ads in bid, 2 shown.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  // It should be possible to load ads multiple times. Each loaded ad should request a new tracking
  // URLs, as they're fetched via XHRs, rather than reporting.
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/4,
                                  /*componentAdsInBid=*/[0, 1, 2, 3],
                                  /*componentAdsToLoad=*/[0, 1, 1, 0, 3, 3, 2, 2, 1, 0]);
}, '4 of 4 component ads shown multiple times.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/2,
                                  /*componentAdsInBid=*/[0, 0, 0, 0],
                                  /*componentAdsToLoad=*/[0, 1, 2, 3]);
}, 'Same component ad used multiple times in bid.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  // The bid only has one component ad, but the renderURL tries to load 5 component ads.
  // The others should all be about:blank. Can't test that, so just make sure there aren't
  // more requests than expected, and there's no crash.
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/2,
                                  /*componentAdsInBid=*/[0],
                                  /*componentAdsToLoad=*/[4, 3, 2, 1, 0]);
}, 'Load component ads not in bid.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const renderURL = createRenderURL(uuid);

  let adComponents = [];
  let adComponentsList = [];
  for (let i = 0; i < 41; ++i) {
    let componentRenderURL = createComponentAdTrackerURL(uuid, i);
    adComponents.push({renderURL: componentRenderURL});
    adComponentsList.push(componentRenderURL);
  }

  await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
      test,
      { uuid: uuid,
        interestGroupOverrides: {
            biddingLogicURL:
                createBiddingScriptURL({
                    generateBid:
                        `return {bid: 1,
                                 render: "${renderURL}",
                                 adComponents: ${JSON.stringify(adComponentsList)}};`}),
            ads: [{renderURL: renderURL}],
            adComponents: adComponents}});
}, '41 component ads not allowed in bid.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  const renderURL = createRenderURL(uuid);

  let adComponents = [];
  let adComponentsList = [];
  for (let i = 0; i < 41; ++i) {
    let componentRenderURL = createComponentAdTrackerURL(uuid, i);
    adComponents.push({renderURL: componentRenderURL});
    adComponentsList.push(adComponents[0].renderURL);
  }

  await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
      test,
      { uuid: uuid,
        interestGroupOverrides: {
            biddingLogicURL:
                createBiddingScriptURL({
            generateBid:
                `return {bid: 1,
                         render: "${renderURL}",
                         adComponents: ${JSON.stringify(adComponentsList)}};`}),
            ads: [{renderURL: renderURL}],
            adComponents: adComponents}});
}, 'Same component ad not allowed 41 times in bid.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);

  // The component ad's render URL will try to send buyer and seller reports,
  // which should not be sent (but not throw an exception), and then request a
  // a tracker URL via fetch, which should be requested from the server.
  const componentRenderURL =
      createRenderURL(
        uuid,
        `window.fence.reportEvent({eventType: "beacon",
                                   eventData: "Should not be sent",
                                   destination: ["buyer", "seller"]});
         fetch("${createComponentAdTrackerURL(uuid, 0)}");`);

  const renderURL = createRenderURL(
      uuid,
      `let fencedFrame = document.createElement("fencedframe");
       fencedFrame.mode = "opaque-ads";
       fencedFrame.config = window.fence.getNestedConfigs()[0];
       document.body.appendChild(fencedFrame);

       async function waitForRequestAndSendBeacons() {
         // Wait for the nested fenced frame to request its tracker URL.
         await waitForObservedRequests("${uuid}", ["${createComponentAdTrackerURL(uuid, 0)}"]);

         // Now that the tracker URL has been received, the component ad has tried to
         // send a beacon, so have the main renderURL send a beacon, which should succeed
         // and should hopefully be sent after the component ad's beacon, if it was
         // going to (incorrectly) send one.
         window.fence.reportEvent({eventType: "beacon",
                                   eventData: "top-ad",
                                   destination: ["buyer", "seller"]});
       }
       waitForRequestAndSendBeacons();`);

  await joinInterestGroup(
      test, uuid,
      { biddingLogicURL:
          createBiddingScriptURL({
              generateBid:
                  `return {bid: 1,
                           render: "${renderURL}",
                           adComponents: ["${componentRenderURL}"]};`,
              reportWin:
                  `registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});` }),
        ads: [{renderURL: renderURL}],
        adComponents: [{renderURL: componentRenderURL}]});

  await runBasicFledgeAuctionAndNavigate(
    test, uuid,
    {decisionLogicURL: createDecisionScriptURL(
        uuid,
        { reportResult: `registerAdBeacon({beacon: '${createSellerBeaconURL(uuid)}'});` }) });

  // Only the renderURL should have sent any beacons, though the component ad should have sent
  // a tracker URL fetch request.
  await waitForObservedRequests(uuid, [createComponentAdTrackerURL(uuid, 0),
                                       `${createBidderBeaconURL(uuid)}, body: top-ad`,
                                       `${createSellerBeaconURL(uuid)}, body: top-ad`]);


}, 'Reports not sent from component ad.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/1,
                                  /*componentAdsInBid=*/[0], /*componentAdsToLoad=*/[0], false, { '%%EXAMPLE-MACRO%%': 'SSP' });
}, 'component ad with render url replacements with percents.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/1,
                                  /*componentAdsInBid=*/[0], /*componentAdsToLoad=*/[0], false, { '${EXAMPLE-MACRO}': 'SSP' });
}, 'component ad with render url replacements with brackets.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/1,
                                  /*componentAdsInBid=*/[0], /*componentAdsToLoad=*/[0], false, { '${EXAMPLE-MACRO-1}': 'SSP-1', '%%EXAMPLE-MACRO-2%%': 'SSP-2' });
}, 'component ad with render url replacements with multiple replacements.');

subsetTest(promise_test, async test => {
  const uuid = generateUuid(test);
  await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/3,
                                  /*componentAdsInBid=*/[0,1,2], /*componentAdsToLoad=*/[0,1,2], false, { '${EXAMPLE-MACRO-1}': 'SSP-1', '%%EXAMPLE-MACRO-2%%': 'SSP-2' });
}, 'component ad with render url replacements with multiple replacements, and multiple component ads.');
