/*
 * Copyright (c) 2011 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */

package com.ibm.wala.cast.js.test;

import java.io.File;
import java.io.IOException;
import java.util.regex.Pattern;

import org.junit.Assert;
import org.junit.ComparisonFailure;
import org.junit.Ignore;
import org.junit.Test;

import com.ibm.wala.cast.js.ipa.callgraph.correlations.extraction.ClosureExtractor;
import com.ibm.wala.cast.js.ipa.callgraph.correlations.extraction.ForInBodyExtractionPolicy;
import com.ibm.wala.cast.tree.CAstEntity;
import com.ibm.wala.cast.tree.impl.CAstImpl;
import com.ibm.wala.classLoader.SourceFileModule;
import com.ibm.wala.classLoader.SourceModule;
import com.ibm.wala.util.io.FileUtil;

public abstract class TestForInBodyExtraction {
	public void testRewriter(String in, String out) {
		testRewriter(null, in, out);
	}
	
	/* The translation to CAst introduces temporary names based on certain characteristics of the translation
	 * process. This sometimes makes it impossible to precisely match up the results of first translating to
	 * CAst and then transforming, and first transforming the JavaScript and then translating to CAst.
	 * 
	 * As a heuristic, we replace some generated names with placeholders, which will be the same in both
	 * versions. This could in principle mask genuine errors.
	 */
	public static String eraseGeneratedNames(String str) {
	  Pattern generatedNamePattern = Pattern.compile("\\$\\$destructure\\$(rcvr|elt)\\d+");
	  str = generatedNamePattern.matcher(str).replaceAll("\\$\\$destructure\\$$1xxx");
	  
	  Pattern generatedFunNamePattern = Pattern.compile("\\.js(@\\d+)+");
	  str = generatedFunNamePattern.matcher(str).replaceAll(".js@xxx");
	  return str;
	}
	
	public void testRewriter(String testName, String in, String out) {
		File tmp = null;
		String expected = null;
		String actual = null;
		try {
			tmp = File.createTempFile("test", ".js");
			FileUtil.writeFile(tmp, in);
			CAstImpl ast = new CAstImpl();
			actual = new CAstDumper().dump(new ClosureExtractor(ast, ForInBodyExtractionPolicy.FACTORY).rewrite(parseJS(tmp, ast)));
			actual = eraseGeneratedNames(actual);
			
			FileUtil.writeFile(tmp, out);
			expected = new CAstDumper().dump(parseJS(tmp, ast));
			expected = eraseGeneratedNames(expected);
			
			Assert.assertEquals(testName, expected, actual);
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ComparisonFailure e) { 
		  System.err.println("Comparison Failure in " + testName + "!");
		  System.err.println(expected);
		  System.err.println(actual);
		  throw e;
		} finally {
			if(tmp != null && tmp.exists())
				tmp.delete();
		}
	}
	
	protected CAstEntity parseJS(File tmp, CAstImpl ast) throws IOException {
		String moduleName = tmp.getName();
		SourceFileModule module = new SourceFileModule(tmp, moduleName, null);
		return parseJS(ast, module);
	}

	protected abstract CAstEntity parseJS(CAstImpl ast, SourceModule module) throws IOException;
	
	// example from the paper
	@Test
	public void test1() {
		testRewriter("function extend(dest, src) {" +
				     "  for(var p in src) {" +
				     "    dest[p] = src[p];" +
				     "  }" +
				     "}",
				     "function extend(dest, src) {" +
				     "  for(var p in src) {" +
				     "    (function _forin_body_0(p) {" +
				     "      dest[p] = src[p];" +
				     "     })(p);" +
				     "  }" +
		         "}");
	}
	
	// example from the paper, but with single-statement loop body
  @Test
	public void test2() {
		testRewriter("function extend(dest, src) {" +
				     "  for(var p in src)" +
				     "    dest[p] = src[p];" +
				     "}",
				     "function extend(dest, src) {" +
				     "  for(var p in src)" +
				     "    (function _forin_body_0(p) {" +
				     "      dest[p] = src[p];" +
				     "     })(p);" +
				     "}");
	}
	
