
load(libdir + "asserts.js");

class A {
  #x = 10

  x() {
    return this.#x;
  }
  ix() {
    this.#x++;
  }
  static readx(o) {
    return o.#x;
  }
  static optionalx(o) {
    return o?.#x;
  }

  static orEqual(o, v) {
    o.#x ||= v;
    return o.#x;
  }

  setX(v) {
    this.#x = v;
  }

  compoundInc() {
    this.#x += 1;
    return this.#x;
  }

  compoundDec() {
    this.#x -= 1;
    return this.#x;
  }

  #y = () => 'hi';
  invoke() {
    return this.#y();
  }

  static #z = 'static';
  gz() {
    return A.#z;
  }

  sz(o) {
    A.#z = o;
  }

  static sgz() {
    return this.#z;
  }

  static ssz(o) {
    this.#z = o;
  }

  static six(o) {
    o.#x++;
  }

  static dix(o) {
    o.#x--;
  }
};

for (var i = 0; i < 1000; i++) {
  var a = new A();
  assertEq(a.x(), 10);
  a.ix();
  assertEq(a.x(), 11);
  assertEq(A.readx(a), 11);
  assertEq(a.compoundInc(), 12);
  assertEq(A.orEqual(a, 13), 12);
  a.setX(null);
  assertEq(A.orEqual(a, 12), 12);
  assertEq(a.compoundDec(), 11);
  assertEq(a.invoke(), 'hi');
  assertEq(a.gz(), 'static');
  assertEq(A.sgz(), 'static');
  A.ssz(i);
  assertEq(A.sgz(), i);
  a.sz(i + 1);
  assertEq(A.sgz(), i + 1);
  A.ssz('static');  // reset for next iteration!

  assertEq(A.optionalx(a), 11);
  assertEq(A.optionalx(null), undefined);
  try {
    A.optionalx({});  // Should throw type error
    assertEq(0, 1);
  } catch (TypeError) {
  }
}

function testTypeErrors(v) {
  assertThrowsInstanceOf(() => A.readx(v), TypeError);  // Read value
  assertThrowsInstanceOf(() => A.six(v), TypeError);    // increment
  assertThrowsInstanceOf(() => A.dix(v), TypeError);    // decrement
}

testTypeErrors(undefined);  // Undefined
testTypeErrors({});         // Random object
testTypeErrors(1);          // Random primitive

assertThrowsInstanceOf(
  () => eval('class B extends class { #x; } { g() { return super.#x; } }'),
  SyntaxError);  // Access super.#private
assertThrowsInstanceOf(
  () => eval('class C { #x = 10; static #x = 14; }'),
  SyntaxError);  // Duplicate name declaration.
assertThrowsInstanceOf(
  () => eval('delete this.#x'),
  SyntaxError);  // deleting a private field in non-strict mode.
assertThrowsInstanceOf(
  () => evaluate(`
  class C {
    #x() {}
    m() { delete this?.#x; }
  }
`, {forceFullParse: false}), SyntaxError);
assertThrowsInstanceOf(
  () => evaluate(`
  class C {
    #x() {}
    m() { delete this.a?.b.#x; }
  }
`, {forceFullParse: false}), SyntaxError);

class B extends class {
  constructor(o) {
    return o;
  }
}
{
  #x = 12;
  static gx(o) {
    return o.#x;
  }
  static sx(o) {
    o.#x++;
  }
}

var bn = new B(1);
var bu = new B(undefined);

// Test we can read an outer classes private fields.
class Outer {
  #outer = 3;
  test() {
    let outerThis = this;
    class Inner {
      #inner = 2;
      test() {
        return outerThis.#outer;
      }
    }
    return new Inner().test();
  }
}

var o = new Outer;
assertEq(o.test(), 3);

// IC tests:

var alreadyConstructedB = new B();
assertEq(B.gx(alreadyConstructedB), 12);

function initIC(o) {
  new B(o);
}
var array = [];
// warm up init IC
for (var i = 1; i < 1000; i++) {
  var newB = {};
  initIC(newB);
}

// Successfully catch double initialization type error.
assertThrowsInstanceOf(() => initIC(alreadyConstructedB), TypeError);
// Do it again, to make sure we didn't attach a stub that is invalid.
assertThrowsInstanceOf(() => initIC(alreadyConstructedB), TypeError);

// Test getters work, and ICs can't be tricked. Setup an array of
//
//    [B, B, B, B, ..., {}, {}]
//
// Then test that as we execute the sudden appearance of {} doesn't
// trick our ICs into setting or getting anything -- do it twice
// to make sure that we didn't get a stub that is invalid.
var elements = [];
for (var i = 0; i < 99; i++) {
  elements.push(new B);
}
elements.push({});
elements.push({});

function getterCheck(e) {
  assertEq(B.gx(e), 12);
}

function setterCheck(e) {
  B.sx(e);
}

var checksPassed = 0;
try {
  for (var e of elements) {
    getterCheck(e);
    checksPassed++;
  }
  throw `Shouldn't arrive here`;
} catch (e) {
  if (!(e instanceof TypeError)) {
    throw e;
  }
  // All but last element should have done the right thing.
  assertEq(checksPassed, elements.length - 2);
}

checksPassed = 0;
try {
  for (var e of elements) {
    setterCheck(e);
    checksPassed++;
  }
  throw `Shouldn't arrive here`;
} catch (e) {
  if (!(e instanceof TypeError)) {
    throw e;
  }
  // All but last element should have done the right thing.
  assertEq(checksPassed, elements.length - 2);
}

// Verify setter did the thing, but throws in the correct places
for (var index in elements) {
  if (index < elements.length - 2) {
    assertEq(B.gx(elements[index]), 13);
  } else {
    assertThrowsInstanceOf(() => {
      B.gx(elements[index]);
    }, TypeError);
  }
}

// Megamorphic Cache Testing:
for (var i = 0; i < 100; i++) {
  var inputs = [{ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }, { e: 5 }, new Proxy({}, {})];
  for (var o of inputs) {
    assertThrowsInstanceOf(() => B.gx(o), TypeError);
    assertThrowsInstanceOf(() => B.sx(o), TypeError);
    new B(o);
    assertEq(B.gx(o), 12);
    B.sx(o);
    assertEq(B.gx(o), 13);
  }
}
