/*
 * Copyright 2011 Google Inc.
 *
 * 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.gwt.dev.javac;

import com.google.gwt.dev.javac.testing.impl.MockJavaResource;

import org.eclipse.jdt.core.compiler.CategorizedProblem;

/**
 * Tests for {@link BytecodeSignatureMaker}
 */
public class BytecodeSignatureMakerTest extends CompilationStateTestBase {
  static final String CLASS_DEP_TYPE_NAME = "test.ClassDependency";

  public void testClassDependencySignature() {
    final MockJavaResource CLASS_DEP_ORIG =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };
    // A verbatim copy of CLASS_DEP_ORIG
    final MockJavaResource CLASS_DEP_NO_CHANGE =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_NO_PRIVATE =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            // Missing fieldPrivate
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            // Missing methodPrivate
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_NO_PROTECTED_FIELD =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  public int fieldPublic;\n");
            // missing fieldProtected
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_NO_DEFAULT_FIELD =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            // missing fieldDefault
            code.append("  private int fieldPrivate;\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_NO_PUBLIC_FIELD =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            // missing public field
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_FIELD_VALUE_CHANGE =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            // Value was 100
            code.append("  static public final int fieldPublicStatic = 99;\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_ORDER =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            // re-ordered this field
            code.append("  public int fieldPublic;\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  public int methodPublic() {return 1;};\n");
            // re-ordered this method
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_INNER =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            // Added an inner class definition
            code.append("  public static class IgnoreMe {\n");
            code.append("    private int ignoreThisMember;\n");
            code.append("  }\n");
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_DEPRECATED_FIELD =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  @Deprecated\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_DEPRECATED_METHOD =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  @Deprecated\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };

    final MockJavaResource CLASS_DEP_ANNOTATED_FIELD =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  @TestAnnotation(\"Foo\")\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_ANNOTATED_METHOD =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  @TestAnnotation(\"Foo\")\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };
    final MockJavaResource CLASS_DEP_JAVADOC =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  /** a static field */\n");
            code.append("  static public final int fieldPublicStatic = 100;\n");
            code.append("  /** a public field */\n");
            code.append("  public int fieldPublic;\n");
            code.append("  protected int fieldProtected;\n");
            code.append("  int fieldDefault;\n");
            code.append("  private int fieldPrivate;\n");
            code.append("  /** a public method */\n");
            code.append("  public int methodPublic() {return 1;};\n");
            code.append("  protected int methodProtected(String arg) {return 1;};\n");
            code.append("  int methodDefault() {return 1;};\n");
            code.append("  private int methodPrivate(){return 1;};\n");
            code.append("}");
            return code;
          }
        };