	// example from the paper, but without var decl
  @Test
	public void test3() {
		testRewriter("function extend(dest, src) {" +
				     "  for(p in src)" +
				     "    dest[p] = src[p];" +
				     "}",
				     "function extend(dest, src) {" +
				     "  for(p in src)" +
				     "    (function _forin_body_0(p) {" +
				     "      dest[p] = src[p];" +
				     "     })(p);" +
				     "}");
	}
	
	// example from the paper, but with separate var decl 
  @Test
	public void test4() {
		testRewriter("function extend(dest, src) {" +
					 "  var p;" +
				     "  for(p in src)" +
				     "    dest[p] = src[p];" +
				     "}",
				     "function extend(dest, src) {" +
				     "  var p;" +
				     "  for(p in src)" +
				     "    (function _forin_body_0(p) {" +
				     "      dest[p] = src[p];" +
				     "     })(p);" +
				     "}");
	}
	
	// example from the paper, but with weirdly placed var decl 
  @Test
	public void test5() {
		testRewriter("function extend(dest, src) {" +
				     "  for(p in src) {" +
				     "    var p;" +
				     "    dest[p] = src[p];" +
				     "  }" +
				     "}",
				     "function extend(dest, src) {" +
				     "  for(p in src) {" +
				     "    var p;" +
				     "    (function _forin_body_0(p) {" +
				     "      dest[p] = src[p];" +
				     "     })(p);" +
				     "  }" +
				     "}");
	}
	
	// example from the paper, but with weirdly placed var decl in a different place
  @Test
	public void test6() {
		testRewriter("function extend(dest, src) {" +
				     "  for(p in src) {" +
				     "    dest[p] = src[p];" +
				     "    var p;" +
				     "  }" +
				     "}",
				     "function extend(dest, src) {" +
				     "  for(p in src) {" +
				     "    var p;" +
				     "    (function _forin_body_0(p) {" +
				     "      dest[p] = src[p];" +
				     "     })(p);" +
				     "  }" +
				     "}");
	}
	
	// example where loop variable is referenced after the loop
	// this isn't currently handled, hence the test fails
	@Test @Ignore
	public void test7() {
		testRewriter("function extend(dest, src) {" +
				     "  for(var p in src) {" +
				     "    dest[p] = src[p];" +
				     "    p = true;" +
				     "  }" +
				     "  return p;" +
				     "}",
				     "function extend(dest, src) {" +
				     "  for(var p in src)" +
				     "    (function _let_0(_let_parm_0) {" +
				     "      (function _forin_body_0(p) {" +
				     "        try {" +
				     "          dest[p] = src[p];" +
				     "          p = true;" +
				     "        } finally {" +
				     "          _let_parm_0 = p;" +
				     "        }" +
				     "       })(p);" +
				     "       p = _let_parm_0;" +
				     "     })(p);" +
				     "  return p;" +
				     "}");
	}
	
	// example with "this"
  @Test
	public void test8() {
		testRewriter("Object.prototype.extend = function(src) {" +
				     "  for(var p in src)" +
				     "    this[p] = src[p];" +
				     "}",
				     "Object.prototype.extend = function(src) {" +
				     "  for(var p in src)" +
				     "    (function _forin_body_0(p, thi$) {" +
				     "      thi$[p] = src[p];" +
				     "    })(p, this);" +
				     "}");
	}
	
	// another example with "this"
  @Test
	public void test9() {
		testRewriter("function defglobals(globals) {" +
				     "  for(var p in globals) {" +
				     "    (function inner() {" +
				     "      this[p] = globals[p];" +
				     "    })();" +
				     "  }" +
				     "}",
				     "function defglobals(globals) {" +
				     "  for(var p in globals) {" +
				     "    (function _forin_body_0(p) {" +
				     "      (function inner() {" +
				     "        this[p] = globals[p];" +
				     "      })()" +
				     "    })(p);" +
				     "  }" +
				     "}");
	}
	
