/*
 * Copyright 2008 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import com.google.common.collect.Maps;

import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.rhino.Node;

import java.util.Map;

/**
 * Unit test for AmbiguateProperties Compiler pass.
 *
 */
public class AmbiguatePropertiesTest extends CompilerTestCase {
  private AmbiguateProperties lastPass;

  private static final String EXTERNS =
      "Function.prototype.call=function(){};" +
      "Function.prototype.inherits=function(){};" +
      "prop.toString;" +
      "var google = { gears: { factory: {}, workerPool: {} } };";

  public AmbiguatePropertiesTest() {
    super(EXTERNS);
    enableNormalize();
    enableTypeCheck(CheckLevel.WARNING);
    enableClosurePass();
  }

  @Override
  public CompilerPass getProcessor(final Compiler compiler) {
    return new CompilerPass() {
      @Override
      public void process(Node externs, Node root) {
        lastPass = new AmbiguateProperties(compiler, new char[]{'$'});
        lastPass.process(externs, root);
      }
    };
  }

  @Override
  protected int getNumRepetitions() {
    return 1;
  }

  @Override
  protected CompilerOptions getOptions() {
    // no missing properties check
    CompilerOptions options = new CompilerOptions();
    options.setLanguageIn(LanguageMode.ECMASCRIPT5);
    return options;
  }

  public void testOneVar1() {
    test("/** @constructor */ var Foo = function(){};Foo.prototype.b = 0;",
         "var Foo = function(){};Foo.prototype.a = 0;");
  }

  public void testOneVar2() {
    testSame("/** @constructor */ var Foo = function(){};" +
             "Foo.prototype = {b: 0};");
  }

  public void testOneVar3() {
    testSame("/** @constructor */ var Foo = function(){};" +
             "Foo.prototype = {get b() {return 0}};");
  }

  public void testOneVar4() {
    testSame("/** @constructor */ var Foo = function(){};" +
             "Foo.prototype = {set b(a) {}};");
  }

  public void testTwoVar1() {
    String js = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "Foo.prototype.z=0;\n"
        + "Foo.prototype.z=0;\n"
        + "Foo.prototype.x=0;";
    String output = ""
        + "var Foo = function(){};\n"
        + "Foo.prototype.a=0;\n"
        + "Foo.prototype.a=0;\n"
        + "Foo.prototype.b=0;";
    test(js, output);
  }

  public void testTwoVar2() {
    String js = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "Foo.prototype={z:0, z:1, x:0};\n";
    // TODO(johnlenz): It would be nice to handle this type of declaration.
    testSame(js);
  }

  public void testTwoIndependentVar() {
    String js = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "Foo.prototype.b = 0;\n"
        + "/** @constructor */ var Bar = function(){};\n"
        + "Bar.prototype.c = 0;";
    String output = ""
        + "var Foo = function(){};"
        + "Foo.prototype.a=0;"
        + "var Bar = function(){};"
        + "Bar.prototype.a=0;";
    test(js, output);
  }

  public void testTwoTypesTwoVar() {
    String js = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "Foo.prototype.r = 0;\n"
        + "Foo.prototype.g = 0;\n"
        + "/** @constructor */ var Bar = function(){};\n"
        + "Bar.prototype.c = 0;"
        + "Bar.prototype.r = 0;";
    String output = ""
        + "var Foo = function(){};"
        + "Foo.prototype.a=0;"
        + "Foo.prototype.b=0;"
        + "var Bar = function(){};"
        + "Bar.prototype.b=0;"
        + "Bar.prototype.a=0;";
    test(js, output);
  }

  public void testUnion() {
    String js = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "/** @constructor */ var Bar = function(){};\n"
        + "Foo.prototype.foodoo=0;\n"
        + "Bar.prototype.bardoo=0;\n"
        + "/** @type {Foo|Bar} */\n"
        + "var U;\n"
        + "U.joint;"
        + "U.joint";
    String output = ""
        + "var Foo = function(){};\n"
        + "var Bar = function(){};\n"
        + "Foo.prototype.b=0;\n"
        + "Bar.prototype.b=0;\n"
        + "var U;\n"
        + "U.a;"
        + "U.a";
    test(js, output);
  }

