/*
 * Copyright 2010 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.jjs.impl;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JDeclarationStatement;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JValueLiteral;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.dev.jjs.impl.EnumOrdinalizer.Tracker;

/**
 * A set of tests for the conditions under which ordinalization is and is not allowed.  The
 * ordinalization is performed when allowed.
 * <p>
 * A complete test of the resulting ordinalization is not performed.  However, the
 * ImplementCastsAndTypeChecks and the EqualityNormalizer are run after the EnumOrdinalizer, to help
 * ensure the integrity of the AST, such that there are no partially mismatched type assignments or
 * comparisons, and that no binary operations between a primitive type and null have been added.
 * Typically, such errors introduced by the EnumOrdinalizer are caught by these normalizers, so it
 * makes sense to test the output in this way.  Thus, we provide confidence that the AST is left in
 * a coherent state, but it is not a complete test that ordinalization has completed correctly in
 * every respec.
 * <p>
 * NOTE: the tests in this test case are very fragile. On one hand EnumOrdinalizer requires other
 * passes to perform their cleanups to be effective; on the other some passes (DeadCodeElimination)
 * optimize enums in a way that the tests might become ineffective and some artificial constructs
 * are used to avoid this scenario. See {@link EnumOrdinalizerTest#setupNotInlineable()}.
 */
public class EnumOrdinalizerTest extends OptimizerTestBase {
  /*
   * Always run ImplementCastsAndTypeChecks and EqualityNormalizer, even in cases where we
   * are testing that ordinalization cannot occur, since there may be other
   * enums (such as DummyEnum) which do get ordinalized, and we want to test
   * that all is well regardless.
   */
  private final boolean performCastReplacement = true;
  private final boolean runEqualityNormalizer = true;
  // These are enabled as needed for a given test
  private boolean runMakeCallsStatic;
  private boolean runMethodInliner;
  private boolean runMethodCallTightener;
  private boolean runPruner;
  private boolean runTypeTightener;

  @Override
  protected void setUp() throws Exception {
    super.setUp();
    EnumOrdinalizer.enableTracker();
    EnumOrdinalizer.resetTracker();

    // defaults, can be overridden by individual test cases
    runTypeTightener = false;
    runMethodCallTightener = false;
    runMethodInliner = true;
    runMakeCallsStatic = true;
    // Opportunities for ordinalization are only present after unused references to $VALUES are
    // pruned.
    // NOTE: because we are pruning, each test case needs to make sure that enums that are
    // considered for ordinalization are still live.
    runPruner = true;
  }

  @Override
  protected void tearDown() throws Exception {
    super.tearDown();
  }