	// an example with "break"
  @Test
	public void test10() {
		testRewriter("function extend(dest, src) {" +
				     "  for(var p in src) {" +
				     "    if(p == \"stop\")" +
				     "      break;" +
				     "    dest[p] = src[p];" +
				     "  }" +
				     "}",
				     "function extend(dest, src) {" +
				     "  for(var p in src) {" +
				     "    re$ = (function _forin_body_0(p) {" +
				     "      if(p == \"stop\")" +
				     "        return {type: 'goto', target: 0};" +
				     "      dest[p] = src[p];" +
				     "    })(p);" +
				     "    if(re$) {" +
				     "      if(re$.type == 'goto') {" +
				     "        if(re$.target == 0)" +
				     "          break;" +
				     "      }" +
				     "    }" +
				     "  }" +
				     "}");
	}
	
	// another example with "break"
  @Test
	public void test11() {
		testRewriter("function extend(dest, src) {" +
				     "  for(var p in src) {" +
				     "    while(true) {" +
				     "      dest[p] = src[p];" +
				     "      break;" +
				     "    }" +
				     "  }" +
				     "}",
				     "function extend(dest, src) {" +
				     "  for(var p in src) {" +
				     "    (function _forin_body_0(p) {" +
				     "      while(true) {" +
				     "        dest[p] = src[p];" +
				     "        break;" +
				     "      }" +
				     "    })(p);" +
				     "  }" +
				     "}");
	}
	
	// an example with labelled "break"
  @Test
	public void test12() {
		testRewriter("function extend(dest, src) {" +
				     "  outer: for(var p in src) {" +
				     "    while(true) {" +
				     "      dest[p] = src[p];" +
				     "      break outer;" +
				     "    }" +
				     "  }" +
				     "}",
				     "function extend(dest, src) {" +
				     "  outer: for(var p in src) {" +
				     "    re$ = (function _forin_body_0(p) {" +
				     "      while(true) {" +
				     "        dest[p] = src[p];" +
				     "        return {type: 'goto', target: 0};" +
				     "      }" +
				     "    })(p);" +
				     "    if(re$) {" +
				     "      if(re$.type == 'goto') {" +
				     "        if(re$.target == 0)" +
				     "          break outer;" +
				     "      }" +
				     "    }" +
				     "  }" +
				     "}");
	}
	
	// an example with exceptions
  @Test
	public void test13() {
		testRewriter("function extend(dest, src) {" +
			         "  for(var p in src) {" +
			         "    if(p == '__proto__')" +
			         "      throw new Exception('huh?');" +
			         "    dest[p] = src[p];" +
			         "  }" +
			         "}",
			         "function extend(dest, src) {" +
			         "  for(var p in src) {" +
			         "    (function _forin_body_0(p) {" +
			         "      if(p == '__proto__')" +
			         "        throw new Exception('huh?');" +
			         "      dest[p] = src[p];" +
			         "     })(p);" +
			         "  }" +
			     	"}");

	}
	
	// an example with a var decl
	// this test fails due to a trivial difference between transformed and expected CAst that isn't semantically relevant
  @Test @Ignore
	public void test14() {
		testRewriter("x = 23;" +
				     "function foo() {" +
				     "  x = 42;" +
				     "  for(var p in {toString : 23}) {" +
				     "    var x = 56;" +
				     "    alert(x);" +
				     "  }" +
				     "  alert(x);" +
				     "}" +
				     "foo();" +
				     "alert(x);",
				     "x = 23;" +
				     "function foo() {" +
				     "  x = 42;" +
				     "  for(var p in {toString : 23}) {" +
				     "    var x;" +
				     "    (function _forin_body_0(p) {" +
				     "      x = 56;" +
				     "      alert(x);" +
				     "    })(p);" +
				     "  }" +
				     "  alert(x);" +
				     "}" +
				     "foo();" +
				     "alert(x);");
	}
	
