import assert from "assert";
import * as esprima from "esprima";
import { parse } from "../lib/parser";
import { Printer } from "../lib/printer";
import * as types from "ast-types";
import { EOL as eol } from "os";

const printer = new Printer();
const { namedTypes: n, builders: b, NodePath } = types;

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

function check(expr: any) {
  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));
}

const 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("ArrowFunctionExpression", () => {
    check("(() => {})()");
    check("test(() => {})");

    check("(() => {}).test");
    check("test[() => {}]");

    check("(() => {}) + (() => {})");
  });

  it("AwaitExpression", function () {
    check("async () => (await a) && (await b)");
    check("async () => +(await a)");
    check("async () => (await f)()");
    check("async () => new (await C)");
    check("async () => [...(await obj)]");
    check("async () => (await a) ? b : c");
    check("async () => (await a).b");
  });

  it("YieldExpression", function () {
    check("function* test () { return (yield a) && (yield b) }");
    check("function* test () { return +(yield a) }");
    check("function* test () { return (yield f)() }");
    check("function* test () { return new (yield C) }");
    check("function* test () { return [...(yield obj)] }");
    check("function* test () { return (yield a) ? b : c }");
    check("function* test () { return (yield a).b }");
    check("function* test () { yield yield foo }");
  });

  it("ArrowFunctionExpression", () => {
    check("(() => {})()");
    check("test(() => {})");

    check("(() => {}).test");
    check("test[() => {}]");

    check("(() => {}) + (() => {})");
  });

  it("ReprintedParens", function () {
    const code = "a(function g(){}.call(this));";
    const ast1 = parse(code);
    const 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]));

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

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

    body.shift();
    reprint = printer.print(ast1).code;
    const ast4 = parse(reprint);
    assert.strictEqual(ast4.program.body.length, 1);
    const 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);

    const objCode = "({ foo: 42 }.foo);";
    const objAst = parse(objCode);
    const 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");
    const 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 () {
    const iifeCode = "(function     spaces   () {        }.call()  )  ;";
    const iifeAst = parse(iifeCode);
    const iifeReprint = printer.print(iifeAst).code;
    assert.strictEqual(iifeReprint, iifeCode);
  });

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

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

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

    const loop = ast.program.body[0];
    const test = loop.test;
    const 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 () {
    const 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),
    );

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

    const 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 () {
    const code = [
      "if (info.line && (i > 0 || !skipFirstLine)) {",
      "  info = copyLineInfo(info);",
      "}",
    ].join(eol);

    const ast = parse(code);

    const 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 () {
    const code = [
      "function foo() {",
      "  return !(",
      "    a &&",
      "    b &&",
      "    c",
      "  );",
      "}",
    ].join(eol);

    const ast = parse(code);
    const 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("should be added to function expressions at start of sequence", function () {
    check("(function (){})(), 0");
  });

  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)()");
  });
});
