// Copyright 2016 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-always-opt --no-opt

// The first part of this file is copied over from debug-set-variable-value.js
// (a few tests were removed because they make no sense for modules). The second
// part is new.

var Debug = debug.Debug;

// Accepts a function/closure 'fun' that must have a debugger statement inside.
// A variable 'variable_name' must be initialized before debugger statement
// and returned after the statement. The test will alter variable value when
// on debugger statement and check that returned value reflects the change.
function RunPauseTest(scope_number, expected_old_result, variable_name,
    new_value, expected_new_result, fun) {
  var actual_old_result = fun();
  assertEquals(expected_old_result, actual_old_result);

  var listener_delegate;
  var listener_called = false;
  var exception = null;

  function listener_delegate(exec_state) {
    var scope = exec_state.frame(0).scope(scope_number);
    scope.setVariableValue(variable_name, new_value);
  }

  function listener(event, exec_state, event_data, data) {
    try {
      if (event == Debug.DebugEvent.Break) {
        listener_called = true;
        listener_delegate(exec_state);
      }
    } catch (e) {
      exception = e;
    }
  }

  // Add the debug event listener.
  Debug.setListener(listener);

  var actual_new_result;
  try {
    actual_new_result = fun();
  } finally {
    Debug.setListener(null);
  }

  if (exception != null) {
   assertUnreachable("Exception in listener\n" + exception.stack);
  }
  assertTrue(listener_called);

  assertEquals(expected_new_result, actual_new_result);
}


function ClosureTestCase(scope_index, old_result, variable_name, new_value,
    new_result, success_expected, factory) {
  this.scope_index_ = scope_index;
  this.old_result_ = old_result;
  this.variable_name_ = variable_name;
  this.new_value_ = new_value;
  this.new_result_ = new_result;
  this.success_expected_ = success_expected;
  this.factory_ = factory;
}

ClosureTestCase.prototype.run_pause_test = function() {
  var th = this;
  var fun = this.factory_(true);
  this.run_and_catch_(function() {
    RunPauseTest(th.scope_index_ + 1, th.old_result_, th.variable_name_,
        th.new_value_, th.new_result_, fun);
  });
}

ClosureTestCase.prototype.run_and_catch_ = function(runnable) {
  if (this.success_expected_) {
    runnable();
  } else {
    assertThrows(runnable);
  }
}


// Test scopes visible from closures.

var closure_test_cases = [
  new ClosureTestCase(0, 'cat', 'v1', 5, 5, true,
      function Factory(debug_stop) {
    var v1 = 'cat';
    return function() {
      if (debug_stop) debugger;
      return v1;
    }
  }),

  new ClosureTestCase(0, 4, 't', 7, 9, true, function Factory(debug_stop) {
    var t = 2;
    var r = eval("t");
    return function() {
      if (debug_stop) debugger;
      return r + t;
    }
  }),

  new ClosureTestCase(0, 6, 't', 10, 13, true, function Factory(debug_stop) {
    var t = 2;
    var r = eval("t = 3");
    return function() {
      if (debug_stop) debugger;
      return r + t;
    }
  }),

  new ClosureTestCase(2, 'capybara', 'foo', 77, 77, true,
      function Factory(debug_stop) {
    var foo = "capybara";
    return (function() {
      var bar = "fish";
      try {
        throw {name: "test exception"};
      } catch (e) {
        return function() {
          if (debug_stop) debugger;
          bar = "beast";
          return foo;
        }
      }
    })();
  }),

  new ClosureTestCase(0, 'AlphaBeta', 'eee', 5, '5Beta', true,
      function Factory(debug_stop) {
    var foo = "Beta";
    return (function() {
      var bar = "fish";
      try {
        throw "Alpha";
      } catch (eee) {
        return function() {
          if (debug_stop) debugger;
          return eee + foo;
        }
      }
    })();
  })
];

for (var i = 0; i < closure_test_cases.length; i++) {
  closure_test_cases[i].run_pause_test();
}


// Test local scope.

RunPauseTest(0, 'HelloYou', 'u', 'We', 'HelloWe', (function Factory() {
  return function() {
    var u = "You";
    var v = "Hello";
    debugger;
    return v + u;
  }
})());

RunPauseTest(0, 'Helloworld', 'p', 'GoodBye', 'HelloGoodBye',
    (function Factory() {
  function H(p) {
    var v = "Hello";
    debugger;
    return v + p;
  }
  return function() {
    return H("world");
  }
})());

RunPauseTest(0, 'mouse', 'v1', 'dog', 'dog', (function Factory() {
  return function() {
    var v1 = 'cat';
    eval("v1 = 'mouse'");
    debugger;
    return v1;
  }
})());