	// another example with a var decl
  @Test
	public void test15() {
		testRewriter("x = 23;" +
				     "function foo() {" +
				     "  x = 42;" +
				     "  for(var p in {toString : 23}) {" +
				     "    (function inner() {" +
				     "      var x = 56;" +
				     "      alert(x);" +
				     "    })();" +
				     "  }" +
				     "  alert(x);" +
				     "}" +
				     "foo();" +
				     "alert(x);",
				     "x = 23;" +
				     "function foo() {" +
				     "  x = 42;" +
				     "  for(var p in {toString : 23}) {" +
				     "    (function _forin_body_0(p) {" +
				     "      (function inner() {" +
				     "        var x = 56;" +
				     "        alert(x);" +
				     "      })();" +
				     "    })(p);" +
				     "  }" +
				     "  alert(x);" +
				     "}" +
				     "foo();" +
				     "alert(x);");
	}
	
	// an example with a "with" block
  @Test
	public void test16() {
		testRewriter("function extend(dest, src) {" +
				     "  var o = { dest: dest };" +
				     "  with(o) {" +
				     "    for(var p in src) {" +
				     "      dest[p] = src[p];" +
				     "    }" +
				     "  }" +
				     "}",
				     "function extend(dest, src) {" +
				     "  var o = { dest: dest };" +
				     "  with(o) {" +
				     "    for(var p in src) {" +
				     "      (function _forin_body_0(p) {" +
				     "        dest[p] = src[p];" +
				     "      })(p);" +
				     "    }" +
				     "  }" +
				     "}");
	}
	
	// top-level for-in loop
  @Test
	public void test17() {
		testRewriter("var o = {x:23};" +
					 "for(x in o) {" +
					 "  o[x] += 19;" +
					 "}",
					 "var o = {x:23};" +
					 "for(x in o) {" +
					 "  (function _forin_body_0(x) {" +
					 "    o[x] += 19;" +
					 "  })(x);" +
					 "}");
	}
	
	
	// nested for-in loops
  @Test
	public void test18() {
		testRewriter("var o = {x:{y:23}};" +
					 "for(x in o) {" +
					 "  for(y in o[x]) {" +
					 "    o[x][y] += 19;" +
					 "  }" +
					 "}",
					 "var o = {x:{y:23}};" +
					 "for(x in o) {" +
					 "  (function _forin_body_0(x) {" +
					 "    for(y in o[x]) {" +
					 "      (function _forin_body_1(y) {" +
					 "        o[x][y] += 19;" +
					 "      })(y);" +
					 "    }" +
					 "  })(x);" +
					 "}");
	}
	
	// return in loop body
  @Test
	public void test19() {
		testRewriter("function foo(x) {" +
				     "  for(var p in x) {" +
				     "    if(p == 'ret')" +
				     "      return x[p];" +
				     "    x[p]++;" +
				     "  }" +
				     "}",
				     "function foo(x) {" +
				     "  for(var p in x) {" +
				     "    re$ = (function _forin_body_0(p) {" +
				     "      if(p == 'ret')" +
				     "        return {type: 'return', value: x[p]};" +
				     "      x[p]++;" +
				     "    })(p);" +
				     "    if(re$) {" +
				     "      if(re$.type == 'return')" +
				     "        return re$.value;" +
				     "    }" +
				     "  }" +
				     "}");
	}
	
	// example with two functions
  @Test
	public void test20() {
		testRewriter("function extend(dest, src) {" +
					 "  for(var p in src)" +
					 "    dest[p] = src[p];" +
					 "}" +
					 "function foo() {" +
					 "  extend({}, {});" +
					 "}" +
					 "foo();",
					 "function extend(dest, src) {" +
					 "  for(var p in src)" +
					 "    (function _forin_body_0(p) {" +
					 "      dest[p] = src[p];" +
					 "    })(p);" +
					 "}" +
					 "function foo() {" +
					 "  extend({}, {});" +
					 "}" +
					 "foo();");	
	}
	
	// example with nested for-in loops and this (adapted from MooTools)
  // currently fails because generated names look different
  @Test
	public void test21() {
		testRewriter("function foo() {" +
				     "  var result = [];" +
				     "  for(var style in Element.ShortStyles) {" +
				     "    for(var s in Element.ShortStyles[style]) {" +
				     "      result.push(this.getStyle(s));" +
				     "    }" +
				     "  }" +
				     "}",
				     "function foo() {" +
				     "  var result = [];" +
				     "  for(var style in Element.ShortStyles) {" +
				     "    var s;" +
				     "    (function _forin_body_0(style, thi$) {" +
				     "      for(s in Element.ShortStyles[style]) {" +
				     "        (function _forin_body_1(s) {" +
				     "          result.push(thi$.getStyle(s));" +
				     "        })(s);" +
				     "      }" +
				     "    })(style, this);" +
				     "  }" +
				     "}");
	}

