// 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: timeout=long
// META: variant=?1-10
// META: variant=?11-20
// META: variant=?21-30
// META: variant=?31-40
// META: variant=?41-50
// META: variant=?51-60
// META: variant=?61-70
// META: variant=?71-80
// META: variant=?81-last

"use strict";

// These tests are focused on joinAdInterestGroup() and leaveAdInterestGroup().
// Most join tests do not run auctions, but instead only check the result of
// the returned promise, since testing that interest groups are actually
// joined, and that each interestGroup field behaves as intended, are covered
// by other tests.

// Minimal fields needed for a valid interest group. Used in most test cases.
const BASE_INTEREST_GROUP = {
  owner: window.location.origin,
  name: 'default name',
}

// Each test case attempts to join and then leave an interest group, checking
// if any exceptions are thrown from either operation.
const SIMPLE_JOIN_LEAVE_TEST_CASES = [
  { expectJoinSucces: false,
    expectLeaveSucces: false,
    interestGroup: null
  },
  { expectJoinSucces: false,
    expectLeaveSucces: false,
    interestGroup: {}
  },

  // Basic success test case.
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: BASE_INTEREST_GROUP
  },

  // "owner" tests
  { expectJoinSucces: false,
    expectLeaveSucces: false,
    interestGroup: { name: 'default name' }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: false,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     owner: null}
  },
  { expectJoinSucces: false,
    expectLeaveSucces: false,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     owner: window.location.origin.replace('https', 'http')}
  },
  { expectJoinSucces: false,
    expectLeaveSucces: false,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     owner: window.location.origin.replace('https', 'wss')}
  },
  // Cross-origin joins and leaves are not allowed without .well-known
  // permissions.
  { expectJoinSucces: false,
    expectLeaveSucces: false,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     owner: '{{hosts[][www]}}' }
  },

  // "name" tests
  { expectJoinSucces: false,
    expectLeaveSucces: false,
    interestGroup: { owner: window.location.origin }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     name: ''}
  },

  // "priority" tests
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     priority: 1}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     priority: 0}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     priority: -1.5}
  },

  // "priorityVector" tests
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     priorityVector: null}
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     priorityVector: 1}
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     priorityVector: {a: 'apple'}}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     priorityVector: {}}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     priorityVector: {a: 1}}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     priorityVector: {'a': 1, 'b': -4.5, 'a.b': 0}}
  },

  // "prioritySignalsOverrides" tests
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     prioritySignalsOverrides: null}
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     prioritySignalsOverrides: 1}
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     prioritySignalsOverrides: {a: 'apple'}}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     prioritySignalsOverrides: {}}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     prioritySignalsOverrides: {a: 1}}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     prioritySignalsOverrides: {'a': 1, 'b': -4.5, 'a.b': 0}}
  },

  // "enableBiddingSignalsPrioritization" tests
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     enableBiddingSignalsPrioritization: true}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     enableBiddingSignalsPrioritization: false}
  },

  // "biddingLogicURL" tests
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     biddingLogicURL: null }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     biddingLogicURL: 'https://{{hosts[][www]}}/foo.js' }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     biddingLogicURL: 'data:text/javascript,Foo' }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     biddingLogicURL: `${window.location.origin}/foo.js`}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     biddingLogicURL: 'relative/path' }
  },

  // "biddingWasmHelperURL" tests
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     biddingWasmHelperURL: null }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     biddingWasmHelperURL: 'https://{{hosts[][www]}}/foo.js' }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     biddingWasmHelperURL: 'data:application/wasm,Foo' }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     biddingWasmHelperURL: `${window.location.origin}/foo.js`}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     biddingWasmHelperURL: 'relative/path' }
  },

  // "updateURL" tests
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     updateURL: null }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     updateURL: 'https://{{hosts[][www]}}/foo.js' }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     updateURL: 'data:application/wasm,Foo' }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     updateURL: `${window.location.origin}/foo.js`}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     updateURL: 'relative/path' }
  },

  // "executionMode" tests
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     executionMode: 'compatibility' }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     executionMode: 'groupByOrigin' }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     executionMode: 'unknownValuesAreValid' }
  },

  // "trustedBiddingSignalsURL" tests
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     trustedBiddingSignalsURL: null }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     trustedBiddingSignalsURL: 'https://{{hosts[][www]}}/foo.js' }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     trustedBiddingSignalsURL: 'data:application/json,{}' }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     trustedBiddingSignalsURL: `${window.location.origin}/foo.js`}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     trustedBiddingSignalsURL: 'relative/path' }
  },

  // "trustedBiddingSignalsKeys" tests
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     trustedBiddingSignalsKeys: null }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     trustedBiddingSignalsKeys: {}}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     trustedBiddingSignalsKeys: []}
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     trustedBiddingSignalsKeys: ['a', 4, 'Foo']}
  },

  // "userBiddingSignals" tests
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     userBiddingSignals: null }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     userBiddingSignals: 'foo' }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     userBiddingSignals: 15 }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     userBiddingSignals: [5, 'foo', [-6.4, {a: 'b'}]] }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     userBiddingSignals: {a: [5, 'foo', {b: -6.4}] }}
  },

  // "ads" tests
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     ads: null }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     ads: 5 }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     ads: {} }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     ads: [] }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     ads: [{}] }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     ads: [{metadata: [{a:'b'}, 'c'], 1:[2,3]}] }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     ads: [{renderURL: 'https://somewhere.test/',
                            adRenderId: 'thirteenChars' }] }
  },

  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     ads: [{renderURL: 'https://somewhere.test/'}] }
  },

  // "adComponents" tests
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     adComponents: null }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     adComponents: 5 }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     adComponents: [{}] }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     adComponents: [{metadata: [{a:'b'}, 'c'], 1:[2,3]}] }
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     adComponents: [{renderURL: 'https://somewhere.test/',
                                     adRenderId: 'More than twelve characters'}] }
  },
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     adComponents: [{renderURL: 'https://somewhere.test/'}] }
  },

  // Miscellaneous tests.
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
                     extra: false,
                     fields: {do:'not'},
                     matter: 'at',
                     all: [3,4,5] }
  },

  // Interest group dictionaries must be less than 1 MB (1048576 bytes), so
  // test that here by using a large name on an otherwise valid interest group
  // dictionary. The first case is the largest name value that still results in
  // a valid dictionary, whereas the second test case produces a dictionary
  // that's one byte too large.
  { expectJoinSucces: true,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
      name: 'a'.repeat(1048516)
    },
    testCaseName: "Largest possible interest group dictionary",
  },
  { expectJoinSucces: false,
    expectLeaveSucces: true,
    interestGroup: { ...BASE_INTEREST_GROUP,
      name: 'a'.repeat(1048517)
    },
    testCaseName: "Oversized interest group dictionary",
  },
];