  public void testUnions() {
    String js = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "/** @constructor */ var Bar = function(){};\n"
        + "/** @constructor */ var Baz = function(){};\n"
        + "/** @constructor */ var Bat = function(){};\n"
        + "Foo.prototype.lone1=0;\n"
        + "Bar.prototype.lone2=0;\n"
        + "Baz.prototype.lone3=0;\n"
        + "Bat.prototype.lone4=0;\n"
        + "/** @type {Foo|Bar} */\n"
        + "var U1;\n"
        + "U1.j1;"
        + "U1.j2;"
        + "/** @type {Baz|Bar} */\n"
        + "var U2;\n"
        + "U2.j3;"
        + "U2.j4;"
        + "/** @type {Baz|Bat} */\n"
        + "var U3;"
        + "U3.j5;"
        + "U3.j6";
    String output = ""
        + "var Foo = function(){};\n"
        + "var Bar = function(){};\n"
        + "var Baz = function(){};\n"
        + "var Bat = function(){};\n"
        + "Foo.prototype.c=0;\n"
        + "Bar.prototype.e=0;\n"
        + "Baz.prototype.e=0;\n"
        + "Bat.prototype.c=0;\n"
        + "var U1;\n"
        + "U1.a;"
        + "U1.b;"
        + "var U2;\n"
        + "U2.c;"
        + "U2.d;"
        + "var U3;"
        + "U3.a;"
        + "U3.b";
    test(js, output);
  }

  public void testExtends() {
    String js = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "Foo.prototype.x=0;\n"
        + "/** @constructor \n @extends Foo */ var Bar = function(){};\n"
        + "goog.inherits(Bar, Foo);\n"
        + "Bar.prototype.y=0;\n"
        + "Bar.prototype.z=0;\n"
        + "/** @constructor */ var Baz = function(){};\n"
        + "Baz.prototype.l=0;\n"
        + "Baz.prototype.m=0;\n"
        + "Baz.prototype.n=0;\n"
        + "(new Baz).m\n";
    String output = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "Foo.prototype.a=0;\n"
        + "/** @constructor \n @extends Foo */ var Bar = function(){};\n"
        + "goog.inherits(Bar, Foo);\n"
        + "Bar.prototype.b=0;\n"
        + "Bar.prototype.c=0;\n"
        + "/** @constructor */ var Baz = function(){};\n"
        + "Baz.prototype.b=0;\n"
        + "Baz.prototype.a=0;\n"
        + "Baz.prototype.c=0;\n"
        + "(new Baz).a\n";
    test(js, output);
  }

  public void testLotsOfVars() {
    StringBuilder js = new StringBuilder();
    StringBuilder output = new StringBuilder();
    js.append("/** @constructor */ var Foo = function(){};\n");
    js.append("/** @constructor */ var Bar = function(){};\n");
    output.append(js.toString());

    int vars = 10;
    for (int i = 0; i < vars; i++) {
      js.append("Foo.prototype.var" + i + " = 0;");
      js.append("Bar.prototype.var" + (i + 10000) + " = 0;");
      output.append("Foo.prototype." + (char) ('a' + i) + "=0;");
      output.append("Bar.prototype." + (char) ('a' + i) + "=0;");
    }
    test(js.toString(), output.toString());
  }

  public void testLotsOfClasses() {
    StringBuilder b = new StringBuilder();
    int classes = 10;
    for (int i = 0; i < classes; i++) {
      String c = "Foo" + i;
      b.append("/** @constructor */ var " + c + " = function(){};\n");
      b.append(c + ".prototype.varness" + i + " = 0;");
    }
    String js = b.toString();
    test(js, js.replaceAll("varness\\d+", "a"));
  }