    final MockJavaResource TEST_ANNOTATION =
        new MockJavaResource("test.TestAnnotation") {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public @interface TestAnnotation {\n");
            code.append("  String value();");
            code.append("}\n");
            return code;
          }
        };
    CompiledClass originalClass = buildClass(CLASS_DEP_ORIG);
    assertNotNull(originalClass);

    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_NO_CHANGE));
    assertSignaturesNotEqual(originalClass, buildClass(CLASS_DEP_NO_PRIVATE));
    assertSignaturesNotEqual(originalClass,
        buildClass(CLASS_DEP_NO_PUBLIC_FIELD));
    assertSignaturesNotEqual(originalClass,
        buildClass(CLASS_DEP_NO_PROTECTED_FIELD));
    assertSignaturesNotEqual(originalClass,
        buildClass(CLASS_DEP_NO_DEFAULT_FIELD));
    assertSignaturesNotEqual(originalClass,
        buildClass(CLASS_DEP_FIELD_VALUE_CHANGE));
    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_ORDER));
    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_INNER));
    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_DEPRECATED_FIELD));
    assertSignaturesEqual(originalClass,
        buildClass(CLASS_DEP_DEPRECATED_METHOD));

    oracle.add(TEST_ANNOTATION);
    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_ANNOTATED_FIELD));
    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_ANNOTATED_METHOD));
    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_JAVADOC));
  }

  public void testClassDependencySignatureWithExceptions() {
    MockJavaResource ILLEGAL_STATE_EXCEPTION =
        new MockJavaResource("java.lang.IllegalStateException") {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package java.lang;\n");
            code.append("public class IllegalStateException extends Throwable {}\n");
            return code;
          }
        };
    MockJavaResource NUMBER_FORMAT_EXCEPTION =
        new MockJavaResource("java.lang.NumberFormatException") {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package java.lang;\n");
            code.append("public class NumberFormatException extends Throwable {}\n");
            return code;
          }
        };
    MockJavaResource CLASS_DEP_EXCEPTION_ORIG =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  public int methodPublic(String arg) ");
            code.append("      throws IllegalStateException, NumberFormatException {");
            code.append("    return 1;\n");
            code.append("  }\n");
            code.append("}\n");
            return code;
          }
        };
    MockJavaResource CLASS_DEP_EXCEPTION_MOD1 =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            // no exceptions declared
            code.append("  public int methodPublic(String arg) {return 1;};\n");
            code.append("}");
            return code;
          }
        };
    MockJavaResource CLASS_DEP_EXCEPTION_MOD2 =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            // one exception declared
            code.append("  public int methodPublic(String arg)");
            code.append("     throws IllegalStateException {");
            code.append("    return 1;\n");
            code.append("  }\n");
            code.append("}");
            return code;
          }
        };
    MockJavaResource CLASS_DEP_EXCEPTION_MOD3 =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency {\n");
            code.append("  public int methodPublic(String arg)");
            // order of declared exceptions is flipped
            code.append("     throws NumberFormatException, IllegalStateException {");
            code.append("    return 1;\n");
            code.append("  }\n");
            code.append("}");
            return code;
          }
        };

    oracle.add(ILLEGAL_STATE_EXCEPTION);
    oracle.add(NUMBER_FORMAT_EXCEPTION);
    CompiledClass originalClass = buildClass(CLASS_DEP_EXCEPTION_ORIG);
    assertSignaturesNotEqual(originalClass,
        buildClass(CLASS_DEP_EXCEPTION_MOD1));
    assertSignaturesNotEqual(originalClass,
        buildClass(CLASS_DEP_EXCEPTION_MOD2));
    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_EXCEPTION_MOD3));
  }

  public void testClassDependencySignatureWithGenerics() {
    MockJavaResource CLASS_DEP_GENERIC_ORIG =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("public class ClassDependency<T> {\n");
            code.append("  public int methodPublic(T arg) {return 1;};\n");
            code.append("}");
            return code;
          }
        };
    MockJavaResource CLASS_DEP_GENERIC_PARAMETERIZED =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("import java.util.Map;");
            code.append("public class ClassDependency<T extends Map> {\n");
            code.append("  public int methodPublic(T arg) {return 1;};\n");
            code.append("}");
            return code;
          }
        };
    CompiledClass originalClass = buildClass(CLASS_DEP_GENERIC_ORIG);
    assertSignaturesNotEqual(originalClass,
        buildClass(CLASS_DEP_GENERIC_PARAMETERIZED));
  }

  public void testClassDependencySignatureWithInterfaces() {
    MockJavaResource CLASS_DEP_INTERFACE_ORIG =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("import java.util.Map;");
            code.append("import java.util.Collection;");
            code.append("public class ClassDependency implements Map, Collection {\n");
            code.append("  public int methodPublic(String arg) { return 1;}\n");
            code.append("}\n");
            return code;
          }
        };
    MockJavaResource CLASS_DEP_INTERFACE_MOD1 =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("import java.util.Map;");
            code.append("import java.util.Collection;");
            // no interfaces
            code.append("public class ClassDependency {\n");
            code.append("  public int methodPublic(String arg) { return 1;}\n");
            code.append("}\n");
            return code;
          }
        };
    MockJavaResource CLASS_DEP_INTERFACE_MOD2 =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("import java.util.Map;");
            code.append("import java.util.Collection;");
            // only one interface
            code.append("public class ClassDependency implements Map {\n");
            code.append("  public int methodPublic(String arg) { return 1;}\n");
            code.append("}\n");
            return code;
          }
        };
    MockJavaResource CLASS_DEP_INTERFACE_MOD3 =
        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
          @Override
          public CharSequence getContent() {
            StringBuilder code = new StringBuilder();
            code.append("package test;\n");
            code.append("import java.util.Map;");
            code.append("import java.util.Collection;");
            // flipped order of interface decls
            code.append("public class ClassDependency implements Collection, Map {\n");
            code.append("  public int methodPublic(String arg) { return 1;}\n");
            code.append("}\n");
            return code;
          }
        };
    CompiledClass originalClass = buildClass(CLASS_DEP_INTERFACE_ORIG);
    assertSignaturesNotEqual(originalClass,
        buildClass(CLASS_DEP_INTERFACE_MOD1));
    assertSignaturesNotEqual(originalClass,
        buildClass(CLASS_DEP_INTERFACE_MOD2));
    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_INTERFACE_MOD3));
  }

  private void assertSignaturesEqual(CompiledClass original,
      CompiledClass updated) {
    String originalSignature =
        BytecodeSignatureMaker.getCompileDependencySignature(original.getBytes());
    String updatedSignature =
        BytecodeSignatureMaker.getCompileDependencySignature(updated.getBytes());
    if (!originalSignature.equals(updatedSignature)) {
      String originalRaw =
          BytecodeSignatureMaker.getCompileDependencyRawSignature(original.getBytes());
      String updatedRaw =
          BytecodeSignatureMaker.getCompileDependencyRawSignature(updated.getBytes());
      fail("Signatures don't match.  raw data expected=<" + originalRaw
          + "> actual=<" + updatedRaw + ">");
    }
  }

  private void assertSignaturesNotEqual(CompiledClass original,
      CompiledClass updated) {
    String originalSignature =
        BytecodeSignatureMaker.getCompileDependencySignature(original.getBytes());
    String updatedSignature =
        BytecodeSignatureMaker.getCompileDependencySignature(updated.getBytes());
    if (originalSignature.equals(updatedSignature)) {
      String originalRaw =
          BytecodeSignatureMaker.getCompileDependencyRawSignature(original.getBytes());
      String updatedRaw =
          BytecodeSignatureMaker.getCompileDependencyRawSignature(updated.getBytes());
      fail("Signatures should not match.  raw data expected=<" + originalRaw
          + "> actual=<" + updatedRaw + ">");
    }
  }

  private CompiledClass buildClass(MockJavaResource resource) {
    oracle.addOrReplace(resource);
    this.rebuildCompilationState();
    CompilationUnit unit =
        state.getCompilationUnitMap().get(resource.getTypeName());
    assertNotNull(unit);
    String internalName = resource.getTypeName().replace(".", "/");
    CategorizedProblem[] problems = unit.getProblems();
    if (problems != null && problems.length != 0) {
      fail(problems[0].toString());
    }
    for (CompiledClass cc : unit.getCompiledClasses()) {
      if (cc.getInternalName().equals(internalName)) {
        return cc;
      }
    }
    fail("Couldn't find class " + internalName + " after compiling.");
    return null;
  }
}