for (let testCase of SIMPLE_JOIN_LEAVE_TEST_CASES) {
  var test_name = 'Join and leave interest group: ';
  if ('testCaseName' in testCase) {
    test_name += testCase.testCaseName;
  } else {
    test_name += JSON.stringify(testCase);
  }

  subsetTest(promise_test, (async (testCase) => {
    const INTEREST_GROUP_LIFETIME_SECS = 1;

    let join_promise = navigator.joinAdInterestGroup(testCase.interestGroup,
                                                     INTEREST_GROUP_LIFETIME_SECS);
    assert_true(join_promise instanceof Promise, "join should return a promise");
    if (testCase.expectJoinSucces) {
      assert_equals(await join_promise, undefined);
    } else {
      let joinExceptionThrown = false;
      try {
        await join_promise;
      } catch (e) {
        joinExceptionThrown = true;
      }
      assert_true(joinExceptionThrown, 'Exception not thrown on join.');
    }

    let leave_promise = navigator.leaveAdInterestGroup(testCase.interestGroup);
    assert_true(leave_promise instanceof Promise, "leave should return a promise");
    if (testCase.expectLeaveSucces) {
      assert_equals(await leave_promise, undefined);
    } else {
      let leaveExceptionThrown = false;
      try {
        await leave_promise;
      } catch (e) {
        leaveExceptionThrown = true;
      }
      assert_true(leaveExceptionThrown, 'Exception not thrown on leave.');
    }
  }).bind(undefined, testCase), test_name);
}

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

  // Joining an interest group without a bidding script and run an auction.
  // There should be no winner.
  await joinInterestGroup(test, uuid, { biddingLogicURL: null });
  assert_equals(null, await runBasicFledgeAuction(test, uuid),
                'Auction unexpectedly had a winner');

  // Joining an interest group with a bidding script and the same owner/name as
  // the previously joined interest group, and re-run the auction. There should
  // be a winner this time.
  await joinInterestGroup(test, uuid);
  let config = await runBasicFledgeAuction(test, uuid);
  assert_true(config instanceof FencedFrameConfig,
              'Wrong value type returned from auction: ' +
              config.constructor.name);

  // Re-join the first interest group, and re-run the auction. The interest
  // group should be overwritten again, and there should be no winner.
  await joinInterestGroup(test, uuid, { biddingLogicURL: null });
  assert_equals(null, await runBasicFledgeAuction(test, uuid),
                'Auction unexpectedly had a winner');
}, 'Join same interest group overwrites old matching group.');

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

  // Join an interest group, run an auction to make sure it was joined.
  await joinInterestGroup(test, uuid);
  let config = await runBasicFledgeAuction(test, uuid);
  assert_true(config instanceof FencedFrameConfig,
              'Wrong value type returned from auction: ' +
              config.constructor.name);

  // Leave the interest group, re-run the auction. There should be no winner.
  await leaveInterestGroup();
  assert_equals(null, await runBasicFledgeAuction(test, uuid),
                'Auction unexpectedly had a winner');
}, 'Leaving interest group actually leaves interest group.');

