/*
 * Copyright 2008 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.core.ext.TreeLogger;
import com.google.gwt.dev.javac.CompilationUnit.GeneratedClassnameFinder;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;

import junit.framework.TestCase;

import java.util.List;

/**
 * Test-cases to check that we indeed obtain the correct list of nested types
 * with generated classNames by examining bytecodes using ASM.
 *
 */
public class GeneratedClassnameFinderTest extends TestCase {
  enum EnumClass {
    A, B, C,
  }

  static class MainClass {
    static class NestedClass {
      void foo() {
        TestInterface c = new TestInterface() {
          @Override
          public void foo() {
          }
        };
        EnumClass et = EnumClass.A;
        switch (et) {
          case A:
            break;
        }
        TestInterface d = new TestInterface() {
          @Override
          public void foo() {
          }
        };
      }
    }

    void foo() {
      TestInterface a = new TestInterface() {
        @Override
        public void foo() {
        }
      };
      EnumClass et = EnumClass.A;
      switch (et) {
        case A:
          break;
      }
      TestInterface b = new TestInterface() {
        @Override
        public void foo() {
        }
      };
    }
  }
  interface TestInterface {
    void foo();
  }

  static final TreeLogger logger = new PrintWriterTreeLogger();

  public void test() {
    String mainClassName = this.getClass().getName().replace('.', '/');
    assertEquals(
        4,
        new GeneratedClassnameFinder(logger, mainClassName).getClassNames().size());
    assertEquals(0, new GeneratedClassnameFinder(logger, mainClassName
        + "$EnumClass").getClassNames().size());
    assertEquals(0, new GeneratedClassnameFinder(logger, mainClassName
        + "$TestInterface").getClassNames().size());
    assertEquals(4, new GeneratedClassnameFinder(logger, mainClassName
        + "$MainClass").getClassNames().size());
    assertEquals(2, new GeneratedClassnameFinder(logger, mainClassName
        + "$MainClass$NestedClass").getClassNames().size());
  }

  public void testAbstractNative() {
    assertEquals(2, new AbstractNativeTester().getGeneratedClasses().size());
  }

  public void testAnonymous() {
    assertEquals(1, new AnonymousTester().getGeneratedClasses().size());
  }

  public void testEnum() {
    assertEquals(0, new EnumTester().getGeneratedClasses().size());
  }

  public void testJavacWeirdness() {
    List<String> classNames = new JavacWeirdnessTester().getGeneratedClasses();
    if (classNames.size() == 4) {
      // javac8:
      // JavacWeirdnessTester$1
      // JavacWeirdnessTester$2
      // JavacWeirdnessTester$2Foo
      // JavacWeirdnessTester$3Foo
      assertFalse(classNames.get(0) + " should not contain Foo",
              classNames.get(0).contains("Foo"));
      assertFalse(classNames.get(1) + " should not contain Foo",
              classNames.get(1).contains("Foo"));
      assertTrue(classNames.get(2) + " should contain Foo", classNames.get(2).contains("Foo"));
      assertTrue(classNames.get(3) + " should contain Foo", classNames.get(3).contains("Foo"));
    } else if (classNames.size() == 5) {
      // javac22:
      // JavacWeirdnessTester$1
      // JavacWeirdnessTester$2
      // JavacWeirdnessTester$1Foo
      // JavacWeirdnessTester$2Foo
      // JavacWeirdnessTester$3Foo
      assertFalse(classNames.get(0) + " should not contain Foo",
              classNames.get(0).contains("Foo"));
      assertFalse(classNames.get(1) + " should not contain Foo",
              classNames.get(1).contains("Foo"));
      assertTrue(classNames.get(2) + " should contain Foo", classNames.get(2).contains("Foo"));
      assertTrue(classNames.get(3) + " should contain Foo", classNames.get(2).contains("Foo"));
      assertTrue(classNames.get(4) + " should contain Foo", classNames.get(3).contains("Foo"));
    } else {
      fail("Expected 4 or 5 classes, found " + classNames);
    }
  }

  public void testNamedLocal() {
    List<String> generatedClasses = new NamedLocalTester().getGeneratedClasses();
    assertTrue(generatedClasses.toString(),
            generatedClasses.size() == 2 || generatedClasses.size() == 3);
  }

  public void testNested() {
    assertEquals(2, new NestedTester().getGeneratedClasses().size());
  }

  public void testStatic() {
    assertEquals(0, new StaticTester().getGeneratedClasses().size());
  }

  public void testTopLevel() {
    assertEquals(1, new TopLevelTester().getGeneratedClasses().size());
  }


}

/**
 * For testing a class containing anonymous inner classes with abstract and
 * native methods.
 */
class AbstractNativeTester {
  void foo() {
    abstract class Fooer {
      abstract void foo();
    }
    Fooer a = new Fooer() {
      @Override
      native void foo();
    };
    a.foo();
  }

  List<String> getGeneratedClasses() {
    return (new GeneratedClassnameFinder(GeneratedClassnameFinderTest.logger,
        this.getClass().getName().replace('.', '/'))).getClassNames();
  }
}

/**
 * For testing a class containing an anonymous inner class.
 */