  public void testOrdinalizeBasicAssignment()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Fruit apple = Fruit.APPLE;",
        "Fruit orange = Fruit.ORANGE;");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeNewArrayAndAssignmentLocalRef()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result =
        optimize("void", "Fruit[] fruits = new Fruit[] {Fruit.APPLE, Fruit.ORANGE, Fruit.APPLE};",
            "if (fruits[0] == Fruit.APPLE) {",
            "  fruits[0] = Fruit.ORANGE;",
            "}");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeNewArrayOfArrayAndAssignmentLocalRef()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result =
        optimize("void", "Fruit[][] fruits = new Fruit[][] ",
            " {{Fruit.APPLE, Fruit.ORANGE},{Fruit.APPLE, Fruit.ORANGE}};",
            "if (fruits[0][1] == Fruit.APPLE) {",
            "  fruits[0][1] = Fruit.ORANGE;",
            "}");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeNewArrayAndAssignmentFieldRef()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("private final Fruit[] fruits = new Fruit[] ",
        "  {Fruit.APPLE, Fruit.ORANGE, Fruit.APPLE};");
    Result result = optimize("void", "EntryPoint ep = new EntryPoint();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizableFinalFieldUninitializedByDefault()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("private final Fruit uninitializedFinalFruit;",
        "public EntryPoint() {",
        "  uninitializedFinalFruit = Fruit.ORANGE;",
        "}");
    Result result = optimize("void", "EntryPoint ep = new EntryPoint();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeSwitchStatement()
      throws UnableToCompleteException {
    setupFruitEnum();
    setupFruitSwitchMethod();
    Result result = optimize("void", "String apple = fruitSwitch(Fruit.APPLE);",
        "String orange = fruitSwitch(Fruit.ORANGE);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeIfStatement()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl(
        "public static String fruitIf(Fruit fruit) {",
        " if (fruit == Fruit.APPLE) {",
        "   return \"Apple\";",
        " } else if (fruit == Fruit.ORANGE) {",
        "   return \"Orange\";",
        " } else {",
        "   return \"Unknown\";",
        " }",
        "}");
    Result result = optimize("void", "String apple = fruitIf(Fruit.APPLE);",
        "String orange = fruitIf(Fruit.ORANGE);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeConditional()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void",
        "Fruit fruit = (new Integer(1)).toString().isEmpty() ? Fruit.APPLE : Fruit.ORANGE;");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeFieldRefOrdinalMethodCall()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "int i = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeVariableRefOrdinalMethodCall()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "int i = fruit.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeUnusedEmptyEnum() throws UnableToCompleteException {
    setupEmptyEnum();

    Result result = optimize("void", "EmptyEnum myEnum;");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$EmptyEnum") ||
        !tracker.isVisited("test.EntryPoint$EmptyEnum"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeUnusedEnum() throws UnableToCompleteException {
    setupFruitEnum();

    Result result = optimize("void", "Fruit myEnum;");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit") ||
        !tracker.isVisited("test.EntryPoint$Fruit"));

    // This enum is not referenced and it has gone away before ordinalizing, however make
    // sure there are no references anyway.
    tracker.addOrdinalized("test.EntryPoint$Fruit");
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizeMethodCallExpressionOrdinalFieldRef()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static Fruit getResolvedFruit(Fruit fruit) {",
        "  if (fruit == Fruit.APPLE) {",
        "    return Fruit.ORANGE;",
        "  } else { ",
        "    return Fruit.APPLE;",
        "  }",
        "}");
    addSnippetClassDecl("public static int switchMethodCall(Fruit fruit) {",
        "  int retVal = 0;",
        "  switch (getResolvedFruit(fruit)) {",
        "    case APPLE: retVal = 12; break;",
        "    case ORANGE:retVal = 73; break;",
        "  }",
        "  return retVal;",
        "}");
    Result result = optimize("void", "int i = switchMethodCall(Fruit.APPLE);",
        "Fruit fruit = Fruit.ORANGE;",
        "int j = switchMethodCall(fruit);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizableStaticFieldRef()
      throws UnableToCompleteException {
    // this will cause a static field ref in the enum clinit
    setupFruitEnumWithStaticField();
    Result result = optimize("void",
        "String y = Fruit.staticField + Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizableStaticMethod()
      throws UnableToCompleteException {
    // this will cause a static method enum class
    setupFruitEnumWithStaticMethod();
    Result result = optimize("void", "int y = Fruit.staticMethod() + Fruit.APPLE.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizableCallingValues()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "int l = Fruit.values().length;",
        "int ord = Fruit.APPLE.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizableStaticFieldRefToVALUES()
      throws UnableToCompleteException {
    // this ends up inlining the values() method call, and thus $VALUES is referenced external
    // to the Fruit enum class.
    setupFruitEnum();
    Result result = optimize("void", "Fruit[] fruits = Fruit.values();",
        "int ord = Fruit.APPLE.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testOrdinalizableStaticMethodThatRefsValuesLength()
      throws UnableToCompleteException {
    // this will cause a static method that references values().length
    setupFruitEnumWithStaticMethodThatRefsValuesLength();
    Result result = optimize("void", "Fruit y = Fruit.forInteger(0);",
        "int ord = Fruit.APPLE.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableInstanceStaticFieldRef()
      throws UnableToCompleteException {
    // this will cause a static field ref in the enum clinit
    setupFruitEnumWithStaticField();
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "String y = fruit.staticField;");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableInstanceStaticMethod()
      throws UnableToCompleteException {
    // this will cause a static method enum class
    setupFruitEnumWithStaticMethod();
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "int y = fruit.staticMethod();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableClassLiteralReference()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Class clazz = Fruit.class;",
        "String clazzStr = clazz.toString() + Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableEnumValueOfWithClassLiteralArg()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Object Carrot = Enum.valueOf(Fruit.class, \"APPLE\");",
        "String carrot = Carrot.toString() + Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableGetClassMethodCall()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Class clazz = Fruit.APPLE.getClass();",
        "String clazzStr = clazz.toString() + Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableExplicitCastToEnumClass()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Object obj = new Object();",
        "Fruit fruit = (Fruit) obj;",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableExplicitCastToArrayOfEnumClass()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Enum[] enumArray = new Enum[10];",
        "Fruit[] fruitArray = (Fruit[]) enumArray;",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableExplicitCastFromArrayOfEnumClass()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Fruit[] fruitArray = new Fruit[10];",
        "Enum[] enumArray = (Enum[]) fruitArray;",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableExplicitCastToArrayOfArrayOfEnumClass()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Enum[][] enumArray = new Enum[10][10];",
        "Fruit[][] fruitArray = (Fruit[][]) enumArray;",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableExplicitCastFromArrayOfArrayOfEnumClass()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Fruit[][] fruitArray = new Fruit[10][10];",
        "Enum[][] enumArray = (Enum[][]) fruitArray;",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableExplicitCastFromEnumClass()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Enum Carrot = (Enum) Fruit.APPLE;",
        "String carrot = Carrot.toString();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableOrdinalMethodRefFromExplicitCastWithBlackListableSubExpression()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "int ord = " +
        "((Fruit) Enum.valueOf(Fruit.class,\"APPLE\")).ordinal();",
        "int ord2 = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableInstanceFieldRef()
      throws UnableToCompleteException {
    // this will cause an instance field ref in the enum constructor
    setupFruitEnumWithInstanceField();
    Result result = optimize("void", "String instanceField = Fruit.APPLE.instanceField;");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableInstanceOfEnumExpression()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "if (fruit instanceof Enum) {",
        "  fruit = Fruit.ORANGE;",
        "}");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableInstanceOfEnumTestType()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Object fruitObj = new Object();",
        "if (fruitObj instanceof Fruit) {",
        "  fruitObj = null;",
        "}",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableStaticMethodCallValues()
      throws UnableToCompleteException {
    // make sure values() method call doesn't doesn't get inlined
    runMethodInliner = false;

    setupFruitEnum();
    Result result = optimize("void", "Fruit[] fruits = Fruit.values();",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableJsTypeEnum()
      throws UnableToCompleteException {
    setupJsTypeEnums();
    addSnippetClassDecl(
        "public static JsFruit instanceFruit;",
        "public static JsCustom instanceCustom;");
    Result result = optimize("void",
        "instanceFruit = JsFruit.JSAPPLE;",
        "instanceCustom = JsCustom.VALUE1;");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$JsFruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$JsFruit"));

    assertTrue(tracker.isVisited("test.EntryPoint$JsCustom"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$JsCustom"));

    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  private void setupJsTypeEnums() {
    addSnippetImport("jsinterop.annotations.JsType");
    addSnippetClassDecl("@JsType public enum JsFruit {JSAPPLE, JSORANGE}");
    addSnippetClassDecl("@JsType public enum JsCustom {VALUE0, VALUE1 {} }");
  }

  public void testNotOrdinalizableJsniFieldRef()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static Fruit instanceFruit;");
    addSnippetClassDecl("public static native void jsniMethod() /*-{",
        "  var x = @test.EntryPoint::instanceFruit",
        "}-*/");
    Result result = optimize("void", "instanceFruit = Fruit.APPLE;",
        "jsniMethod();",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableJsniFieldRefStatic()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static native void jsniMethod() /*-{",
        "  var x = @test.EntryPoint.Fruit::APPLE",
        "}-*/");
    Result result = optimize("void", "jsniMethod();",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableJsniFieldRefClassLiteral()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static native void jsniMethod() /*-{",
        "  var x = @test.EntryPoint.Fruit::class",
        "}-*/");
    Result result = optimize("void", "jsniMethod();",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastBinaryOpAssignment()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void",
        "Enum tomato;",
        "tomato = Fruit.APPLE;",
        "int ord = tomato.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastFieldInitializedWithNullByDefault()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("static private Fruit uninitializedFruitAsNull;");
    Result result = optimize("void", "if (uninitializedFruitAsNull != Fruit.APPLE) {",
        "  uninitializedFruitAsNull = Fruit.ORANGE;",
        "}",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastBinaryOpEquals()
      throws UnableToCompleteException {
    setupFruitAndVegetableEnums();
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "Enum carrot = (Enum) Vegetable.CARROT;",
        "boolean test = (fruit == carrot);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertTrue(tracker.isVisited("test.EntryPoint$Vegetable"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Vegetable"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastBinaryOpNotEquals()
      throws UnableToCompleteException {
    setupFruitAndVegetableEnums();
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "Enum carrot = (Enum) Vegetable.CARROT;",
        // do in opposite order from OpEquals test
        "boolean test = (carrot != fruit);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertTrue(tracker.isVisited("test.EntryPoint$Vegetable"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Vegetable"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastBinaryOpEqualsNull()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static boolean testIsNull(Fruit fruit) {",
        "  if (fruit == null) {",
        "    return true;",
        "    } else {",
        "    return false;",
        "    }",
        "}");
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "boolean isNull = testIsNull(fruit) || testIsNull(Fruit.ORANGE);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastBinaryOpNotEqualsNull()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static boolean testIsNull(Fruit fruit) {",
        "  if (fruit != null) {",
        "    return true;",
        "  } else {",
        "    return false;",
        "  }",
        "}");
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "boolean isNull = testIsNull(fruit) || testIsNull(Fruit.ORANGE);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastBinaryOpStringConcat()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "String str = \"A string followed by \" + fruit;");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastBinaryOpStringConcat2()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "String str = fruit + \" followed by a string\";");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastBinaryOpStringConcatAssignment()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Fruit fruit = Fruit.APPLE;",
        "String str = \"A string concatenated with: \";",
        "str += fruit;");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastDeclarationToNull()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Fruit fruit = null;",
        "int ord = fruit == null ? Util.notInlineable(Fruit.APPLE).ordinal() : fruit.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastAssignmentToNull()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Fruit fruit;",
        "fruit = null;",
        "int ord = fruit == null ? Util.notInlineable(Fruit.APPLE).ordinal() : fruit.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastDeclaration()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Enum tomato = Fruit.APPLE;",
        "int ord = Fruit.APPLE.ordinal() + tomato.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastConditional()
      throws UnableToCompleteException {
    setupFruitAndVegetableEnums();
    Result result = optimize("void", "Enum tomato = null;",
        "tomato = (true) ? Fruit.APPLE : Vegetable.CARROT;",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal() +",
        "    Util.notInlineable(Vegetable.CARROT).ordinal() + tomato.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertTrue(tracker.isVisited("test.EntryPoint$Vegetable"));
    assertTrue(tracker.isOrdinalized("test.EntryPoint$Vegetable"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastOverriddenMethodReturnType()
      throws UnableToCompleteException {

    // this test depends on the tighteners running
    runTypeTightener = true;
    runMethodCallTightener = true;

    /*
     * Create methods with covariant return type, which the MethodTypeTightener
     * will optimize to no longer be covariant, so make sure we check original
     * overridden method type.
     */
    addSnippetClassDecl("public interface EnumInterface {",
        "  String name();",
        "}");
    addSnippetClassDecl("public abstract class AbstractClass<T extends EnumInterface> {",
        "  public abstract T getEnumClass();",
        "}");
    addSnippetClassDecl("public class CustomClass1 extends AbstractClass<EnumClass1> {",
        "  public EnumClass1 getEnumClass() { return EnumClass1.CONST1; }",
        "}");
    addSnippetClassDecl("public class CustomClass2 extends AbstractClass<EnumClass2> {",
        "  public EnumClass2 getEnumClass() { return EnumClass2.CONST2; }",
        "}");
    addSnippetClassDecl("public enum EnumClass1 implements EnumInterface {",
        "  CONST1;",
        "}");
    addSnippetClassDecl("public enum EnumClass2 implements EnumInterface {",
        "  CONST2;",
        "}");
    addSnippetClassDecl("public static void testEnumClass(AbstractClass abstractClass) {",
        "  EnumInterface enumClass = abstractClass.getEnumClass();",
        "}");
    Result result = optimize("void", "EntryPoint ep = new EntryPoint();",
        "AbstractClass abstractClass1 = ep.new CustomClass1();",
        "AbstractClass abstractClass2 = ep.new CustomClass2();",
        "testEnumClass(abstractClass1);",
        "testEnumClass(abstractClass2);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$EnumClass1"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$EnumClass1"));
    assertTrue(tracker.isVisited("test.EntryPoint$EnumClass2"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$EnumClass2"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastMethodCallArgs()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static String getEnumString(Enum myEnum) {",
        // make sure this method does something so not inlined
        "  int ord = myEnum.ordinal();",
        "  String retString = \"\";",
        "  for (int i = 0;i<ord;i++) {",
        "    retString += \"-\";",
        "  }",
        "  retString += myEnum.name();",
        "  return retString;",
        "}");
    Result result = optimize("void", "String stringApple = getEnumString(Fruit.APPLE);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastMethodCallArgsNewArray()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static String getEnumString(Enum[] myEnumArray) {",
        "  String retString = \"\";",
        "  for (Enum myEnum : myEnumArray) {",
        "    retString += myEnum.name();",
        "  }",
        "  return retString;",
        "}");
    Result result = optimize("void",
        "String stringFruits = getEnumString(new Enum[] {Fruit.APPLE, Fruit.ORANGE});");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastMethodCallVarArgs()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static String getEnumString(Enum...myEnumArray) {",
        "  String retString = \"\";",
        "  for (Enum myEnum : myEnumArray) {",
        "    retString += myEnum.name();",
        "  }",
        "  return retString;",
        "}");
    Result result = optimize("void",
        "String stringFruits = getEnumString(Fruit.APPLE, Fruit.ORANGE);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastNewArrayElements()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void", "Enum[] enums = new Enum[] {Fruit.APPLE, Fruit.ORANGE};");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastNewArrayArrayElements()
      throws UnableToCompleteException {
    setupFruitEnum();
    Result result = optimize("void",
        "Enum[][] enums = new Enum[][] {{Fruit.APPLE, Fruit.ORANGE},{Fruit.ORANGE, Fruit.APPLE}};");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableJsniMethodBodyParams()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static native void passEnumToJsniMethod(Enum myEnum) /*-{",
        "  myEnum == null; }-*/");
    Result result = optimize("void", "passEnumToJsniMethod(Fruit.APPLE);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastJsniMethodBodyParams()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static native void passEnumToJsniMethod(Fruit myEnum) /*-{",
        "   myEnum == null; }-*/;");
    Result result = optimize("void", "passEnumToJsniMethod(Fruit.APPLE);");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableJsniMethodBodyCall()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static native void consumeFruitViaJsni() /*-{",
        "  var myJso = @test.EntryPoint::calledFromJsni(*)();",
        "}-*/;",
        "public static Fruit calledFromJsni() {",
        "  return Fruit.APPLE;",
        "}");
    Result result = optimize("void", "consumeFruitViaJsni();",
        "int ord = Fruit.APPLE.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastJsniMethodRefParams()
      throws UnableToCompleteException {
    setupFruitEnum();
    setupFruitSwitchMethod();
    addSnippetClassDecl("public static native void fruitSwitchViaJsni() /*-{",
        "  var myJso;",
        "  var result = @test.EntryPoint::fruitSwitch(Ltest/EntryPoint$Fruit;)(myJso);",
        "}-*/");
    Result result = optimize("void", "fruitSwitchViaJsni();",
        "int ord = Util.notInlineable(Fruit.APPLE).ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastJsniMethodRefReturnType()
      throws UnableToCompleteException {
    setupFruitEnum();
    addSnippetClassDecl("public static Fruit returnSomeFruit() {",
        "  return Fruit.APPLE;",
        "}");
    addSnippetClassDecl("public static native void jsniMethodRefWithEnumReturn() /*-{",
        "  var result = @test.EntryPoint::returnSomeFruit()();",
        "}-*/");
    Result result = optimize("void", "jsniMethodRefWithEnumReturn();",
        "int ord = Fruit.APPLE.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  public void testNotOrdinalizableImplicitUpcastReturnStatement()
      throws UnableToCompleteException {
    setupFruitAndVegetableEnums();
    addSnippetClassDecl("public static Enum returnAsEnum(int mode) {",
        "  if (mode == 0) {",
        "    return Fruit.APPLE;",
        "  } else {",
        "    return Vegetable.CARROT;",
        "  }",
        "}");
    Result result = optimize("void", "Enum myEnum = returnAsEnum(0);",
        // do a second one, to prevent inlining
        "Enum myOtherEnum = returnAsEnum(1);",
        "int ord = myEnum.ordinal() + myOtherEnum.ordinal();");

    EnumOrdinalizer.Tracker tracker = EnumOrdinalizer.getTracker();
    assertTrue(tracker.isVisited("test.EntryPoint$Fruit"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Fruit"));
    assertTrue(tracker.isVisited("test.EntryPoint$Vegetable"));
    assertFalse(tracker.isOrdinalized("test.EntryPoint$Vegetable"));
    assertAllEnumOrdinalizedReferencesReplaced(result.getOptimizedProgram(), tracker);
  }

  private void setupEmptyEnum() {
    addSnippetClassDecl("public enum EmptyEnum {}");
  }

  private void setupFruitEnum() {
    addSnippetClassDecl("public enum Fruit {APPLE, ORANGE}");
    setupNotInlineable("Fruit");
  }

  private void setupFruitEnumWithInstanceField() {
    addSnippetClassDecl("public enum Fruit {APPLE(\"a\"), ORANGE(\"b\");",
        "  public final String instanceField;",
        "  private Fruit(String str) {",
        "    instanceField = str;",
        "  }",
        "}");
    setupNotInlineable("Fruit");
  }

  private void setupFruitEnumWithStaticField() {
    addSnippetClassDecl("public enum Fruit {APPLE, ORANGE;",
        "  public static String staticField = \"STATIC\";",
        "}");
    setupNotInlineable("Fruit");
  }

  private void setupFruitEnumWithStaticMethod() {
    addSnippetImport("javaemul.internal.annotations.DoNotInline");
    addSnippetClassDecl("public enum Fruit {APPLE, ORANGE;",
        "  @DoNotInline",
        "  public static final int staticMethod() {",
        "    int x = 0;",
        "    return x;",
        "  }",
        "}");
    setupNotInlineable("Fruit");
  }

  private void setupFruitEnumWithStaticMethodThatRefsValuesArray() {
    // add a little extra logic here, to prevent inlining
    addSnippetClassDecl("public enum Fruit {APPLE, ORANGE;",
        "  public static Fruit forInteger(int value) {",
        "    if (value < 0 || value >= 2) {",
        "      return ORANGE;",
        "    }",
        "    return Fruit.values()[value];",
        "  }",
        "}");
  }

  private void setupFruitEnumWithStaticMethodThatRefsValuesLength() {
    addSnippetClassDecl("public enum Fruit {APPLE, ORANGE;",
        "  public static Fruit forInteger(int value) {",
        "    if (value < 0 || value >= Fruit.values().length) {",
        "      return ORANGE;",
        "    }",
        "    return APPLE;",
        "  }",
        "}");
    setupNotInlineable("Fruit");
  }

  private void setupVegetableEnum() {
    addSnippetClassDecl("public enum Vegetable {CARROT, SPINACH}");
    setupNotInlineable("Vegetable");
  }

  private void setupFruitAndVegetableEnums() {
    addSnippetClassDecl("public enum Fruit {APPLE, ORANGE}");
    addSnippetClassDecl("public enum Vegetable {CARROT, SPINACH}");
    setupNotInlineable("Fruit", "Vegetable");
  }

  private void setupFruitSwitchMethod() {
    addSnippetClassDecl("public static String fruitSwitch(Fruit fruit) {",
        " switch(fruit) {",
        "   case APPLE: return \"Apple\";",
        "   case ORANGE: return \"Orange\";",
        "   default: return \"Unknown\";",
        " }",
        "}");
  }
  private void setupNotInlineable(String... classes) {
    addSnippetClassDecl("public static class Util {");
    for (String clazz : classes) {
      addSnippetClassDecl(
          "  public static " + clazz + " notInlineable(" + clazz + " obj) {",
          "    if (new Integer(1).toString().isEmpty()) return obj;",
          "    return obj;",
          "  }");
    }
    addSnippetClassDecl("}");
  }

  @Override
  protected boolean doOptimizeMethod(TreeLogger logger, JProgram program, JMethod method) {
    /*
     * EnumOrdinalizer depends MakeCallsStatic and MethodInliner running before
     * it runs, since they cleanup the internal structure of an enum class to
     * inline instance methods like $init.
     *
     * TypeTightener and methodCallTightener are necessary to test some cases
     * involving implicit casts on overridden method call return types.
     *
     * These are a subset of the actual optimizers run in JJS.optimizeLoop().
     */
    boolean didChange = false;
    program.addEntryMethod(findMainMethod(program));

    OptimizerContext optimizerContext = new FullOptimizerContext(program);
    if (runMakeCallsStatic) {
      didChange = MakeCallsStatic.exec(program, false, optimizerContext).didChange() || didChange;
    }
    if (runTypeTightener) {
      didChange = TypeTightener.exec(program, optimizerContext).didChange() || didChange;
    }
    if (runMethodCallTightener) {
      didChange = MethodCallTightener.exec(program, optimizerContext).didChange() || didChange;
    }
    if (runMethodInliner) {
      didChange = MethodInliner.exec(program, optimizerContext).didChange() || didChange;
    }
    if (runPruner) {
      didChange = Pruner.exec(program, true, optimizerContext).didChange() || didChange;
    }

    didChange = EnumOrdinalizer.exec(program, optimizerContext).didChange() || didChange;

    /*
     * Run these normalizers to sanity check the AST.  If there are any
     * dangling type substitutions, the ImplementCastsAndTypeChecks will generally find it.
     * If there are any introduced binary ops between an int and a null, the
     * EqualityNormalizer will find it.
     */
    if (performCastReplacement) {
      ComputeCastabilityInformation.exec(program, false);
      ImplementCastsAndTypeChecks.exec(program, false);
    }
    if (runEqualityNormalizer) {
      EqualityNormalizer.exec(program);
    }

    return didChange;
  }

  private void assertAllEnumOrdinalizedReferencesReplaced(JProgram program, final Tracker tracker) {
    new JVisitor() {
      @Override
      public void endVisit(JFieldRef x, Context ctx) {
        assertTrue(x.getField() + " was not replaced everywhere",
            ctx.isLvalue() || !tracker.isOrdinalized(x.getEnclosingType().getName()));
      }

      @Override
      public void endVisit(JDeclarationStatement x, Context ctx) {
        assertTrue(x.getVariableRef().getTarget() + " was not replaced everywhere",
            x.getInitializer() instanceof JValueLiteral ||
                !(x.getVariableRef() instanceof JFieldRef) ||
                !tracker.isOrdinalized(
                    ((JFieldRef) x.getVariableRef()).getField().getEnclosingType().getName()));
      }
    }.accept(program);
  }
}
