/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */

"use strict";

/*
 * Copyright 2013 Mozilla Foundation and contributors
 * Licensed under the New BSD license. See LICENSE.md or:
 * http://opensource.org/licenses/BSD-2-Clause
 */
var prettyFast = this.prettyFast || require("./pretty-fast");

var testCases = [

  {
    name: "Simple function",
    input: "function foo() { bar(); }",
    output: "function foo() {\n" +
            "  bar();\n" +
            "}\n",
    mappings: [
      // function foo() {
      {
        inputLine: 1,
        outputLine: 1
      },
      // bar();
      {
        inputLine: 1,
        outputLine: 2
      },
      // }
      {
        inputLine: 1,
        outputLine: 3
      },
    ]
  },

  {
    name: "Nested function",
    input: "function foo() { function bar() { debugger; } bar(); }",
    output: "function foo() {\n" +
            "  function bar() {\n" +
            "    debugger;\n" +
            "  }\n" +
            "  bar();\n" +
            "}\n",
    mappings: [
      // function bar() {
      {
        inputLine: 1,
        outputLine: 2
      },
      // debugger;
      {
        inputLine: 1,
        outputLine: 3
      },
      // bar();
      {
        inputLine: 1,
        outputLine: 5
      },
    ]
  },

  {
    name: "Immediately invoked function expression",
    input: "(function(){thingy()}())",
    output: "(function () {\n" +
            "  thingy()\n" +
            "}())\n"
  },

  {
    name: "Single line comment",
    input: "// Comment\n" +
           "function foo() { bar(); }\n",
    output: "// Comment\n" +
            "function foo() {\n" +
            "  bar();\n" +
            "}\n",
    mappings: [
      // // Comment
      {
        inputLine: 1,
        outputLine: 1
      }
    ]
  },

  {
    name: "Multi line comment",
    input: "/* Comment\n" +
           "more comment */\n" +
           "function foo() { bar(); }\n",
    output: "/* Comment\n" +
            "more comment */\n" +
            "function foo() {\n" +
            "  bar();\n" +
            "}\n",
    mappings: [
      // /* Comment
      {
        inputLine: 1,
        outputLine: 1
      },
      // \nmore comment */
      {
        inputLine: 1,
        outputLine: 2
      }
    ]
  },

  {
    name: "Null assignment",
    input: "var i=null;\n",
    output: "var i = null;\n",
    mappings: [
      {
        inputLine: 1,
        outputLine: 1
      }
    ]
  },

  {
    name: "Undefined assignment",
    input: "var i=undefined;\n",
    output: "var i = undefined;\n"
  },

  {
    name: "Void 0 assignment",
    input: "var i=void 0;\n",
    output: "var i = void 0;\n"
  },

  {
    name: "This property access",
    input: "var foo=this.foo;\n",
    output: "var foo = this.foo;\n"
  },

  {
    name: "True assignment",
    input: "var foo=true;\n",
    output: "var foo = true;\n"
  },

  {
    name: "False assignment",
    input: "var foo=false;\n",
    output: "var foo = false;\n"
  },

  {
    name: "For loop",
    input: "for (var i = 0; i < n; i++) { console.log(i); }",
    output: "for (var i = 0; i < n; i++) {\n" +
            "  console.log(i);\n" +
            "}\n",
    mappings: [
      // for (var i = 0; i < n; i++) {
      {
        inputLine: 1,
        outputLine: 1
      },
      // console.log(i);
      {
        inputLine: 1,
        outputLine: 2
      },
    ]
  },

  {
    name: "String with semicolon",
    input: "var foo = ';';\n",
    output: "var foo = ';';\n"
  },

  {
    name: "String with quote",
    input: "var foo = \"'\";\n",
    output: "var foo = '\\'';\n"
  },

  {
    name: "Function calls",
    input: "var result=func(a,b,c,d);",
    output: "var result = func(a, b, c, d);\n"
  },

  {
    name: "Regexp",
    input: "var r=/foobar/g;",
    output: "var r = /foobar/g;\n"
  },

  {
    name: "In operator",
    input: "if(foo in bar){doThing()}",
    output: "if (foo in bar) {\n" +
            "  doThing()\n" +
            "}\n"
  },

  {
    name: "With statement",
    input: "with(obj){crock()}",
    output: "with (obj) {\n" +
            "  crock()\n" +
            "}\n"
  },

  {
    name: "New expression",
    input: "var foo=new Foo();",
    output: "var foo = new Foo();\n"
  },

  {
    name: "Continue/break statements",
    input: "while(1){if(x){continue}if(y){break}if(z){break foo}}",
    output: "while (1) {\n" +
            "  if (x) {\n" +
            "    continue\n" +
            "  }\n" +
            "  if (y) {\n" +
            "    break\n" +
            "  }\n" +
            "  if (z) {\n" +
            "    break foo\n" +
            "  }\n" +
            "}\n"
  },

  {
    name: "Instanceof",
    input: "var a=x instanceof y;",
    output: "var a = x instanceof y;\n"
  },

  {
    name: "Binary operators",
    input: "var a=5*30;var b=5>>3;",
    output: "var a = 5 * 30;\n" +
            "var b = 5 >> 3;\n"
  },

  {
    name: "Delete",
    input: "delete obj.prop;",
    output: "delete obj.prop;\n"
  },

  {
    name: "Try/catch/finally statement",
    input: "try{dangerous()}catch(e){handle(e)}finally{cleanup()}",
    output: "try {\n" +
            "  dangerous()\n" +
            "} catch (e) {\n" +
            "  handle(e)\n" +
            "} finally {\n" +
            "  cleanup()\n" +
            "}\n"
  },

  {
    name: "If/else statement",
    input: "if(c){then()}else{other()}",
    output: "if (c) {\n" +
            "  then()\n" +
            "} else {\n" +
            "  other()\n" +
            "}\n"
  },

  {
    name: "If/else without curlies",
    input: "if(c) a else b",
    output: "if (c) a else b\n"
  },

  {
    name: "Objects",
    input: "var o={a:1,\n" +
           "       b:2};",
    output: "var o = {\n" +
            "  a: 1,\n" +
            "  b: 2\n" +
            "};\n",
    mappings: [
      // a: 1,
      {
        inputLine: 1,
        outputLine: 2
      },
      // b: 2
      {
        inputLine: 2,
        outputLine: 3
      },
    ]
  },

  {
    name: "Do/while loop",
    input: "do{x}while(y)",
    output: "do {\n" +
            "  x\n" +
            "} while (y)\n"
  },

  {
    name: "Arrays",
    input: "var a=[1,2,3];",
    output: "var a = [\n" +
            "  1,\n" +
            "  2,\n" +
            "  3\n" +
            "];\n"
  },

  {
    name: "Code that relies on ASI",
    input: "var foo = 10\n" +
           "var bar = 20\n" +
           "function g() {\n" +
           "  a()\n" +
           "  b()\n" +
           "}",
    output: "var foo = 10\n" +
            "var bar = 20\n" +
            "function g() {\n" +
            "  a()\n" +
            "  b()\n" +
            "}\n"
  },

  {
    name: "Ternary operator",
    input: "bar?baz:bang;",
    output: "bar ? baz : bang;\n"
  },

  {
    name: "Switch statements",
    input: "switch(x){case a:foo();break;default:bar()}",
    output: "switch (x) {\n" +
            "  case a:\n" +
            "    foo();\n" +
            "    break;\n" +
            "  default:\n" +
            "    bar()\n" +
            "}\n"
  },

  {
    name: "Multiple single line comments",
    input: "function f() {\n" +
           "  // a\n" +
           "  // b\n" +
           "  // c\n" +
           "}\n",
    output: "function f() {\n" +
            "  // a\n" +
            "  // b\n" +
            "  // c\n" +
            "}\n",
  },

  {
    name: "Indented multiline comment",
    input: "function foo() {\n" +
           "  /**\n" +
           "   * java doc style comment\n" +
           "   * more comment\n" +
           "   */\n" +
           "  bar();\n" +
           "}\n",
    output: "function foo() {\n" +
            "  /**\n" +
            "   * java doc style comment\n" +
            "   * more comment\n" +
            "   */\n" +
            "  bar();\n" +
            "}\n",
  },

  {
    name: "ASI return",
    input: "function f() {\n" +
           "  return\n" +
           "  {}\n" +
           "}\n",
    output: "function f() {\n" +
            "  return\n" +
            "  {\n" +
            "  }\n" +
            "}\n",
  },

  {
    name: "Non-ASI property access",
    input: "[1,2,3]\n" +
           "[0]",
    output: "[\n" +
            "  1,\n" +
            "  2,\n" +
            "  3\n" +
            "]\n" +
            "[0]\n"
  },

  {
    name: "Non-ASI in",
    input: "'x'\n" +
           "in foo",
    output: "'x' in foo\n"
  },

  {
    name: "Non-ASI function call",
    input: "f\n" +
           "()",
    output: "f()\n"
  },

  {
    name: "Non-ASI new",
    input: "new\n" +
           "F()",
    output: "new F()\n"
  },

  {
    name: "Getter and setter literals",
    input: "var obj={get foo(){return this._foo},set foo(v){this._foo=v}}",
    output: "var obj = {\n" +
            "  get foo() {\n" +
            "    return this._foo\n" +
            "  },\n" +
            "  set foo(v) {\n" +
            "    this._foo = v\n" +
            "  }\n" +
            "}\n"
  },

  {
    name: "Escaping backslashes in strings",
    input: "'\\\\'\n",
    output: "'\\\\'\n"
  },

  {
    name: "Escaping carriage return in strings",
    input: "'\\r'\n",
    output: "'\\r'\n"
  },

  {
    name: "Escaping tab in strings",
    input: "'\\t'\n",
    output: "'\\t'\n"
  },

  {
    name: "Escaping vertical tab in strings",
    input: "'\\v'\n",
    output: "'\\v'\n"
  },

  {
    name: "Escaping form feed in strings",
    input: "'\\f'\n",
    output: "'\\f'\n"
  },

  {
    name: "Escaping null character in strings",
    input: "'\\0'\n",
    output: "'\\0'\n"
  },

  {
    name: "Bug 977082 - space between grouping operator and dot notation",
    input: "JSON.stringify(3).length;\n" +
           "([1,2,3]).length;\n" +
           "(new Date()).toLocaleString();\n",
    output: "JSON.stringify(3).length;\n" +
            "([1,\n" +
            "2,\n" +
            "3]).length;\n" +
            "(new Date()).toLocaleString();\n"
  },

  {
    name: "Bug 975477 don't move end of line comments to next line",
    input: "switch (request.action) {\n" +
           "  case 'show': //$NON-NLS-0$\n" +
           "    if (localStorage.hideicon !== 'true') { //$NON-NLS-0$\n" +
           "      chrome.pageAction.show(sender.tab.id);\n" +
           "    }\n" +
           "    break;\n" +
           "  case 'hide': /*Multiline\n" +
           "    Comment */" +
           "    break;\n" +
           "  default:\n" +
           "    console.warn('unknown request'); //$NON-NLS-0$\n" +
           "    // don't respond if you don't understand the message.\n" +
           "    return;\n" +
           "}\n",
    output: "switch (request.action) {\n" +
            "  case 'show': //$NON-NLS-0$\n" +
            "    if (localStorage.hideicon !== 'true') { //$NON-NLS-0$\n" +
            "      chrome.pageAction.show(sender.tab.id);\n" +
            "    }\n" +
            "    break;\n" +
            "  case 'hide': /*Multiline\n" +
            "    Comment */\n" +
            "    break;\n" +
            "  default:\n" +
            "    console.warn('unknown request'); //$NON-NLS-0$\n" +
            "    // don't respond if you don't understand the message.\n" +
            "    return;\n" +
            "}\n"
  }

];

var sourceMap = this.sourceMap || require("source-map");

function run_test() {
  testCases.forEach(function (test) {
    console.log(test.name);

    var actual = prettyFast(test.input, {
      indent: "  ",
      url: "test.js"
    });

    if (actual.code !== test.output) {
      throw new Error("Expected:\n" + test.output
                      + "\nGot:\n" + actual.code);
    }

    if (test.mappings) {
      var smc = new sourceMap.SourceMapConsumer(actual.map.toJSON());
      test.mappings.forEach(function (m) {
        var query = { line: m.outputLine, column: 0 };
        var original = smc.originalPositionFor(query);
        if (original.line != m.inputLine) {
          throw new Error("Querying:\n" + JSON.stringify(query, null, 2) + "\n"
                          + "Expected line:\n" + m.inputLine + "\n"
                          + "Got:\n" + JSON.stringify(original, null, 2));
        }
      });
    }
  });
  console.log("✓ All tests pass!");
}

// Only run the tests if this is node and we are running this file
// directly. (Firefox's test runner will import this test file, and then call
// run_test itself.)
if (typeof require == "function" && typeof module == "object"
    && require.main === module) {
  run_test();
}