// Check that we correctly update local variable that
// is referenced from an inner closure.
RunPauseTest(0, 'Blue', 'v', 'Green', 'Green', (function Factory() {
  return function() {
    function A() {
      var v = "Blue";
      function Inner() {
        return void v;
      }
      debugger;
      return v;
    }
    return A();
  }
})());

// Check that we correctly update parameter, that is known to be stored
// both on stack and in heap.
RunPauseTest(0, 5, 'p', 2012, 2012, (function Factory() {
  return function() {
    function A(p) {
      function Inner() {
        return void p;
      }
      debugger;
      return p;
    }
    return A(5);
  }
})());


////////////////////////////////////////////////////////////////////////////////
// From here on we test the module scope.
////////////////////////////////////////////////////////////////////////////////


// Non-existing variable.
{
  let exception;
  function listener(event, exec_state) {
    if (event == Debug.DebugEvent.Break) {
      let module_scope = exec_state.frame().scope(1);
      assertEquals(debug.ScopeType.Module, module_scope.scopeType());
      try {
        module_scope.setVariableValue('spargel', 42);
      } catch(e) { exception = e; }
    }
  }

  Debug.setListener(listener);
  assertThrows(() => spargel, ReferenceError);
  debugger;
  assertThrows(() => spargel, ReferenceError);
  assertTrue(exception !== undefined);
}


// Local (non-exported) variable.
let salat = 12;
{
  function listener(event, exec_state) {
    if (event == Debug.DebugEvent.Break) {
      let module_scope = exec_state.frame().scope(1);
      assertEquals(debug.ScopeType.Module, module_scope.scopeType());
      module_scope.setVariableValue('salat', 42);
    }
  }

  Debug.setListener(listener);
  assertEquals(12, salat);
  debugger;
  assertEquals(42, salat);
}


// Local (non-exported) variable, nested access.
let salad = 12;
{
  function listener(event, exec_state) {
    if (event == Debug.DebugEvent.Break) {
      let scope_count = exec_state.frame().scopeCount();
      let module_scope = exec_state.frame().scope(1);
      assertEquals(debug.ScopeType.Module, module_scope.scopeType());
      module_scope.setVariableValue('salad', 42);
    }
  }

  Debug.setListener(listener);
  function foo() {
    assertEquals(12, salad);
    debugger;
    assertEquals(42, salad);
  };
  foo();
}


// Exported variable.
export let salami = 1;
{
  function listener(event, exec_state) {
    if (event == Debug.DebugEvent.Break) {
      let module_scope = exec_state.frame().scope(1);
      assertEquals(debug.ScopeType.Module, module_scope.scopeType());
      module_scope.setVariableValue('salami', 2);
    }
  }

  Debug.setListener(listener);
  assertEquals(1, salami);
  debugger;
  assertEquals(2, salami);
}


// Exported variable, nested access.
export let ham = 1;
{
  function listener(event, exec_state) {
    if (event == Debug.DebugEvent.Break) {
      let scope_count = exec_state.frame().scopeCount();
      let module_scope = exec_state.frame().scope(1);
      assertEquals(debug.ScopeType.Module, module_scope.scopeType());
      module_scope.setVariableValue('ham', 2);
    }
  }

  Debug.setListener(listener);
  function foo() {
    assertEquals(1, ham);
    debugger;
    assertEquals(2, ham);
  };
  foo();
}


// Imported variable. Setting is currently not supported.
import { salami as wurst } from "./debug-modules-set-variable-value.mjs";
{
  let exception;
  function listener(event, exec_state) {
    if (event == Debug.DebugEvent.Break) {
      let module_scope = exec_state.frame().scope(1);
      assertEquals(debug.ScopeType.Module, module_scope.scopeType());
      try {
        module_scope.setVariableValue('wurst', 3);
      } catch(e) { exception = e; }
    }
  }

  Debug.setListener(listener);
  assertEquals(2, wurst);
  debugger;
  assertEquals(2, wurst);
  assertTrue(exception !== undefined);
}


// Imported variable, nested access. Setting is currently not supported.
import { salami as wurstl } from "./debug-modules-set-variable-value.mjs";
{
  let exception;
  function listener(event, exec_state) {
    if (event == Debug.DebugEvent.Break) {
      let scope_count = exec_state.frame().scopeCount();
      let module_scope = exec_state.frame().scope(2);
      assertEquals(debug.ScopeType.Module, module_scope.scopeType());
      try {
        module_scope.setVariableValue('wurstl', 3);
      } catch(e) { exception = e; }
    }
  }

  Debug.setListener(listener);
  function foo() {
    assertEquals(2, wurstl);
    debugger;
    assertEquals(2, wurstl);
    assertTrue(exception !== undefined);
  };
  foo();
}


Debug.setListener(null);