import assert from "assert";
import { parse } from "../lib/parser";
import { Printer } from "../lib/printer";
import * as types from "ast-types";
const n = types.namedTypes;
const b = types.builders;
import { EOL as eol } from "os";

describe("ES6 Compatability", function () {
  function convertShorthandMethod() {
    const printer = new Printer({ tabWidth: 2 });

    const code = [
      "var name='test-name';",
      "var shorthandObj = {",
      "  name,",
      "  func() { return 'value'; }",
      "};",
    ].join(eol);

    const ast = parse(code);
    n.VariableDeclaration.assert(ast.program.body[1]);

    const shorthandObjDec = ast.program.body[1].declarations[0].init;
    const methodDecProperty = shorthandObjDec.properties[1];
    const newES5MethodProperty = b.property(
      methodDecProperty.kind,
      methodDecProperty.key,
      methodDecProperty.value,
    );

    const correctMethodProperty = b.property(
      methodDecProperty.kind,
      methodDecProperty.key,
      b.functionExpression(
        methodDecProperty.value.id,
        methodDecProperty.value.params,
        methodDecProperty.value.body,
        methodDecProperty.value.generator,
        methodDecProperty.value.expression,
      ),
    );

    assert.strictEqual(
      printer.print(newES5MethodProperty).code,
      printer.print(correctMethodProperty).code,
    );
  }

  it(
    "correctly converts from a shorthand method to ES5 function",
    convertShorthandMethod,
  );

  function respectDestructuringAssignment() {
    const printer = new Printer({ tabWidth: 2 });
    const code = "var {a} = {};";
    const ast = parse(code);
    n.VariableDeclaration.assert(ast.program.body[0]);
    assert.strictEqual(printer.print(ast).code, code);
  }

  it("respects destructuring assignments", respectDestructuringAssignment);
});

describe("import/export syntax", function () {
  const printer = new Printer({ tabWidth: 2 });

  function check(source: string) {
    const ast1 = parse(source);
    const ast2 = parse(printer.printGenerically(ast1).code);
    types.astNodesAreEquivalent.assert(ast1, ast2);
  }

  it("should parse and print import statements correctly", function () {
    check("import foo from 'foo'");

    // default imports
    check("import foo from 'foo';");
    check("import {default as foo} from 'foo';");

    // named imports
    check("import {bar} from 'foo';");
    check("import {bar, baz} from 'foo';");
    check("import {bar as baz} from 'foo';");
    check("import {bar as baz, xyz} from 'foo';");

    // glob imports
    check("import * as foo from 'foo';");

    // mixing imports
    check("import foo, {baz as xyz} from 'foo';");
    check("import foo, * as bar from 'foo';");

    // just import
    check("import 'foo';");
  });

  it("should parse and print export statements correctly", function () {
    // default exports
    check("export default 42;");
    check("export default {};");
    check("export default [];");
    check("export default foo;");
    check("export default function () {}");
    check("export default class {}");
    check("export default function foo () {}");
    check("export default class foo {}");
    check("export default `hello world`");

    // variables exports
    check("export var foo = 1;");
    check("export var foo = function () {};");
    check("export var bar;"); // lazy initialization
    check("export let foo = 2;");
    check("export let bar;"); // lazy initialization
    check("export const foo = 3;");
    check("export function foo () {}");
    check("export class foo {}");

    // named exports
    check("export {foo};");
    check("export {foo, bar};");
    check("export {foo as bar};");
    check("export {foo as default};");
    check("export {foo as default, bar};");

    // exports from
    check("export * from 'foo';");
    check("export {foo} from 'foo';");
    check("export {foo, bar} from 'foo';");
    check("export {foo as bar} from 'foo';");
    check("export {foo as default} from 'foo';");
    check("export {foo as default, bar} from 'foo';");
    check("export {default} from 'foo';");
    check("export {default as foo} from 'foo';");
  });

  it("should forbid invalid import/export syntax", function () {
    function checkInvalid(source: string, expectedMessage: string) {
      try {
        parse(source);
        throw new Error(
          "Parsing should have failed: " + JSON.stringify(source),
        );
      } catch (err: any) {
        assert.strictEqual(err.message, "Line 1: " + expectedMessage);
      }
    }

    // const variables must have an initializer
    checkInvalid(
      "export const bar;",
      "Missing initializer in const declaration",
    );

    // Unexpected token identifier, invalid named export syntax
    checkInvalid("export foo;", "Unexpected identifier");

    // Unexpected token (, use a function declaration instead
    checkInvalid("export function () {}", "Unexpected token (");

    // Unexpected token default
    checkInvalid("export function default () {}", "Unexpected token default");

    // Missing from after import
    checkInvalid("import foo;", "Unexpected token ;");

    // Missing from after import
    checkInvalid("import { foo, bar };", "Unexpected token ;");

    // Invalid module specifier
    checkInvalid("import foo from bar;", "Unexpected token");

    // Unexpected token default
    checkInvalid("import default from 'foo';", "Unexpected token default");

    // Unexpected token from
    checkInvalid("export default from 'foo';", "Unexpected token from");

    // Missing from after export
    checkInvalid("export {default};", "Unexpected token ;");

    // Missing from after export
    checkInvalid("export *;", "Unexpected token ;");

    // Missing from after import
    checkInvalid("import {default as foo};", "Unexpected token ;");

    // Missing as after import *
    checkInvalid("import * from 'foo';", "Unexpected token");

    // Unexpected token =
    checkInvalid("export default = 42;", "Unexpected token =");

    // Unexpected token default
    checkInvalid(
      "import {bar as default} from 'foo';",
      "Unexpected token default",
    );

    // Unexpected token ,
    checkInvalid(
      "import foo, * as bar, {baz as xyz} from 'foo';",
      "Unexpected token ,",
    );

    // Unexpected token ,
    checkInvalid("import {bar}, foo from 'foo';", "Unexpected token ,");

    // Unexpected token ,
    checkInvalid("import {bar}, * as foo from 'foo';", "Unexpected token ,");

    // Unexpected token ,
    checkInvalid("import foo, {bar}, foo from 'foo';", "Unexpected token ,");

    // Unexpected token ,
    checkInvalid("import {bar}, {foo} from 'foo';", "Unexpected token ,");

    // Unexpected token ,
    checkInvalid(
      "import * as bar, {baz as xyz} from 'foo';",
      "Unexpected token ,",
    );
  });

  it("should pretty-print template strings with backticks", function () {
    const code = [
      'var noun = "fool";',
      "var s = `I am a ${noun}`;",
      "var t = tag`You said: ${s}!`;",
    ].join(eol);

    const ast = parse(code);

    assert.strictEqual(
      new Printer({
        tabWidth: 2,
      }).printGenerically(ast).code,
      code,
    );
  });
});
