/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Bug 727429: Test the debugger watch expressions.
 */

const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html";

function test() {
  // Debug test slaves are a bit slow at this test.
  requestLongerTimeout(2);

  let gTab, gPanel, gDebugger;
  let gWatch, gVariables;

  let options = {
    source: TAB_URL,
    line: 1
  };
  initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
    gTab = aTab;
    gPanel = aPanel;
    gDebugger = gPanel.panelWin;
    gWatch = gDebugger.DebuggerView.WatchExpressions;
    gVariables = gDebugger.DebuggerView.Variables;

    gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });

    addExpressions();
    performTest()
      .then(finishTest)
      .then(() => closeDebuggerAndFinish(gPanel))
      .catch(aError => {
        ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
      });
  });

  let totalExpressionsAdded = 0;

  function addExpressions() {
    function addExpression(aExpression) {
      gWatch.addExpression(aExpression);
      ++totalExpressionsAdded;
    }

    is(totalExpressionsAdded, 0, "addExpressions must only be called once");

    // The order in which expressions are added here almost doesn't matter for
    // this test's purposes.  (Excepting only the position of the final test.)
    // Use the same order as in `checkWatchExpressions` for simplicity.
    addExpression("'a'");
    addExpression("\"a\"");
    addExpression("'a\"\"'");
    addExpression("\"a''\"");
    addExpression("?");
    addExpression("a");
    addExpression("this");
    addExpression("this.canada");
    addExpression("[1, 2, 3]");
    addExpression("x = [1, 2, 3]");
    addExpression("y = [1, 2, 3]; y.test = 4");
    addExpression("z = [1, 2, 3]; z.test = 4; z");
    addExpression("t = [1, 2, 3]; t.test = 4; !t");
    addExpression("arguments[0]");
    addExpression("encodeURI(\"\\\")");
    addExpression("decodeURI(\"\\\")");
    addExpression("decodeURIComponent(\"%\")");
    addExpression("//");
    addExpression("// 42");
    addExpression("{}.foo");
    addExpression("{}.foo()");
    addExpression("({}).foo()");
    addExpression("new Array(-1)");
    addExpression("4.2.toExponential(-4.2)");
    addExpression("throw new Error(\"bazinga\")");
    addExpression("({ get error() { throw new Error(\"bazinga\") } }).error");

    // https://bugzilla.mozilla.org/show_bug.cgi?id=1205353
    // BZ#1205353 - wrong result for string replace with backslash-dollar.
    addExpression("'$$$'.replace(/\\$/, 'foo')");

    // Functions with parameters that aren't just a simple series of names can't
    // be made strict mode code by adding 'use strict'.  The resulting syntax
    // error message currently contains U+022 QUOTATION MARK characters that
    // DebuggerController.StackFrames.syncWatchExpressions at one time
    // mishandled: test that handling here.
    addExpression("function x(a = 3) { 'use strict'; } 'NOT EVALUATED'");
    addExpression("function y(...b) { 'use strict'; } 'NOT EVALUATED'");
    addExpression("function z([]) { 'use strict'; } 'NOT EVALUATED'");
    addExpression("function w({}) { 'use strict'; } 'NOT EVALUATED'");

    // Add a working expression to verify that earlier failing expressions don't
    // halt stuff.
    addExpression("42");

    // For the moment, this watch-expression must be last: by throwing an object
    // with a .name that throws, it (intentionally?) mucks with the mini-script
    // consed up in DebuggerController.StackFrames.syncWatchExpressions for
    // eventual evaluation, and subsequent `addExpression` calls will have no
    // effect.
    //
    // This watch-expression appears initially to be added as a hidden
    // expression -- but at first breakpoint it appears to be silently removed,
    // hence the `totalExpressionsAdded - 1` in test1 (with lingering effects on
    // the rest of this test).
    addExpression("throw { get name() { throw \"bazinga\" } }");

    is(totalExpressionsAdded, 33, "handy-dandy count-verifying assertion");
  }

  function performTest() {
    let deferred = promise.defer();

    is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
      "There should be 0 hidden nodes in the watch expressions container");
    is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, totalExpressionsAdded,
      "There should be " + totalExpressionsAdded + " visible nodes in the watch expressions container");

    test1(function () {
      test2(function () {
        test3(function () {
          test4(function () {
            test5(function () {
              test6(function () {
                test7(function () {
                  test8(function () {
                    test9(function () {
                      deferred.resolve();
                    });
                  });
                });
              });
            });
          });
        });
      });
    });

    return deferred.promise;
  }

  function finishTest() {
    is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 0,
      "There should be 0 hidden nodes in the watch expressions container");
    is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, totalExpressionsAdded,
      "There should be " + totalExpressionsAdded + " visible nodes in the watch expressions container");
  }

  function test1(aCallback) {
    gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
      checkWatchExpressions(totalExpressionsAdded - 1, {
        a: "ReferenceError: a is not defined",
        this: { type: "object", class: "Object" },
        prop: { type: "object", class: "String" },
        args: { type: "undefined" }
      });
      aCallback();
    });

    callInTab(gTab, "test");
  }

  function test2(aCallback) {
    gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
      checkWatchExpressions(totalExpressionsAdded - 1, {
        a: { type: "undefined" },
        this: { type: "object", class: "Window" },
        prop: { type: "undefined" },
        args: "sensational"
      });
      aCallback();
    });

    EventUtils.sendMouseEvent({ type: "mousedown" },
      gDebugger.document.getElementById("resume"),
      gDebugger);
  }

  function test3(aCallback) {
    gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
      checkWatchExpressions(totalExpressionsAdded - 1, {
        a: { type: "object", class: "Object" },
        this: { type: "object", class: "Window" },
        prop: { type: "undefined" },
        args: "sensational"
      });
      aCallback();
    });

    EventUtils.sendMouseEvent({ type: "mousedown" },
      gDebugger.document.getElementById("resume"),
      gDebugger);
  }

  function test4(aCallback) {
    gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
      checkWatchExpressions(totalExpressionsAdded, {
        a: 5,
        this: { type: "object", class: "Window" },
        prop: { type: "undefined" },
        args: "sensational"
      });
      aCallback();
    });

    gWatch.addExpression("a = 5");
    EventUtils.sendKey("RETURN", gDebugger);
  }

  function test5(aCallback) {
    gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
      checkWatchExpressions(totalExpressionsAdded, {
        a: 5,
        this: { type: "object", class: "Window" },
        prop: { type: "undefined" },
        args: "sensational"
      });
      aCallback();
    });

    gWatch.addExpression("encodeURI(\"\\\")");
    EventUtils.sendKey("RETURN", gDebugger);
  }

  function test6(aCallback) {
    gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
      checkWatchExpressions(totalExpressionsAdded, {
        a: 5,
        this: { type: "object", class: "Window" },
        prop: { type: "undefined" },
        args: "sensational"
      });
      aCallback();
    });

    gWatch.addExpression("decodeURI(\"\\\")");
    EventUtils.sendKey("RETURN", gDebugger);
  }

  function test7(aCallback) {
    gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
      checkWatchExpressions(totalExpressionsAdded, {
        a: 5,
        this: { type: "object", class: "Window" },
        prop: { type: "undefined" },
        args: "sensational"
      });
      aCallback();
    });

    gWatch.addExpression("?");
    EventUtils.sendKey("RETURN", gDebugger);
  }

  function test8(aCallback) {
    gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => {
      checkWatchExpressions(totalExpressionsAdded, {
        a: 5,
        this: { type: "object", class: "Window" },
        prop: { type: "undefined" },
        args: "sensational"
      });
      aCallback();
    });

    gWatch.addExpression("a");
    EventUtils.sendKey("RETURN", gDebugger);
  }

  function test9(aCallback) {
    gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => {
      aCallback();
    });

    EventUtils.sendMouseEvent({ type: "mousedown" },
      gDebugger.document.getElementById("resume"),
      gDebugger);
  }

  function checkWatchExpressions(aTotal, aExpectedExpressions) {
    let {
      a: expected_a,
      this: expected_this,
      prop: expected_prop,
      args: expected_args
    } = aExpectedExpressions;

    is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, aTotal,
      "There should be " + aTotal + " hidden nodes in the watch expressions container.");
    is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0,
      "There should be 0 visible nodes in the watch expressions container.");

    let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
    let scope = gVariables._currHierarchy.get(label);

    ok(scope, "There should be a watch expressions scope in the variables view.");
    is(scope._store.size, aTotal, "There should be " + aTotal + " evaluations available.");

    let w1 = scope.get("'a'");
    let w2 = scope.get("\"a\"");
    let w3 = scope.get("'a\"\"'");
    let w4 = scope.get("\"a''\"");
    let w5 = scope.get("?");
    let w6 = scope.get("a");
    let w7 = scope.get("this");
    let w8 = scope.get("this.canada");
    let w9 = scope.get("[1, 2, 3]");
    let w10 = scope.get("x = [1, 2, 3]");
    let w11 = scope.get("y = [1, 2, 3]; y.test = 4");
    let w12 = scope.get("z = [1, 2, 3]; z.test = 4; z");
    let w13 = scope.get("t = [1, 2, 3]; t.test = 4; !t");
    let w14 = scope.get("arguments[0]");
    let w15 = scope.get("encodeURI(\"\\\")");
    let w16 = scope.get("decodeURI(\"\\\")");
    let w17 = scope.get("decodeURIComponent(\"%\")");
    let w18 = scope.get("//");
    let w19 = scope.get("// 42");
    let w20 = scope.get("{}.foo");
    let w21 = scope.get("{}.foo()");
    let w22 = scope.get("({}).foo()");
    let w23 = scope.get("new Array(-1)");
    let w24 = scope.get("4.2.toExponential(-4.2)");
    let w25 = scope.get("throw new Error(\"bazinga\")");
    let w26 = scope.get("({ get error() { throw new Error(\"bazinga\") } }).error");
    let w27 = scope.get("'$$$'.replace(/\\$/, 'foo')");
    let w28 = scope.get("function x(a = 3) { 'use strict'; } 'NOT EVALUATED'");
    let w29 = scope.get("function y(...b) { 'use strict'; } 'NOT EVALUATED'");
    let w30 = scope.get("function z([]) { 'use strict'; } 'NOT EVALUATED'");
    let w31 = scope.get("function w({}) { 'use strict'; } 'NOT EVALUATED'");
    let w32 = scope.get("42");

    // This doesn't *have* to be last -- these could be in any order -- but keep
    // it last for consistency with `addExpressions`.
    let w33 = scope.get("throw { get name() { throw \"bazinga\" } }");

    ok(w1, "The first watch expression should be present in the scope.");
    ok(w2, "The second watch expression should be present in the scope.");
    ok(w3, "The third watch expression should be present in the scope.");
    ok(w4, "The fourth watch expression should be present in the scope.");
    ok(w5, "The fifth watch expression should be present in the scope.");
    ok(w6, "The sixth watch expression should be present in the scope.");
    ok(w7, "The seventh watch expression should be present in the scope.");
    ok(w8, "The eight watch expression should be present in the scope.");
    ok(w9, "The ninth watch expression should be present in the scope.");
    ok(w10, "The tenth watch expression should be present in the scope.");
    ok(w11, "The eleventh watch expression should be present in the scope.");
    ok(w12, "The twelfth watch expression should be present in the scope.");
    ok(w13, "The 13th watch expression should be present in the scope.");
    ok(w14, "The 14th watch expression should be present in the scope.");
    ok(w15, "The 15th watch expression should be present in the scope.");
    ok(w16, "The 16th watch expression should be present in the scope.");
    ok(w17, "The 17th watch expression should be present in the scope.");
    ok(w18, "The 18th watch expression should be present in the scope.");
    ok(w19, "The 19th watch expression should be present in the scope.");
    ok(w20, "The 20th watch expression should be present in the scope.");
    ok(w21, "The 21st watch expression should be present in the scope.");
    ok(w22, "The 22nd watch expression should be present in the scope.");
    ok(w23, "The 23nd watch expression should be present in the scope.");
    ok(w24, "The 24th watch expression should be present in the scope.");
    ok(w25, "The 25th watch expression should be present in the scope.");
    ok(w26, "The 26th watch expression should be present in the scope.");
    ok(w27, "The 27th watch expression should be present in the scope.");
    ok(w28, "The 28th watch expression should be present in the scope.");
    ok(w29, "The 29th watch expression should be present in the scope.");
    ok(w30, "The 30th watch expression should be present in the scope.");
    ok(w31, "The 31st watch expression should be present in the scope.");
    ok(w32, "The 32nd watch expression should be present in the scope.");

    // Keep this last for consistency with `addExpressions`.
    ok(!w33, "The 33rd watch expression should not be present in the scope.");

    is(w1.value, "a", "The first value is correct.");
    is(w2.value, "a", "The second value is correct.");
    is(w3.value, "a\"\"", "The third value is correct.");
    is(w4.value, "a''", "The fourth value is correct.");
    is(w5.value, "SyntaxError: expected expression, got '?'", "The fifth value is correct.");

    if (typeof expected_a == "object") {
      is(w6.value.type, expected_a.type, "The sixth value type is correct.");
      is(w6.value.class, expected_a.class, "The sixth value class is correct.");
    } else {
      is(w6.value, expected_a, "The sixth value is correct.");
    }

    if (typeof expected_this == "object") {
      is(w7.value.type, expected_this.type, "The seventh value type is correct.");
      is(w7.value.class, expected_this.class, "The seventh value class is correct.");
    } else {
      is(w7.value, expected_this, "The seventh value is correct.");
    }

    if (typeof expected_prop == "object") {
      is(w8.value.type, expected_prop.type, "The eighth value type is correct.");
      is(w8.value.class, expected_prop.class, "The eighth value class is correct.");
    } else {
      is(w8.value, expected_prop, "The eighth value is correct.");
    }

    is(w9.value.type, "object", "The ninth value type is correct.");
    is(w9.value.class, "Array", "The ninth value class is correct.");
    is(w10.value.type, "object", "The tenth value type is correct.");
    is(w10.value.class, "Array", "The tenth value class is correct.");
    is(w11.value, "4", "The eleventh value is correct.");
    is(w12.value.type, "object", "The eleventh value type is correct.");
    is(w12.value.class, "Array", "The twelfth value class is correct.");
    is(w13.value, false, "The 13th value is correct.");

    if (typeof expected_args == "object") {
      is(w14.value.type, expected_args.type, "The 14th value type is correct.");
      is(w14.value.class, expected_args.class, "The 14th value class is correct.");
    } else {
      is(w14.value, expected_args, "The 14th value is correct.");
    }

    is(w15.value, "SyntaxError: \"\" literal not terminated before end of script", "The 15th value is correct.");
    is(w16.value, "SyntaxError: \"\" literal not terminated before end of script", "The 16th value is correct.");
    is(w17.value, "URIError: malformed URI sequence", "The 17th value is correct.");

    is(w18.value.type, "undefined", "The 18th value type is correct.");
    is(w18.value.class, undefined, "The 18th value class is correct.");

    is(w19.value.type, "undefined", "The 19th value type is correct.");
    is(w19.value.class, undefined, "The 19th value class is correct.");

    is(w20.value, "SyntaxError: expected expression, got '.'", "The 20th value is correct.");
    is(w21.value, "SyntaxError: expected expression, got '.'", "The 21th value is correct.");
    is(w22.value, "TypeError: (intermediate value).foo is not a function", "The 22th value is correct.");
    is(w23.value, "RangeError: invalid array length", "The 23th value is correct.");
    is(w24.value, "RangeError: precision -4 out of range", "The 24th value is correct.");
    is(w25.value, "Error: bazinga", "The 25th value is correct.");
    is(w26.value, "Error: bazinga", "The 26th value is correct.");
    is(w27.value, "foo$$", "The 27th value is correct.");

    is(w28.value, "SyntaxError: \"use strict\" not allowed in function with default parameter",
       "The 29th value is correct.");
    is(w29.value, "SyntaxError: \"use strict\" not allowed in function with rest parameter",
       "The 29th value is correct.");
    is(w30.value, "SyntaxError: \"use strict\" not allowed in function with destructuring parameter",
        "The 30th value is correct.");
    is(w31.value, "SyntaxError: \"use strict\" not allowed in function with destructuring parameter",
       "The 31st value is correct.");

    is(w32.value, 42, "The 32nd value is correct.");
  }
}