  public void testFunctionType() {
    String js = ""
        + "/** @constructor */ function Foo(){};\n"
        + "/** @return {Bar} */\n"
        + "Foo.prototype.fun = function() { return new Bar(); };\n"
        + "/** @constructor */ function Bar(){};\n"
        + "Bar.prototype.bazz;\n"
        + "(new Foo).fun().bazz();";
    String output = ""
        + "function Foo(){};\n"
        + "Foo.prototype.a = function() { return new Bar(); };\n"
        + "function Bar(){};\n"
        + "Bar.prototype.a;\n"
        + "(new Foo).a().a();";
    test(js, output);
  }

  public void testPrototypePropertiesAsObjLitKeys1() {
    test("/** @constructor */ function Bar() {};" +
             "Bar.prototype = {2: function(){}, getA: function(){}};",
             "/** @constructor */ function Bar() {};" +
             "Bar.prototype = {2: function(){}, a: function(){}};");
  }

  public void testPrototypePropertiesAsObjLitKeys2() {
    testSame("/** @constructor */ function Bar() {};" +
             "Bar.prototype = {2: function(){}, 'getA': function(){}};");
  }

  public void testQuotedPrototypeProperty() {
    testSame("/** @constructor */ function Bar() {};" +
             "Bar.prototype['getA'] = function(){};" +
             "var bar = new Bar();" +
             "bar['getA']();");
  }

  public void testOverlappingOriginalAndGeneratedNames() {
    test("/** @constructor */ function Bar(){};"
         + "Bar.prototype.b = function(){};"
         + "Bar.prototype.a = function(){};"
         + "var bar = new Bar();"
         + "bar.b();",
         "function Bar(){};"
         + "Bar.prototype.a = function(){};"
         + "Bar.prototype.b = function(){};"
         + "var bar = new Bar();"
         + "bar.a();");
  }

  public void testPropertyAddedToObject() {
    testSame("var foo = {}; foo.prop = '';");
  }

  public void testPropertyAddedToFunction() {
    test("var foo = function(){}; foo.prop = '';",
         "var foo = function(){}; foo.a = '';");
  }

  public void testPropertyOfObjectOfUnknownType() {
    testSame("var foo = x(); foo.prop = '';");
  }

  public void testPropertyOnParamOfUnknownType() {
    testSame("/** @constructor */ function Foo(){};\n"
             + "Foo.prototype.prop = 0;"
             + "function go(aFoo){\n"
             + "  aFoo.prop = 1;"
             + "}");
  }

  public void testSetPropertyOfGlobalThis() {
    testSame("this.prop = 'bar'");
  }

  public void testReadPropertyOfGlobalThis() {
    testSame("f(this.prop);");
  }

  public void testSetQuotedPropertyOfThis() {
    testSame("this['prop'] = 'bar';");
  }

  public void testExternedPropertyName() {
    test("/** @constructor */ var Bar = function(){};"
         + "/** @override */ Bar.prototype.toString = function(){};"
         + "Bar.prototype.func = function(){};"
         + "var bar = new Bar();"
         + "bar.toString();",
         "var Bar = function(){};"
         + "Bar.prototype.toString = function(){};"
         + "Bar.prototype.a = function(){};"
         + "var bar = new Bar();"
         + "bar.toString();");
  }

  public void testExternedPropertyNameDefinedByObjectLiteral() {
    testSame("/**@constructor*/function Bar(){};Bar.prototype.factory");
  }

  public void testStaticAndInstanceMethodWithSameName() {
    test("/** @constructor */function Bar(){}; Bar.getA = function(){}; " +
         "Bar.prototype.getA = function(){}; Bar.getA();" +
         "var bar = new Bar(); bar.getA();",
         "function Bar(){}; Bar.a = function(){};" +
         "Bar.prototype.a = function(){}; Bar.a();" +
         "var bar = new Bar(); bar.a();");
  }

