// @flow

/**
 * This is a script to heuristically test that ASTs for certain syntax parse
 * as expected by our astTypes.js type definitions.
 *
 * The procedure for these tests are as follows:
 *
 * 1) Parse the astTypes.js file to extract the list of types.
 * 2) Find examples for each of the types (coded below in TYPE_EXAMPLE_CODE).
 * 3) Parse each example.
 * 4) Codegen a hypothetical file that does a Flow cast of the parsed AST JSON
 *    to TProgram for each example.
 * 5) Run the generated code through Flow and verify there are no type errors.
 *
 * The idea is that the concrete AST generated by Flow for each of the examples
 * should be compatible with our toplevel TProgram type.
 */

require('babel-polyfill');

import {astFromFileContents} from "./index";
import {astFromFilePath} from "./index";
import {checkContents} from "./index";
import {readFileSync} from "fs";
import {resolve} from "path";

const ROOT_DIR = resolve(__dirname, "..");

const TYPE_EXAMPLE_CODE = {
  "TFlowComment": null,
  "TFlowCommentBlock": "/* test */",
  "TFlowCommentLine": "// asdf",
  "TFlowLoc": null,
  "TFlowParseError": "var = 42;",
  "TFlowRange": null,
  "TArrayExpression": "[]",
  "TArrayPattern": "var [] = 42",
  "TArrowFunctionExpression": "(() => {})",
  "TAssignmentExpression": "a = 42",
  "TBinaryExpression": "a == b",
  "TBlockStatement": "{}",
  "TBreakStatement": "for(;;){break;}",
  "TCallExpression": "a()",
  "TCatchClause": "try {} catch(e) {}",
  "TClassBody": null,
  "TClassDeclaration": "class Foo { constructor(){} meth() {}}",
  "TClassExpression": "(class { constructor(){} meth() {}})",
  "TConditionalExpression": "a?b:c",
  "TContinueStatement": "for(;;){continue;}",
  "TDebuggerStatement": "debugger;",
  "TDoWhileStatement": "do {} while(true)",
  "TEmptyStatement": ";",
  "TExpression": null,
  "TExpressionStatement": "a=b",
  "TForInStatement": "for (var a in b) {}",
  "TForOfStatement": "for (var a of b) {}",
  "TForStatement": "for(;;){}",
  "TFunctionDeclaration": "function foo() {}",
  "TFunctionExpression": "(function() {})",
  "TIdentifier": "a;",
  "TIfStatement": "if (a) {} else {}",
  "TImportDeclaration": "import 'foo';",
  "TImportDefaultSpecifier": "import foo from 'bar';",
  "TImportNamespaceSpecifier": "import * as foo from 'bar';",
  "TImportSpecifier": "import {foo} from 'bar';",
  "TLabeledStatement": "a:b;",
  "TLiteral": "'asdf'",
  "TLogicalExpression": "a || b",
  "TMemberExpression": "a.b",
  "TMetaProperty": "new.target",
  "TMethodDefinition": null,
  "TNewExpression": "new a()",
  "TObjectExpression": "({})",
  "TObjectPattern": "var {a} = b;",
  "TPattern": null,
  "TProgram": null,
  "TProperty": null,
  "TRestElement": null,
  "TReturnStatement": "function foo() { return 42; } function bar() { return; }",
  "TSequenceExpression": "(a,b)",
  "TStatement": null,
  "TSuper": "class Foo() { constructor() { super; } }",
  "TSwitchStatement": "switch (a) { case b: default: }",
  "TTaggedTemplateExpression": "a`asdf`",
  "TTemplateElement": "`${a}asdf${b}`",
  "TTemplateLiteral": "`asdf`",
  "TThisExpression": "this;",
  "TThrowStatement": "throw a",
  "TTryStatement": "try {} catch(e) {} finally {}",
  "TUnaryExpression": "-1",
  "TUpdateExpression": "a++",
  "TVariableDeclaration": "var a; let b; const c;",
  "TVariableDeclarator": null,
  "TWhileStatement": "while (true) {}",
  "TWithStatement": "with (o) {}",
  "TYieldExpression": "function* foo() { yield 1; yield* a; }",
};

const TYPES_FILE_PATH = resolve(ROOT_DIR, "src/astTypes.js");
let typesFileContents = readFileSync(TYPES_FILE_PATH, {encoding: 'utf8'});

async function runTest() {
  // (1) Parse the types file
  let typesFileAST = await astFromFilePath(ROOT_DIR, TYPES_FILE_PATH);

  // (2) Extract the list of types
  if (typesFileAST.errors.length > 0) {
    throw new Error('Error parsing src/astTypes.js file!');
  }
  let exportDecls = [];
  for (let i = 0; i < typesFileAST.body.length; i++) {
    let stmt = typesFileAST.body[i];
    if (stmt.type === 'ExportDeclaration' && stmt.exportKind === "type") {
      exportDecls.push(stmt);
    }
  }
  let listOfNodeTypes = exportDecls.map((decl) => decl.declaration.id.name);

  // (3) Find example code for type (if no example code exists, error) and parse
  let exampleASTs = [];
  listOfNodeTypes.forEach((type) => {
    if (!TYPE_EXAMPLE_CODE.hasOwnProperty(type)) {
      return; //throw new Error('No test code for `' + type + '`!');
    }
    exampleASTs.push(
      TYPE_EXAMPLE_CODE[type] != null
      ? astFromFileContents(__dirname, TYPE_EXAMPLE_CODE[type])
      : Promise.resolve(null)
    );
  });
  exampleASTs = await Promise.all(exampleASTs);

  // (4) Codegen a file that types the parsed AST
  let fileToTypecheck =
    "/* @flow */ import type {TProgram} from './src/astTypes';\n";

  for (var i = 0; i < listOfNodeTypes.length; i++) {
    let type = listOfNodeTypes[i];
    if (!TYPE_EXAMPLE_CODE.hasOwnProperty(type)) { continue; }
    let ast = exampleASTs[i];
    if (ast !== null) {
      fileToTypecheck += "(/*" + type + "*/" + JSON.stringify(ast, null, 2) + ": TProgram);\n";
    }
  }

  // (5) Run flow over the generated code
  let result = await checkContents(ROOT_DIR, fileToTypecheck);

  if (result.passed) {
    console.log("Passed!");
  } else {
    let lines = fileToTypecheck.split("\n");
    result.errors.forEach((err) => {
      let lastType = null;
      let lastTypeLine;
      let failedTestEndLine;
      for (var i = 0; i < lines.length; i++) { //err.message[0].line; i++) {
        let match = lines[i].match(/^\(\/\*(T[^*]*)\*\//);
        if (match != null) {
          if (i >= err.message[0].line) {
            failedTestEndLine = i;
            break;
          }
          lastType = match[1];
          lastTypeLine = i;
        }
      }
      console.log('== Failing test for `%s` ==', lastType);
      console.log('  Error: %s', err.message.map(msg => msg.descr).join('; '));
      console.log('  Lines: %d - %d', err.message[0].line, err.message[0].endline);
      console.log('');
      console.log(lines
        .slice(lastTypeLine, failedTestEndLine)
        .map((line, idx) => {
          let origIdx = idx + lastTypeLine;
          let prefix =
            (origIdx >= err.message[0].line && origIdx <= err.message[0].endline)
            ? '=> '
            : '   '
          ;
          return prefix + line;
        })
        .join('\n')
      );
    });
  }
}

runTest().catch((err) => setTimeout(() => {throw err;}, 0));
