// |jit-test| --enable-symbols-as-weakmap-keys

function checkPropertyDescriptor(obj, property, writable, enumerable,
                                 configurable) {
  let desc = Object.getOwnPropertyDescriptor(obj, property);
  assertEq(typeof desc, "object");
  assertEq(desc.writable, writable);
  assertEq(desc.enumerable, enumerable);
  assertEq(desc.configurable, configurable);
}

function assertThrowsTypeError(thunk) {
  let error;
  try {
    thunk();
  } catch (e) {
    error = e;
  }
  assertEq(error instanceof TypeError, true);
}

// 3.1 The FinalizationRegistry Constructor
assertEq(typeof this.FinalizationRegistry, "function");

// 3.1.1 FinalizationRegistry ( cleanupCallback ) 
assertThrowsTypeError(() => new FinalizationRegistry());
assertThrowsTypeError(() => new FinalizationRegistry(1));
new FinalizationRegistry(x => 0);

// 3.2 Properties of the FinalizationRegistry Constructor
assertEq(Object.getPrototypeOf(FinalizationRegistry), Function.prototype);

// 3.2.1 FinalizationRegistry.prototype
checkPropertyDescriptor(FinalizationRegistry, 'prototype', false, false, false);

// 3.3 Properties of the FinalizationRegistry Prototype Object
let proto = FinalizationRegistry.prototype;
assertEq(Object.getPrototypeOf(proto), Object.prototype);

// 3.3.1 FinalizationRegistry.prototype.constructor
assertEq(proto.constructor, FinalizationRegistry);

// 3.3.2 FinalizationRegistry.prototype.register ( target , holdings [, unregisterToken ] )
assertEq(proto.hasOwnProperty('register'), true);
assertEq(typeof proto.register, 'function');

// 3.3.3 FinalizationRegistry.prototype.unregister ( unregisterToken )
assertEq(proto.hasOwnProperty('unregister'), true);
assertEq(typeof proto.unregister, 'function');

// 3.3.4 FinalizationRegistry.prototype.cleanupSome ( [ callback ] )
assertEq(proto.hasOwnProperty('cleanupSome'), true);
assertEq(typeof proto.cleanupSome, 'function');

// 3.3.5 FinalizationRegistry.prototype [ @@toStringTag ]
assertEq(proto[Symbol.toStringTag], "FinalizationRegistry");
checkPropertyDescriptor(proto, Symbol.toStringTag, false, false, true);

// 3.4 Properties of FinalizationRegistry Instances
let registry = new FinalizationRegistry(x => 0);
assertEq(Object.getPrototypeOf(registry), proto);
assertEq(Object.getOwnPropertyNames(registry).length, 0);

let heldValues = [];
registry = new FinalizationRegistry(value => {
  heldValues.push(value);
});

// Test a single target.
heldValues = [];
registry.register({}, 42);
gc();
drainJobQueue();
assertEq(heldValues.length, 1);
assertEq(heldValues[0], 42);

// Test multiple targets.
heldValues = [];
for (let i = 0; i < 100; i++) {
  registry.register({}, i);
}
gc();
drainJobQueue();
assertEq(heldValues.length, 100);
heldValues = heldValues.sort((a, b) => a - b);
for (let i = 0; i < 100; i++) {
  assertEq(heldValues[i], i);
}

// Test a single object in multiple registries
heldValues = [];
let heldValues2 = [];
let registry2 = new FinalizationRegistry(value => {
  heldValues2.push(value);
});
{
  let object = {};
  registry.register(object, 1);
  registry2.register(object, 2);
  object = null;
}
gc();
drainJobQueue();
assertEq(heldValues.length, 1);
assertEq(heldValues[0], 1);
assertEq(heldValues2.length, 1);
assertEq(heldValues2[0], 2);

// Unregister a single target.
heldValues = [];
let token = {};
registry.register({}, 1, token);
registry.unregister(token);
gc();
drainJobQueue();
assertEq(heldValues.length, 0);