  public void testStaticAndInstanceProperties() {
    test("/** @constructor */function Bar(){};" +
         "Bar.getA = function(){}; " +
         "Bar.prototype.getB = function(){};",
         "function Bar(){}; Bar.a = function(){};" +
         "Bar.prototype.a = function(){};");
  }

  public void testStaticAndSubInstanceProperties() {
    String js = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "Foo.x=0;\n"
        + "/** @constructor \n @extends Foo */ var Bar = function(){};\n"
        + "goog.inherits(Bar, Foo);\n"
        + "Bar.y=0;\n"
        + "Bar.prototype.z=0;\n";
    String output = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "Foo.a=0;\n"
        + "/** @constructor \n @extends Foo */ var Bar = function(){};\n"
        + "goog.inherits(Bar, Foo);\n"
        + "Bar.a=0;\n"
        + "Bar.prototype.a=0;\n";
    test(js, output);
  }

  public void testStaticWithFunctions() {
    String js = ""
      + "/** @constructor */ var Foo = function() {};\n"
      + "Foo.x = 0;"
      + "/** @param {!Function} x */ function f(x) { x.y = 1 }"
      + "f(Foo)";
    String output = ""
      + "/** @constructor */ var Foo = function() {};\n"
      + "Foo.a = 0;"
      + "/** @param {!Function} x */ function f(x) { x.y = 1 }"
      + "f(Foo)";
    test(js, output);

    js = ""
      + "/** @constructor */ var Foo = function() {};\n"
      + "Foo.x = 0;"
      + "/** @param {!Function} x */ function f(x) { x.y = 1; x.x = 2;}"
      + "f(Foo)";
    test(js, js);

    js = ""
      + "/** @constructor */ var Foo = function() {};\n"
      + "Foo.x = 0;"
      + "/** @constructor */ var Bar = function() {};\n"
      + "Bar.y = 0;";

    output = ""
      + "/** @constructor */ var Foo = function() {};\n"
      + "Foo.a = 0;"
      + "/** @constructor */ var Bar = function() {};\n"
      + "Bar.a = 0;";
    test(js, output);

  }

  public void testTypeMismatch() {
    testSame(EXTERNS, "/** @constructor */var Foo = function(){};\n"
             + "/** @constructor */var Bar = function(){};\n"
             + "Foo.prototype.b = 0;\n"
             + "/** @type {Foo} */\n"
             + "var F = new Bar();",
             TypeValidator.TYPE_MISMATCH_WARNING,
             "initializing variable\n"
             + "found   : Bar\n"
             + "required: (Foo|null)");
  }

  public void testRenamingMap() {
    String js = ""
        + "/** @constructor */ var Foo = function(){};\n"
        + "Foo.prototype.z=0;\n"
        + "Foo.prototype.z=0;\n"
        + "Foo.prototype.x=0;\n"
        + "Foo.prototype.y=0;";
    String output = ""
        + "var Foo = function(){};\n"
        + "Foo.prototype.a=0;\n"
        + "Foo.prototype.a=0;\n"
        + "Foo.prototype.b=0;\n"
        + "Foo.prototype.c=0;";
    test(js, output);

    Map<String, String> answerMap = Maps.newHashMap();
    answerMap.put("x", "b");
    answerMap.put("y", "c");
    answerMap.put("z", "a");
    assertEquals(answerMap, lastPass.getRenamingMap());
  }

  public void testInline() {
    String js = ""
        + "/** @interface */ function Foo(){}\n"
        + "Foo.prototype.x = function(){};\n"
        + "/**\n"
        + " * @constructor\n"
        + " * @implements {Foo}\n"
        + " */\n"
        + "function Bar(){}\n"
        + "/** @inheritDoc */\n"
        + "Bar.prototype.x = function() { return this.y; };\n"
        + "Bar.prototype.z = function() {};\n"
        // Simulates inline getters.
        + "/** @type {Foo} */ (new Bar).y;";
    String output = ""
        + "function Foo(){}\n"
        + "Foo.prototype.a = function(){};\n"
        + "function Bar(){}\n"
        + "Bar.prototype.a = function() { return this.b; };\n"
        + "Bar.prototype.c = function() {};\n"
        // Simulates inline getters.
        + "(new Bar).b;";
    test(js, output);
  }