class AnonymousTester {
  interface TestInterface {
    void foo();
  }

  void foo() {
    TestInterface a = new TestInterface() {
      @Override
      public void foo() {
      }
    };
    a.foo();
  }

  List<String> getGeneratedClasses() {
    return (new GeneratedClassnameFinder(GeneratedClassnameFinderTest.logger,
        this.getClass().getName().replace('.', '/'))).getClassNames();
  }
}

/**
 * For testing a class with an Enum (for which javac generates a synthetic
 * class).
 */
class EnumTester {
  enum EnumClass {
    A, B, C,
  }

  void foo() {
    EnumClass et = EnumClass.A;
    switch (et) {
      case A:
        break;
    }
  }

  List<String> getGeneratedClasses() {
    return (new GeneratedClassnameFinder(GeneratedClassnameFinderTest.logger,
        this.getClass().getName().replace('.', '/'))).getClassNames();
  }
}

/**
 * Javac generates weird code for the following class. It passes a synthetic
 * class ...Tester$1 as a first parameter to constructors of Fuji and Granny.
 * Normally, it generates the synthetic class, but in this case, it decides not
 * to generate the class. However, the bytecode still has reference to the
 * synthetic class -- it just passes null for the synthetic class.
 *
 * This code also tests for an anonymous class extending a named local class.
 */
class JavacWeirdnessTester {
  private abstract static class Apple implements Fruit {
  }

  private static interface Fruit {
  }

  private static class Fuji extends Apple {
  }

  private static class Granny extends Apple {
  }

  private static volatile boolean TRUE = true;

  List<String> getGeneratedClasses() {
    return (new GeneratedClassnameFinder(GeneratedClassnameFinderTest.logger,
        this.getClass().getName().replace('.', '/'))).getClassNames();
  }

  private void assertEquals(Object a, Object b) {
  }

  private void testArrayStore() {
    Apple[] apple = TRUE ? new Granny[3] : new Apple[3];
    Apple g = TRUE ? (Apple) new Granny() : (Apple) new Fuji();
    Apple a = apple[0] = g;
    assertEquals(g, a);
  }

  @SuppressWarnings("ReturnValueIgnored")
  private void testDeadTypes() {
    if (false) {
      new Object() {
      }.toString();

      class Foo {
        void a() {
        }
      }
      new Foo().a();
    }
  }

  private void testLocalClasses() {
    class Foo {
      public Foo(int j) {
        assertEquals(1, j);
      }
    }
    final int i;
    new Foo(i = 1) {
      {
        assertEquals(1, i);
      }
    };
    assertEquals(1, i);
  }

  private void testReturnStatementInCtor() {
    class Foo {
      int i;

      Foo(int i) {
        this.i = i;
        if (i == 0) {
          return;
        } else if (i == 1) {
          return;
        }
        return;
      }
    }
    assertEquals(new Foo(0).i, 0);
  }
}

/**
 * For testing a class with a generated name like $1Foo.
 */
class NamedLocalTester {
  void foo1() {
    if (false) {
      class Foo {
        void foo() {
        }
      }
      new Foo().foo();
    }
  }

  void foo2() {
    class Foo {
      void foo() {
      }
    }
    new Foo().foo();
  }

  void foo3() {
    class Foo {
      void foo() {
      }
    }
    new Foo().foo();
  }

  List<String> getGeneratedClasses() {
    return (new GeneratedClassnameFinder(GeneratedClassnameFinderTest.logger,
        this.getClass().getName().replace('.', '/'))).getClassNames();
  }
}

/**
 * For testing that nested classes are examined recursively for classes with
 * generated names.
 */
class NestedTester {
  class MainClass {
    class NestedClass {
      void foo() {
        class Foo {
          void bar() {
          }
        }
        new Foo().bar();
      }
    }

    void foo() {
      class Foo {
        void bar() {
        }
      }
      new Foo().bar();
    }
  }

  List<String> getGeneratedClasses() {
    return (new GeneratedClassnameFinder(GeneratedClassnameFinderTest.logger,
        this.getClass().getName().replace('.', '/'))).getClassNames();
  }
}

/**
 * For testing classes with private static members (javac generates a synthetic
 * class here but the jdt does not).
 */
class StaticTester {
  private abstract static class Apple implements Fruit {
  }

  private static interface Fruit {
    void bar();
  }

  private static class Fuji extends Apple {
    @Override
    public void bar() {
    }
  }

  private static class Granny extends Apple {
    @Override
    public void bar() {
    }
  }

  List<String> getGeneratedClasses() {
    return (new GeneratedClassnameFinder(GeneratedClassnameFinderTest.logger,
        this.getClass().getName().replace('.', '/'))).getClassNames();
  }

}

/**
 * For testing that a class with a generated name inside another top-level class
 * is found.
 */
class TopLevelTester {
  public void foo() {
    GeneratedClassnameFinderTest.TestInterface a = new GeneratedClassnameFinderTest.TestInterface() {
      @Override
      public void foo() {
      }
    };
  }

  List<String> getGeneratedClasses() {
    return (new GeneratedClassnameFinder(GeneratedClassnameFinderTest.logger,
        this.getClass().getName().replace('.', '/'))).getClassNames();
  }
}