// Unregister multiple targets.
heldValues = [];
let token2 = {};
registry.register({}, 1, token);
registry.register({}, 2, token2);
registry.register({}, 3, token);
registry.register({}, 4, token2);
registry.unregister(token);
gc();
drainJobQueue();
assertEq(heldValues.length, 2);
heldValues = heldValues.sort((a, b) => a - b);
assertEq(heldValues[0], 2);
assertEq(heldValues[1], 4);

// Watch object in another global.
let other = newGlobal({newCompartment: true});
heldValues = [];
registry.register(evalcx('({})', other), 1);
gc();
drainJobQueue();
assertEq(heldValues.length, 1);
assertEq(heldValues[0], 1);

// Pass heldValues from another global.
let heldValue = evalcx('{}', other);
heldValues = [];
registry.register({}, heldValue);
gc();
drainJobQueue();
assertEq(heldValues.length, 1);
assertEq(heldValues[0], heldValue);

// Pass unregister token from another global.
token = evalcx('({})', other);
heldValues = [];
registry.register({}, 1, token);
gc();
drainJobQueue();
assertEq(heldValues.length, 1);
assertEq(heldValues[0], 1);
heldValues = [];
registry.register({}, 1, token);
registry.unregister(token);
gc();
drainJobQueue();
assertEq(heldValues.length, 0);

// FinalizationRegistry is designed to be subclassable.
class MyRegistry extends FinalizationRegistry {
  constructor(callback) {
    super(callback);
  }
}
let r2 = new MyRegistry(value => {
  heldValues.push(value);
});
heldValues = [];
r2.register({}, 42);
gc();
drainJobQueue();
assertEq(heldValues.length, 1);
assertEq(heldValues[0], 42);

// Test cleanupSome.
heldValues = [];
let r5 = new FinalizationRegistry(v => heldValues.push(v));
r5.register({}, 1);
r5.register({}, 2);
r5.register({}, 3);
gc();
r5.cleanupSome();
assertEq(heldValues.length, 3);
heldValues = heldValues.sort((a, b) => a - b);
assertEq(heldValues[0], 1);
assertEq(heldValues[1], 2);
assertEq(heldValues[2], 3);

// Test trying to call cleanupSome in callback.
let r6 = new FinalizationRegistry(x => {
  r6.cleanupSome();
});
r6.register({}, 1);
gc();
drainJobQueue();

// Test trying to call cleanupSome in callback with multiple values.
let callbackCounter7 = 0;
let r7 = new FinalizationRegistry(x => {
  callbackCounter7++;
  r7.cleanupSome();
});
r7.register({}, 1);
r7.register({}, 2);
r7.register({}, 3);
r7.register({}, 4);
gc();
drainJobQueue();
assertEq(callbackCounter7, 4);

// Test that targets don't keep the finalization registry alive.
let target = {};
registry = new FinalizationRegistry(value => undefined);
registry.register(target, 1);
let weakRef = new WeakRef(registry);
registry = undefined;
assertEq(typeof weakRef.deref(), 'object');
drainJobQueue();
gc();
assertEq(weakRef.deref(), undefined);
assertEq(typeof target, 'object');

// Test that targets don't keep the finalization registry alive when also
// used as the unregister token.
registry = new FinalizationRegistry(value => undefined);
registry.register(target, 1, target);
weakRef = new WeakRef(registry);
registry = undefined;
assertEq(typeof weakRef.deref(), 'object');
drainJobQueue();
gc();
assertEq(weakRef.deref(), undefined);
assertEq(typeof target, 'object');

// Test that cleanup doesn't happen if the finalization registry dies.
// todo: ok per spec if they both die in the same GC?
heldValues = [];
new FinalizationRegistry(value => {
  heldValues.push(value);
}).register({}, 1);
gc();
drainJobQueue();
assertEq(heldValues.length, 0);