	// example with nested for-in loops and continue (adapted from MooTools)
  @Test
	public void test22() {
		testRewriter("function foo(property) {" +
				     "  var result = [];" +
				     "  for(var style in Element.ShortStyles) {" +
				     "    if(property != style) continue; " +
				     "    for(var s in Element.ShortStyles[style]) {" +
				     "      ;" +
				     "    }" +
				     "  }" +
				     "}",
				     "function foo(property) {" +
				     "  var result = [];" +
				     "  for(var style in Element.ShortStyles) {" +
				     "    var s;" +
				     "    re$ = (function _forin_body_0(style) {" +
				     "      if(property != style) return {type:'goto', target:0}; " +
				     "      for(s in Element.ShortStyles[style]) {" +
				     "        (function _forin_body_1(s) {" +
				     "          ;" +
				     "        })(s);" +
				     "      }" +
				     "    })(style);" +
				     "    if(re$) {" +
				     "      if(re$.type == 'goto') {" +
				     "        if(re$.target == 0)" +
				     "          continue;" +
				     "      }" +
				     "    }" +
				     "  }" +
				     "}");
	}
	
  @Test
	public void test23() {
		testRewriter("function foo(obj) {" +
				     "  for(var p in obj) {" +
				     "    if(p != 'bar')" +
				     "      continue;" +
				     "    return obj[p];" +
				     "  }" +
				     "}",
				     "function foo(obj) {" +
				     "  for(var p in obj) {" +
				     "    re$ = (function _forin_body_0(p) {" +
				     "      if(p != 'bar')" +
				     "        return {type:'goto', target:0};" +
				     "      return {type:'return', value:obj[p]};" +
				     "    })(p);" +
				     "    if(re$) {" +
				     "      if(re$.type == 'return')" +
				     "        return re$.value;" +
				     "      if(re$.type == 'goto') {" +
				     "        if(re$.target == 0)" +
				     "          continue;" +
				     "      }" +
				     "    }" +
				     "  }" +
				     "}");
	}
	
	// currently fails because generated names look different
  @Test
	public void test24() {
		testRewriter("var addSlickPseudos = function() {" +
				     "  for(var name in pseudos)" +
				     "    if(pseudos.hasOwnProperty(name)) {" +
				     "      ;" +
				     "    }" +
				     "}",
				     "var addSlickPseudos = function() {" +
				     "  for(var name in pseudos)" +
				     "    (function _forin_body_0(name) {" +
				     "      if(pseudos.hasOwnProperty(name)) {" +
				     "        ;" +
				     "      }" +
				     "    })(name);" +
				     "}");
	}
	
  @Test
	public void test25() {
		testRewriter("function ext(dest, src) {" +
				     "  for(var p in src)" +
				     "    do_ext(dest, p, src);" +
				     "}" +
				     "function do_ext(x, p, y) { x[p] = y[p]; }",
				     "function ext(dest, src) {" +
				     "  for(var p in src)" +
				     "    (function _forin_body_0(p) {" +
				     "      do_ext(dest, p, src);" +
				     "    })(p);" +
				     "}" +
				     "function do_ext(x, p, y) { x[p] = y[p]; }");
	}
	
  @Test
	public void test26() {
		testRewriter("function foo(x) {" +
				     "  for(p in x) {" +
				     "    for(q in p[x]) {" +
				     "      if(b)" +
				     "        return 23;" +
				     "    }" +
				     "  }" +
				     "}",
				     "function foo(x) {" +
				     "  for(p in x) {" +
				     "    re$ = (function _forin_body_0(p) {" +
				     "      for(q in p[x]) {" +
				     "        re$ = (function _forin_body_1(q) {" +
				     "          if(b)" +
				     "            return { type: 'return', value: 23 };" +
				     "        })(q);" +
				     "        if(re$) {" +
			         "          return re$;" +
				     "        }" +
				     "      }" +
				     "    })(p);" +
				     "    if(re$) {" +
				     "      if(re$.type == 'return')" +
				     "        return re$.value;" +
				     "    }" +
				     "  }" +
				     "}");
	}