subsetTest(promise_test, async test => {
  // This should not throw.
  await leaveInterestGroup({ name: 'Never join group' });
}, 'Leave an interest group that was never joined.');

///////////////////////////////////////////////////////////////////////////////
// Expiration tests
///////////////////////////////////////////////////////////////////////////////

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

  // Joins the default interest group, with a 0.2 second duration.
  await joinInterestGroup(test, uuid, {}, 0.2);

  // Keep on running auctions until interest group duration expires.
  // Unfortunately, there's no duration that's guaranteed to be long enough to
  // be be able to win an auction once, but short enough to prevent this test
  // from running too long, so can't check the interest group won at least one
  // auction.
  while (await runBasicFledgeAuction(test, uuid) !== null);
}, 'Interest group duration.');

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

  // Join interest group with a duration of -600. The interest group should
  // immediately expire, and not be allowed to participate in auctions.
  await joinInterestGroup(test, uuid, {}, -600);
  assert_true(await runBasicFledgeAuction(test, uuid) === null);
}, 'Interest group duration of -600.');

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

  // Join a long-lived interest group.
  await joinInterestGroup(test, uuid, {}, 600);

  // Make sure interest group with a non-default timeout was joined.
  assert_true(await runBasicFledgeAuction(test, uuid) !== null);

  // Re-join interest group with a duration value of 0.2 seconds.
  await joinInterestGroup(test, uuid, {}, 0.2);

  // Keep on running auctions until interest group expires.
  while (await runBasicFledgeAuction(test, uuid) !== null);
}, 'Interest group test with overwritten duration.');

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

  // Join a long-lived interest group.
  await joinInterestGroup(test, uuid, {}, 600);

  // Re-join interest group with a duration value of 0.2 seconds. The new
  // duration should take precedence, and the interest group should immediately
  // expire.
  await joinInterestGroup(test, uuid, {}, -600);
  assert_true(await runBasicFledgeAuction(test, uuid) === null);
}, 'Interest group test with overwritten duration of -600.');
