var assert = require("assert");
var esprima = require("esprima");
var parse = require("../lib/parser").parse;
var Printer = require("../lib/printer").Printer;
var NodePath = require("ast-types").NodePath;
var types = require("../lib/types");
var n = types.namedTypes;
var b = types.builders;
var printer = new Printer;
var eol = require("os").EOL;

function parseExpression(expr) {
  var ast = esprima.parse(expr);
  n.Program.assert(ast);
  ast = ast.body[0];
  return n.ExpressionStatement.check(ast) ? ast.expression : ast;
}

function check(expr) {
  const ast = parse(expr);
  const reprinted = printer.print(ast).code;
  assert.strictEqual(reprinted, expr);

  const expressionAst = parseExpression(expr);
  const generic = printer.printGenerically(expressionAst).code;
  types.astNodesAreEquivalent.assert(
    expressionAst,
    parseExpression(generic)
  );
}

var operators = [
  "==", "!=", "===", "!==",
  "<", "<=", ">", ">=",
  "<<", ">>", ">>>",
  "+", "-", "*", "/", "%",
  "&", // TODO Missing from the Parser API.
  "|", "^", "in",
  "instanceof",
  "&&", "||"
];

describe("parens", function () {
  it("Arithmetic", function () {
    check("1 - 2");
    check("  2 +2 ");

    operators.forEach(function (op1) {
      operators.forEach(function (op2) {
        check("(a " + op1 + " b) " + op2 + " c");
        check("a " + op1 + " (b " + op2 + " c)");
      });
    });
  });

  it("Unary", function () {
    check("(-a).b");
    check("(+a).b");
    check("(!a).b");
    check("(~a).b");
    check("(typeof a).b");
    check("(void a).b");
    check("(delete a.b).c");
  });

  it("Binary", function () {
    check("(a && b)()");
    check("typeof (a && b)");
    check("(a && b)[c]");
    check("(a && b).c");
  });

  it("Sequence", function () {
    check("(a, b)()");
    check("a(b, (c, d), e)");
    check("!(a, b)");
    check("a + (b, c) + d");
    check("var a = (1, 2), b = a + a;");
    check("(a, { b: 2 }).b");
    check("[a, (b, c), d]");
    check("({ a: (1, 2) }).a");
    check("(a, b) ? (a = 1, b = 2) : (c = 3)");
    check("a = (1, 2)");
  });

  it("NewExpression", function () {
    check("new (a.b())");
    check("new (a.b())(c)");
    check("new a.b(c)");
    check("+new Date");
    check("(new Date).getTime()");
    check("new a");
    check("(new a)(b)");
    check("(new (a.b(c))(d))(e)");
    check("(new Date)['getTime']()");
    check('(new Date)["getTime"]()');
  });

  it("Numbers", function () {
    check("(1).foo");
    check("(-1).foo");
    check("+0");
    check("NaN.foo");
    check("(-Infinity).foo");
  });

  it("Assign", function () {
    check("!(a = false)");
    check("a + (b = 2) + c");
    check("(a = fn)()");
    check("(a = b) ? c : d");
    check("(a = b)[c]");
    check("(a = b).c");
  });

  it("Function", function () {
    check("a(function (){}.bind(this))");
    check("(function (){}).apply(this, arguments)");
    check("function f() { (function (){}).call(this) }");
    check("while (true) { (function (){}).call(this) }");
    check("() => ({a:1,b:2})");
    check("(x, y={z:1}) => x + y.z");
    check("a || ((x, y={z:1}) => x + y.z)");
  });

  it("ObjectLiteral", function () {
    check("a({b:c(d)}.b)");
    check("({a:b(c)}).a");
  });

  it("ReprintedParens", function () {
    var code = "a(function g(){}.call(this));";
    var ast1 = parse(code);
    var body = ast1.program.body;

    // Copy the function from a position where it does not need
    // parentheses to a position where it does need parentheses.
    body.push(b.expressionStatement(
      body[0].expression.arguments[0]));

    var generic = printer.printGenerically(ast1).code;
    var ast2 = parse(generic);
    types.astNodesAreEquivalent.assert(ast1, ast2);

    var reprint = printer.print(ast1).code;
    var ast3 = parse(reprint);
    types.astNodesAreEquivalent.assert(ast1, ast3);

    body.shift();
    reprint = printer.print(ast1).code;
    var ast4 = parse(reprint);
    assert.strictEqual(ast4.program.body.length, 1);
    var callExp = ast4.program.body[0].expression;
    n.CallExpression.assert(callExp);
    n.MemberExpression.assert(callExp.callee);
    n.FunctionExpression.assert(callExp.callee.object);
    types.astNodesAreEquivalent.assert(ast1, ast4);

    var objCode = "({ foo: 42 }.foo);";
    var objAst = parse(objCode);
    var memExp = objAst.program.body[0].expression;
    n.MemberExpression.assert(memExp);
    n.ObjectExpression.assert(memExp.object);
    n.Identifier.assert(memExp.property);
    assert.strictEqual(memExp.property.name, "foo");
    var blockStmt = b.blockStatement([b.expressionStatement(memExp)]);
    reprint = printer.print(blockStmt).code;
    types.astNodesAreEquivalent.assert(
      blockStmt,
      parse(reprint).program.body[0]
    );
  });

  it("don't reparenthesize valid IIFEs", function () {
    var iifeCode = "(function     spaces   () {        }.call()  )  ;";
    var iifeAst = parse(iifeCode);
    var iifeReprint = printer.print(iifeAst).code;
    assert.strictEqual(iifeReprint, iifeCode);
  });

  it("don't reparenthesize valid object literals", function () {
    var objCode = "(  {    foo   :  42}.  foo )  ;";
    var objAst = parse(objCode);
    var objReprint = printer.print(objAst).code;
    assert.strictEqual(objReprint, objCode);
  });

  it("don't parenthesize return statements with sequence expressions", function () {
    var objCode = "function foo() { return 1, 2; }";
    var objAst = parse(objCode);
    var objReprint = printer.print(objAst).code;
    assert.strictEqual(objReprint, objCode);
  });

  it("NegatedLoopCondition", function () {
    var ast = parse([
      "for (var i = 0; i < 10; ++i) {",
      "  console.log(i);",
      "}"
    ].join(eol))

    var loop = ast.program.body[0];
    var test = loop.test;
    var negation = b.unaryExpression("!", test);

    assert.strictEqual(
      printer.print(negation).code,
      "!(i < 10)"
    );

    loop.test = negation;

    assert.strictEqual(printer.print(ast).code, [
      "for (var i = 0; !(i < 10); ++i) {",
      "  console.log(i);",
      "}"
    ].join(eol));
  });

  it("MisleadingExistingParens", function () {
    var ast = parse([
      // The key === "oyez" expression appears to have parentheses
      // already, but those parentheses won't help us when we negate the
      // condition with a !.
      'if (key === "oyez") {',
      "  throw new Error(key);",
      "}"
    ].join(eol));

    var ifStmt = ast.program.body[0];
    ifStmt.test = b.unaryExpression("!", ifStmt.test);

    var binaryPath = new NodePath(ast).get(
      "program", "body", 0, "test", "argument");

    assert.ok(binaryPath.needsParens());

    assert.strictEqual(printer.print(ifStmt).code, [
      'if (!(key === "oyez")) {',
      "  throw new Error(key);",
      "}"
    ].join(eol));
  });

  it("DiscretionaryParens", function () {
    var code = [
      "if (info.line && (i > 0 || !skipFirstLine)) {",
      "  info = copyLineInfo(info);",
      "}"
    ].join(eol);

    var ast = parse(code);

    var rightPath = new NodePath(ast).get(
      "program", "body", 0, "test", "right");

    assert.ok(rightPath.needsParens());
    assert.strictEqual(printer.print(ast).code, code);
  });

  it("should not be added to multiline boolean expressions", function () {
    var code = [
      "function foo() {",
      "  return !(",
      "    a &&",
      "    b &&",
      "    c",
      "  );",
      "}"
    ].join(eol);

    var ast = parse(code);
    var printer = new Printer({
      tabWidth: 2
    });

    assert.strictEqual(
      printer.print(ast).code,
      code
    );
  });

  it("should be added to callees that are function expressions", function () {
    check("(()=>{})()");
    check("(function (){})()");
  });

  it("issues #504 and #512", function () {
    check("() => ({})['foo']");
    check("() => ({ foo: 123 }[foo] + 2) * 3");
    check("() => ({ foo: 123 }['foo'] + 1 - 2 - 10)");
    check("() => (function () { return 123 })()");
    check("() => (function () { return 456 }())");
  });

  it("should be added to bound arrow function expressions", function () {
    check("(()=>{}).bind(x)");
  });

  it("should be added to object destructuring assignment expressions", function () {
    check("({x}={x:1})");
    // Issue #533
    check("({ foo } = bar)");
  });

  it("regression test for issue #327", function () {
    const expr = "(function(){}())";
    check(expr);

    const ast = parse(expr);
    const callExpression = ast.program.body[0].expression;
    assert.strictEqual(callExpression.type, "CallExpression");
    callExpression.callee.type = "ArrowFunctionExpression";
    assert.strictEqual(
      printer.print(ast).code,
      "((() => {})())"
    );
    // Print just the callExpression without its enclosing AST context.
    assert.strictEqual(
      printer.print(callExpression).code,
      "(() => {})()"
    );
    // Trigger pretty-printing of the callExpression to remove the outer
    // layer of parentheses.
    callExpression.original = null;
    assert.strictEqual(
      printer.print(ast).code,
      "(() => {})();"
    );
  });

  it("regression test for issue #366", function () {
    const code = "typeof a ? b : c";
    check(code);

    const ast = parse(code);
    const exprStmt = ast.program.body[0];
    const callee = exprStmt.expression;
    exprStmt.expression = b.callExpression(callee, []);

    assert.strictEqual(
      printer.print(ast).code,
      "(typeof a ? b : c)()"
    );
  });
});