	// variation of test22
  @Test
	public void test27() {
		testRewriter("function foo(property) {" +
				     "  var result = [];" +
				     "  outer: for(var style in Element.ShortStyles) {" +
				     "    for(var s in Element.ShortStyles[style]) {" +
				     "      if(s != style) continue outer;" +
				     "    }" +
				     "  }" +
				     "}",
				     "function foo(property) {" +
				     "  var result = [];" +
				     "  outer: for(var style in Element.ShortStyles) {" +
				     "    var s;" +
				     "    re$ = (function _forin_body_0(style) {" +
				     "      for(s in Element.ShortStyles[style]) {" +
				     "        re$ = (function _forin_body_1(s) {" +
				     "          if(s != style) return {type:'goto', target:0};" +
				     "        })(s);" +
				     "        if(re$) {" +
				     "          return re$;" +
				     "        }" +
				     "      }" +
				     "    })(style);" +
				     "    if(re$) {" +
				     "      if(re$.type == 'goto') {" +
				     "        if(re$.target == 0)" +
				     "          continue outer;" +
				     "      }" +
				     "    }" +
				     "  }" +
				     "}");
	}

	// another variation of test22
  @Test
	public void test28() {
		testRewriter("function foo(property) {" +
				     "  var result = [];" +
				     "  outer: for(var style in Element.ShortStyles) {" +
				     "    for(var s in Element.ShortStyles[style]) {" +
				     "      if(s != style) continue;" +
				     "    }" +
				     "  }" +
				     "}",
				     "function foo(property) {" +
				     "  var result = [];" +
				     "  outer: for(var style in Element.ShortStyles) {" +
				     "    var s;" +
				     "    (function _forin_body_0(style) {" +
				     "      for(s in Element.ShortStyles[style]) {" +
				     "        re$ = (function _forin_body_1(s) {" +
				     "          if(s != style) return {type:'goto', target:0};" +
				     "        })(s);" +
				     "        if(re$) {" +
				     "          if(re$.type == 'goto') {" +
				     "            if(re$.target == 0)" +
				     "              continue;" +
				     "          }" +
				     "        }" +
				     "      }" +
				     "    })(style);" +
				     "  }" +
				     "}");
	}
	
	// test where the same entity (namely the inner function "copy") is rewritten more than once
	// this probably shouldn't happen
  @Test
	public void test29() {
		testRewriter("Element.addMethods = function(methods) {" +
                	 "  function copy() {" +
                	 "    for (var property in methods) {" +
                	 "    }" +
                	 "  }" +
                	 "  for (var tag in methods) {" +
                	 "  }" +
                	 "};",
                     "Element.addMethods = function(methods) {" +
                     "  function copy() {" +
                     "    for (var property in methods) {" +
                     "      (function _forin_body_1(property) {" +
                     "      })(property);" +
                     "    }" +
                     "  }" +
                     "  for (var tag in methods) {" +
                     "    (function _forin_body_0(tag){ })(tag);" +
                     "  }" +
                     "};");
	}
	
  @Test
	public void test30() {
		testRewriter("try {" +
				     "  for(var i in {}) {" +
				     "    f();" +
				     "  }" +
				     "} catch(_) {}",
				     "try {" +
				     "  for(var i in {}) {" +
				     "    (function _forin_body_0(i) {" +
				     "      f();" +
				     "    })(i);" +
				     "  }" +
				     "} catch(_) {}");
	}
  
  // cannot extract for-in body referring to "arguments"
  @Test
  public void test31() {
    testRewriter("function extend(dest, src) {" +
             "  for(var p in src) {" +
             "    arguments[0][p] = src[p];" +
             "  }" +
             "}",
             "function extend(dest, src) {" +
             "  for(var p in src) {" +
             "    arguments[0][p] = src[p];" +
             "  }" +
             "}");
  }
}
