// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Flags: --no-legacy-const --harmony-sloppy --harmony-sloppy-let
// Flags: --harmony-sloppy-function --harmony-destructuring-bind

// Test Annex B 3.3 semantics for functions declared in blocks in sloppy mode.
// http://www.ecma-international.org/ecma-262/6.0/#sec-block-level-function-declarations-web-legacy-compatibility-semantics

(function overridingLocalFunction() {
  var x = [];
  assertEquals('function', typeof f);
  function f() {
    x.push(1);
  }
  f();
  {
    f();
    function f() {
      x.push(2);
    }
    f();
  }
  f();
  {
    f();
    function f() {
      x.push(3);
    }
    f();
  }
  f();
  assertArrayEquals([1, 2, 2, 2, 3, 3, 3], x);
})();

(function newFunctionBinding() {
  var x = [];
  assertEquals('undefined', typeof f);
  {
    f();
    function f() {
      x.push(2);
    }
    f();
  }
  f();
  {
    f();
    function f() {
      x.push(3);
    }
    f();
  }
  f();
  assertArrayEquals([2, 2, 2, 3, 3, 3], x);
})();

(function shadowingLetDoesntBind() {
  let f = 1;
  assertEquals(1, f);
  {
    let y = 3;
    function f() {
      y = 2;
    }
    f();
    assertEquals(2, y);
  }
  assertEquals(1, f);
})();

(function shadowingClassDoesntBind() {
  class f { }
  assertEquals('class f { }', f.toString());
  {
    let y = 3;
    function f() {
      y = 2;
    }
    f();
    assertEquals(2, y);
  }
  assertEquals('class f { }', f.toString());
})();

(function shadowingConstDoesntBind() {
  const f = 1;
  assertEquals(1, f);
  {
    let y = 3;
    function f() {
      y = 2;
    }
    f();
    assertEquals(2, y);
  }
  assertEquals(1, f);
})();

(function shadowingVarBinds() {
  var f = 1;
  assertEquals(1, f);
  {
    let y = 3;
    function f() {
      y = 2;
    }
    f();
    assertEquals(2, y);
  }
  assertEquals('function', typeof f);
})();

(function conditional() {
  if (true) {
    function f() { return 1; }
  } else {
    function f() { return 2; }
  }
  assertEquals(1, f());

  if (false) {
    function g() { return 1; }
  } else {
    function g() { return 2; }
  }
  assertEquals(2, g());
})();

(function skipExecution() {
  {
    function f() { return 1; }
  }
  assertEquals(1, f());
  {
    function f() { return 2; }
  }
  assertEquals(2, f());
  L: {
    assertEquals(3, f());
    break L;
    function f() { return 3; }
  }
  assertEquals(2, f());
})();

// Test that shadowing arguments is fine
(function shadowArguments(x) {
  assertArrayEquals([1], arguments);
  {
    assertEquals('function', typeof arguments);
    function arguments() {}
    assertEquals('function', typeof arguments);
  }
  assertEquals('function', typeof arguments);
})(1);

// Shadow function parameter
(function shadowParameter(x) {
  assertEquals(1, x);
  {
    function x() {}
  }
  assertEquals('function', typeof x);
})(1);

// Shadow function parameter
(function shadowDefaultParameter(x = 0) {
  assertEquals(1, x);
  {
    function x() {}
  }
  // TODO(littledan): Once destructured parameters are no longer
  // let-bound, enable this assertion. This is the core of the test.
  // assertEquals('function', typeof x);
})(1);

(function shadowRestParameter(...x) {
  assertArrayEquals([1], x);
  {
    function x() {}
  }
  // TODO(littledan): Once destructured parameters are no longer
  // let-bound, enable this assertion. This is the core of the test.
  // assertEquals('function', typeof x);
})(1);

assertThrows(function notInDefaultScope(x = y) {
  {
    function y() {}
  }
  assertEquals('function', typeof y);
  assertEquals(x, undefined);
}, ReferenceError);

// Test that hoisting from blocks does happen in global scope
function globalHoisted() { return 0; }
{
  function globalHoisted() { return 1; }
}
assertEquals(1, globalHoisted());

// Also happens when not previously defined
assertEquals(undefined, globalUndefinedHoisted);
{
  function globalUndefinedHoisted() { return 1; }
}
assertEquals(1, globalUndefinedHoisted());
var globalUndefinedHoistedDescriptor =
    Object.getOwnPropertyDescriptor(this, "globalUndefinedHoisted");
assertFalse(globalUndefinedHoistedDescriptor.configurable);
assertTrue(globalUndefinedHoistedDescriptor.writable);
assertTrue(globalUndefinedHoistedDescriptor.enumerable);
assertEquals(1, globalUndefinedHoistedDescriptor.value());

// When a function property is hoisted, it should be
// made enumerable.
// BUG(v8:4451)
Object.defineProperty(this, "globalNonEnumerable", {
  value: false,
  configurable: true,
  writable: true,
  enumerable: false
});
eval("{function globalNonEnumerable() { return 1; }}");
var globalNonEnumerableDescriptor
    = Object.getOwnPropertyDescriptor(this, "globalNonEnumerable");
// BUG(v8:4451): Should be made non-configurable
assertTrue(globalNonEnumerableDescriptor.configurable);
assertTrue(globalNonEnumerableDescriptor.writable);
// BUG(v8:4451): Should be made enumerable
assertFalse(globalNonEnumerableDescriptor.enumerable);
assertEquals(1, globalNonEnumerableDescriptor.value());

// When a function property is hoisted, it should be overwritten and
// made writable and overwritten, even if the property was non-writable.
Object.defineProperty(this, "globalNonWritable", {
  value: false,
  configurable: true,
  writable: false,
  enumerable: true
});
eval("{function globalNonWritable() { return 1; }}");
var globalNonWritableDescriptor
    = Object.getOwnPropertyDescriptor(this, "globalNonWritable");
// BUG(v8:4451): Should be made non-configurable
assertTrue(globalNonWritableDescriptor.configurable);
// BUG(v8:4451): Should be made writable
assertFalse(globalNonWritableDescriptor.writable);
assertFalse(globalNonEnumerableDescriptor.enumerable);
// BUG(v8:4451): Should be overwritten
assertEquals(false, globalNonWritableDescriptor.value);

// Test that hoisting from blocks does happen in an eval
eval(`
  function evalHoisted() { return 0; }
  {
    function evalHoisted() { return 1; }
  }
  assertEquals(1, evalHoisted());
`);

// Test that hoisting from blocks happens from eval in a function
!function() {
  eval(`
    function evalInFunctionHoisted() { return 0; }
    {
      function evalInFunctionHoisted() { return 1; }
    }
    assertEquals(1, evalInFunctionHoisted());
  `);
}();

let dontHoistGlobal;
{ function dontHoistGlobal() {} }
assertEquals(undefined, dontHoistGlobal);

let dontHoistEval;
// BUG(v8:) This shouldn't hoist and shouldn't throw
var throws = false;
try {
  eval("{ function dontHoistEval() {} }");
} catch (e) {
  throws = true;
}
assertTrue(throws);

// When the global object is frozen, silently don't hoist
// Currently this actually throws BUG(v8:4452)
Object.freeze(this);
throws = false;
try {
  eval('{ function hoistWhenFrozen() {} }');
} catch (e) {
  throws = true;
}
assertFalse(this.hasOwnProperty("hoistWhenFrozen"));
assertThrows(() => hoistWhenFrozen, ReferenceError);
// Should be assertFalse BUG(v8:4452)
assertTrue(throws);