  public void testImplementsAndExtends() {
    String js = ""
        + "/** @interface */ function Foo() {}\n"
        + "/**\n"
        + " * @constructor\n"
        + " */\n"
        + "function Bar(){}\n"
        + "Bar.prototype.y = function() { return 3; };\n"
        + "/**\n"
        + " * @constructor\n"
        + " * @extends {Bar}\n"
        + " * @implements {Foo}\n"
        + " */\n"
        + "function SubBar(){ }\n"
        + "/** @param {Foo} x */ function f(x) { x.z = 3; }\n"
        + "/** @param {SubBar} x */ function g(x) { x.z = 3; }";
    String output = ""
        + "function Foo(){}\n"
        + "function Bar(){}\n"
        + "Bar.prototype.b = function() { return 3; };\n"
        + "function SubBar(){}\n"
        + "function f(x) { x.a = 3; }\n"
        + "function g(x) { x.a = 3; }";
    test(js, output);
  }

  public void testImplementsAndExtends2() {
    String js = ""
        + "/** @interface */ function A() {}\n"
        + "/**\n"
        + " * @constructor\n"
        + " */\n"
        + "function C1(){}\n"
        + "/**\n"
        + " * @constructor\n"
        + " * @extends {C1}\n"
        + " * @implements {A}\n"
        + " */\n"
        + "function C2(){}\n"
        + "/** @param {C1} x */ function f(x) { x.y = 3; }\n"
        + "/** @param {A} x */ function g(x) { x.z = 3; }\n";
    String output = ""
        + "function A(){}\n"
        + "function C1(){}\n"
        + "function C2(){}\n"
        + "function f(x) { x.a = 3; }\n"
        + "function g(x) { x.b = 3; }\n";
    test(js, output);
  }

  public void testExtendsInterface() {
    String js = ""
        + "/** @interface */ function A() {}\n"
        + "/** @interface \n @extends {A} */ function B() {}\n"
        + "/** @param {A} x */ function f(x) { x.y = 3; }\n"
        + "/** @param {B} x */ function g(x) { x.z = 3; }\n";
    String output = ""
        + "function A(){}\n"
        + "function B(){}\n"
        + "function f(x) { x.a = 3; }\n"
        + "function g(x) { x.b = 3; }\n";
    test(js, output);
  }

  public void testFunctionSubType() {
    String js = ""
        + "Function.prototype.a = 1;\n"
        + "function f() {}\n"
        + "f.y = 2;\n";
    String output = ""
        + "Function.prototype.a = 1;\n"
        + "function f() {}\n"
        + "f.b = 2;\n";
    test(js, output);
  }

  public void testFunctionSubType2() {
    String js = ""
        + "Function.prototype.a = 1;\n"
        + "/** @constructor */ function F() {}\n"
        + "F.y = 2;\n";
    String output = ""
        + "Function.prototype.a = 1;\n"
        + "function F() {}\n"
        + "F.b = 2;\n";
    test(js, output);
  }

  public void testPredeclaredType() {
    String js =
        "goog.addDependency('zzz.js', ['goog.Foo'], []);" +
        "/** @constructor */ " +
        "function A() {" +
        "  this.x = 3;" +
        "}" +
        "/** @param {goog.Foo} x */" +
        "function f(x) { x.y = 4; }";
    String result =
        "0;" +
        "/** @constructor */ " +
        "function A() {" +
        "  this.a = 3;" +
        "}" +
        "/** @param {goog.Foo} x */" +
        "function f(x) { x.y = 4; }";
    test(js, result);
  }
}
