/*
 * 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.jjs.test;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.junit.client.GWTTestCase;

/**
 * Tests {@link JavaScriptObject} and subclasses.
 */
public class JsoTest extends GWTTestCase {

  static class Bar extends JavaScriptObject {
    public static int field;

    public static native String staticNative() /*-{
      return "nativeBar";
    }-*/;

    public static String staticValue() {
      return "Bar" + field;
    }

    protected Bar() {
    }

    public final native String getBar() /*-{
      return this.bar;
    }-*/;

    public final String value() {
      return "Bar";
    }
  }

  static final class ClinitStaticFieldFirst extends JavaScriptObject {
    public static String[] FIELD = new String[]{"a","b","c"};
    protected ClinitStaticFieldFirst() {
    }
    public Object invokeInstanceMethod() {
      if (FIELD == null) {
        return null;
      }
      return FIELD;
    }
    public static Object invokeStaticMethod() {
      if (FIELD == null) {
        return null;
      }
      return FIELD;
    }
  }

  static final class ClinitInstanceMethodFirst extends JavaScriptObject {
    public static String[] FIELD = new String[]{"a","b","c"};
    protected ClinitInstanceMethodFirst() {
    }
    public Object invokeInstanceMethod() {
      if (FIELD == null) {
        return null;
      }
      return FIELD;
    }
    public static Object invokeStaticMethod() {
      if (FIELD == null) {
        return null;
      }
      return FIELD;
    }
  }

  static final class ClinitStaticMethodFirst extends JavaScriptObject {
    public static String[] FIELD = new String[]{"a","b","c"};
    protected ClinitStaticMethodFirst() {
    }
    public Object invokeInstanceMethod() {
      if (FIELD == null) {
        return null;
      }
      return FIELD;
    }
    public static Object invokeStaticMethod() {
      if (FIELD == null) {
        return null;
      }
      return FIELD;
    }
  }

  static class Foo extends JavaScriptObject {
    public static int field;

    public static native String staticNative() /*-{
      return "nativeFoo";
    }-*/;
    
    /**
     * Ensure that a supertype can refer to members of a subtype.
     */
    public static native String staticNativeToSub() /*-{
      return @com.google.gwt.dev.jjs.test.JsoTest.FooSub::staticValueSub()();
    }-*/;

    public static String staticValue() {
      return "Foo" + field;
    }

    protected Foo() {
    }

    public final native String getFoo() /*-{
      return this.foo;
    }-*/;

    public final String value() {
      return "Foo";
    }
  }

  static class FooSub extends Foo {
    static String staticValueSub() {
      return "FooSub";
    }
    
    protected FooSub() {
    }

    public final String anotherValue() {
      return "Still Foo";
    }

    public final String superCall() {
      return super.value();
    }
  }

  static class JsArray<T> extends JavaScriptObject {
    public static native <T> JsArray<T> create() /*-{
      return [];
    }-*/;

    protected JsArray() {
    }

    public final native T get(int index) /*-{
      return this[index];
    }-*/;

    public final native int length() /*-{
      return this.length;
    }-*/;

    public final native void put(int index, T value) /*-{
      this[index] = value;
    }-*/;
  }

  static class MethodMangleClash {
    @SuppressWarnings("unused")
    public static String func(JavaScriptObject this_) {
      return "funcJavaScriptObject";
    }

    @SuppressWarnings("unused")
    public static String func(MethodMangleClash this_) {
      return "funcMethodMangleClash";
    }

    @SuppressWarnings("unused")
    public String func() {
      return "func";
    }
  }

  static class Overloads {
    private static volatile boolean FALSE = false;

    @SuppressWarnings("unused")
    static String sFunc(Bar b) {
      return "sFunc Bar";
    }

    @SuppressWarnings("unused")
    static String sFunc(Bar[][] b) {
      return "sFunc Bar[][]";
    }

    @SuppressWarnings("unused")
    static String sFunc(Foo f) {
      return "sFunc Foo";
    }

    @SuppressWarnings("unused")
    static String sFunc(Foo[][] f) {
      return "sFunc Foo[][]";
    }

    @SuppressWarnings("unused")
    public Overloads(Bar b) {
    }

    @SuppressWarnings("unused")
    public Overloads(Bar[][] b) {
    }

    @SuppressWarnings("unused")
    public Overloads(Foo f) {
    }

    @SuppressWarnings("unused")
    public Overloads(Foo[][] f) {
    }

    @SuppressWarnings("unused")
    String func(Bar b) {
      if (FALSE) {
        // prevent inlining
        return func(b);
      }
      return "func Bar";
    }

    @SuppressWarnings("unused")
    String func(Bar[][] b) {
      if (FALSE) {
        // prevent inlining
        return func(b);
      }
      return "func Bar[][]";
    }

    @SuppressWarnings("unused")
    String func(Foo f) {
      if (FALSE) {
        // prevent inlining
        return func(f);
      }
      return "func Foo";
    }

    @SuppressWarnings("unused")
    String func(Foo[][] f) {
      if (FALSE) {
        // prevent inlining
        return func(f);
      }
      return "func Foo[][]";
    }
  }

  private static native Bar makeBar() /*-{
    return {
      toString:function() {
        return "bar";
      },
      bar: "this is bar",
    };
  }-*/;

  private static native Foo makeFoo() /*-{
    return {
      toString:function() {
        return "foo";
      },
      foo: "this is foo",
    };
  }-*/;

  private static native JavaScriptObject makeJSO() /*-{
    return {
      toString:function() {
        return "jso";
      },
      foo: "jso foo",
      bar: "jso bar",
    };
  }-*/;

  private static native JavaScriptObject makeMixedArray() /*-{
    return [
      @com.google.gwt.dev.jjs.test.JsoTest::makeJSO()(),
      "foo",
      @com.google.gwt.dev.jjs.test.JsoTest::makeObject()(),
      null
    ];
  }-*/;

  private static Object makeObject() {
    return new Object() {
      @Override
      public String toString() {
        return "myObject";
      }
    };
  }

  private static native JavaScriptObject returnMe(JavaScriptObject jso) /*-{
    return jso;
  }-*/;

  @Override
  public String getModuleName() {
    return "com.google.gwt.dev.jjs.CompilerSuite";
  }

  public void testArrayInit() {
    Object[] array = {makeJSO(), new Object(), ""};
    assertTrue(array[0] instanceof JavaScriptObject);
    assertFalse(array[1] instanceof JavaScriptObject);
    assertFalse(array[2] instanceof JavaScriptObject);
  }

  public void testArrayStore() {
    JavaScriptObject[] jsoArray = new JavaScriptObject[1];
    jsoArray[0] = makeJSO();
    jsoArray[0] = makeFoo();
    jsoArray[0] = makeBar();

    Foo[] fooArray = new Foo[1];
    fooArray[0] = (Foo) makeJSO();
    fooArray[0] = makeFoo();
    fooArray[0] = makeBar().cast();

    Bar[] barArray = new Bar[1];
    barArray[0] = (Bar) makeJSO();
    barArray[0] = makeBar();
    barArray[0] = makeFoo().cast();

    Object[] objArray = jsoArray;
    try {
      objArray[0] = new Object();
      fail("Expected ArrayStoreException");
    } catch (ArrayStoreException expected) {
    }

    objArray = new Object[1];
    objArray[0] = makeJSO();
    objArray[0] = makeFoo();
    objArray[0] = makeBar();

    Foo[][] foo = new Foo[10][];
    Object[][] fooAsObject = foo;
    fooAsObject[0] = new Bar[1];
  }

  public void testBasic() {
    JavaScriptObject jso = makeJSO();
    assertEquals("jso", jso.toString());

    Foo foo = (Foo) jso;
    assertEquals("jso", foo.toString());
    assertEquals("jso foo", foo.getFoo());
    assertEquals("Foo", foo.value());

    Bar bar = (Bar) jso;
    assertEquals("jso", bar.toString());
    assertEquals("jso bar", bar.getBar());
    assertEquals("Bar", bar.value());

    foo = makeFoo();
    assertEquals("foo", foo.toString());
    assertEquals("this is foo", foo.getFoo());
    assertEquals("Foo", foo.value());

    bar = makeBar();
    assertEquals("bar", bar.toString());
    assertEquals("this is bar", bar.getBar());
    assertEquals("Bar", bar.value());
  }

  @SuppressWarnings("cast")
  public void testCasts() {
    JavaScriptObject jso = makeJSO();
    assertTrue(jso instanceof JavaScriptObject);
    assertTrue(jso instanceof Foo);
    assertTrue(jso instanceof Bar);

    Foo foo = (Foo) jso;
    foo = makeFoo();
    assertTrue((JavaScriptObject) foo instanceof Bar);
    Bar bar = (Bar) (JavaScriptObject) makeFoo();
    bar = makeFoo().cast();

    bar = (Bar) jso;
    bar = makeBar();
    assertTrue((JavaScriptObject) bar instanceof Foo);
    foo = (Foo) (JavaScriptObject) makeBar();
    foo = makeBar().cast();

    // Implicit
    jso = foo;
    jso = bar;

    Object o = new Object();
    assertFalse(o instanceof JavaScriptObject);
    assertFalse(o instanceof Foo);
    assertFalse(o instanceof Bar);
    try {
      jso = (JavaScriptObject) o;
      fail("Expected ClassCastException");
    } catch (ClassCastException expected) {
    }

    o = "foo";
    assertFalse(o instanceof JavaScriptObject);
    assertFalse(o instanceof Foo);
    assertFalse(o instanceof Bar);
    try {
      jso = (JavaScriptObject) o;
      fail("Expected ClassCastException");
    } catch (ClassCastException expected) {
    }

    o = jso;
    assertFalse(o instanceof String);
    try {
      String s = (String) o;
      s = s.toString();
      fail("Expected ClassCastException");
    } catch (ClassCastException expected) {
    }
  }

  @SuppressWarnings("cast")
  public void testCastsArray() {
    JavaScriptObject[][] jso = new JavaScriptObject[0][0];
    assertTrue(jso instanceof JavaScriptObject[][]);
    assertTrue(jso instanceof Foo[][]);
    assertTrue(jso instanceof Bar[][]);

    Foo[][] foo = (Foo[][]) jso;
    foo = new Foo[0][0];
    assertTrue((JavaScriptObject[][]) foo instanceof Bar[][]);
    Bar[][] bar = (Bar[][]) (JavaScriptObject[][]) new Foo[0][0];

    bar = (Bar[][]) jso;
    bar = new Bar[0][0];
    assertTrue((JavaScriptObject[][]) bar instanceof Foo[][]);
    foo = (Foo[][]) (JavaScriptObject[][]) new Bar[0][0];

    Object[][] o = new Object[0][0];
    assertFalse(o instanceof JavaScriptObject[][]);
    assertFalse(o instanceof Foo[][]);
    assertFalse(o instanceof Bar[][]);
    try {
      jso = (JavaScriptObject[][]) o;
      fail("Expected ClassCastException");
    } catch (ClassCastException expected) {
    }

    o = jso;
    assertFalse(o instanceof String[][]);
    try {
      String[][] s = (String[][]) o;
      s.getClass();
      fail("Expected ClassCastException");
    } catch (ClassCastException expected) {
    }
  }

  public void testClassInitializerRun() {
    ClinitStaticMethodFirst staticMethodFirst = JavaScriptObject.createObject().cast();
    assertNotNull("static method", ClinitStaticMethodFirst.invokeStaticMethod());
    assertNotNull("instance method after static method", staticMethodFirst.invokeInstanceMethod());

    ClinitStaticFieldFirst staticFieldFirst = JavaScriptObject.createObject().cast();
    assertNotNull("field access", ClinitStaticFieldFirst.FIELD);
    assertNotNull("instance method after field access", staticFieldFirst.invokeInstanceMethod());

    ClinitInstanceMethodFirst instanceMethodFirst = JavaScriptObject.createObject().cast();
    assertNotNull("instance method", instanceMethodFirst.invokeInstanceMethod());
    assertNotNull("static method after instance method", ClinitInstanceMethodFirst.invokeStaticMethod());
  }

  public void testClassLiterals() {
    JavaScriptObject jso = makeJSO();
    Foo foo = makeFoo();
    Bar bar = makeBar();
    assertEquals(JavaScriptObject.class, jso.getClass());
    assertEquals(Foo.class, jso.getClass());
    assertEquals(Bar.class, jso.getClass());
    assertEquals(JavaScriptObject.class, foo.getClass());
    assertEquals(Foo.class, foo.getClass());
    assertEquals(Bar.class, foo.getClass());
    assertEquals(JavaScriptObject.class, bar.getClass());
    assertEquals(Foo.class, bar.getClass());
    assertEquals(Bar.class, bar.getClass());
    assertEquals(JavaScriptObject.class, Foo.class);
    assertEquals(JavaScriptObject.class, Bar.class);
    assertEquals(Foo.class, Bar.class);

    if (!JavaScriptObject.class.getName().startsWith("Class$")) {
      // Class metadata could be disabled
      assertEquals("com.google.gwt.core.client.JavaScriptObject$",
          JavaScriptObject.class.getName());
    }
  }

  public void testClassLiteralsArray() {
    JavaScriptObject[][] jso = new JavaScriptObject[0][0];
    Foo[][] foo = new Foo[0][0];
    Bar[][] bar = new Bar[0][0];
    assertEquals(JavaScriptObject[][].class, jso.getClass());
    assertEquals(Foo[][].class, jso.getClass());
    assertEquals(Bar[][].class, jso.getClass());
    assertEquals(JavaScriptObject[][].class, foo.getClass());
    assertEquals(Foo[][].class, foo.getClass());
    assertEquals(Bar[][].class, foo.getClass());
    assertEquals(JavaScriptObject[][].class, bar.getClass());
    assertEquals(Foo[][].class, bar.getClass());
    assertEquals(Bar[][].class, bar.getClass());
    assertEquals(JavaScriptObject[][].class, Foo[][].class);
    assertEquals(JavaScriptObject[][].class, Bar[][].class);
    assertEquals(Foo[][].class, Bar[][].class);

    if (!JavaScriptObject.class.getName().startsWith("Class$")) {
      // Class metadata could be disabled
      assertEquals("[[Lcom.google.gwt.core.client.JavaScriptObject$;",
          JavaScriptObject[][].class.getName());
    }
  }

  static class MyJSO extends JavaScriptObject {
    protected MyJSO() {
    }

    public final boolean equalMethod(Object o) {
      return this == o;
    }
  }

  public void testEquality() {
    JavaScriptObject jso = makeJSO();
    assertEquals(jso, jso);

    JavaScriptObject jso2 = makeJSO();
    assertFalse(jso.equals(jso2));
    assertFalse(jso2.equals(jso));

    jso2 = returnMe(jso);
    assertEquals(jso, jso2);

    MyJSO jso3 = (MyJSO) makeJSO();
    MyJSO jso4 = (MyJSO) makeJSO();
    assertTrue(jso3.equalMethod(jso3));
    assertFalse(jso3.equalMethod(jso4));
  }

  public void testGenericsJsos() {
    JsArray<JavaScriptObject> a = JsArray.create();
    a.put(0, makeJSO());
    a.put(1, makeFoo());
    a.put(2, makeBar());
    a.put(3, null);
    assertEquals(4, a.length());
    assertEquals("jso", a.get(0).toString());
    assertEquals("foo", a.get(1).toString());
    assertEquals("bar", a.get(2).toString());
    assertEquals(null, a.get(3));
  }

  public void testGenericsMixed() {
    JsArray<Object> a = JsArray.create();
    a.put(0, makeJSO());
    a.put(1, "foo");
    a.put(2, makeObject());
    a.put(3, null);
    assertEquals(4, a.length());
    assertEquals("jso", a.get(0).toString());
    assertEquals("foo", a.get(1));
    assertEquals("myObject", a.get(2).toString());
    assertEquals(null, a.get(3));
  }

  @SuppressWarnings("unchecked")
  public void testGenericsRawJson() {
    JsArray a = (JsArray) makeMixedArray();
    assertEquals(4, a.length());
    assertEquals("jso", a.get(0).toString());
    assertEquals("foo", a.get(1));
    assertEquals("myObject", a.get(2).toString());
    assertEquals(null, a.get(3));
  }

  public void testGenericsStrings() {
    JsArray<String> a = JsArray.create();
    a.put(0, "foo");
    a.put(1, "bar");
    a.put(2, "baz");
    a.put(3, null);
    assertEquals(4, a.length());
    assertEquals("foo", a.get(0));
    assertEquals("bar", a.get(1));
    assertEquals("baz", a.get(2));
    assertEquals(null, a.get(3));
  }

  public void testHashCode() {
    // TODO: make this better.
    JavaScriptObject jso = makeJSO();
    int jsoHashCode = jso.hashCode();
    Foo foo = makeFoo();
    Bar bar = makeBar();
    Object o = new Object() {
      @Override
      public int hashCode() {
        // Return something unlikely so as not to collide with the JSOs.
        return 0xDEADBEEF;
      }
    };

    assertEquals(jsoHashCode, jso.hashCode());
    assertFalse(jsoHashCode == foo.hashCode());
    assertFalse(jsoHashCode == bar.hashCode());
    assertFalse(jsoHashCode == o.hashCode());
    assertFalse(foo.hashCode() == bar.hashCode());
    assertFalse(foo.hashCode() == o.hashCode());
    assertFalse(bar.hashCode() == o.hashCode());

    o = jso;
    assertEquals(jsoHashCode, o.hashCode());

    String s = "foo";
    int stringHashCode = s.hashCode();
    o = s;
    assertEquals(stringHashCode, o.hashCode());
  }

  @SuppressWarnings("JUnitAssertSameCheck")
  public void testIdentity() {
    JavaScriptObject jso = makeJSO();
    assertSame(jso, jso);

    JavaScriptObject jso2 = makeJSO();
    assertNotSame(jso, jso2);

    jso2 = returnMe(jso);
    assertSame(jso, jso2);
  }

  public void testInheritance() {
    Foo foo = makeFoo();
    FooSub fooSub = (FooSub) foo;
    assertEquals("Foo", fooSub.value());
    assertEquals("Still Foo", fooSub.anotherValue());
    assertEquals("Foo", fooSub.superCall());
  }

  public void testMethodMangleClash() {
    assertEquals("funcJavaScriptObject",
        MethodMangleClash.func((JavaScriptObject) null));
    assertEquals("funcMethodMangleClash",
        MethodMangleClash.func((MethodMangleClash) null));
    assertEquals("func", new MethodMangleClash().func());
  }

  public void testOverloads() {
    Foo foo = makeFoo();
    assertEquals("func Foo", new Overloads(foo).func(foo));
    assertEquals("sFunc Foo", Overloads.sFunc(foo));

    Bar bar = makeBar();
    assertEquals("func Bar", new Overloads(bar).func(bar));
    assertEquals("sFunc Bar", Overloads.sFunc(bar));
  }

  public void testOverloadsArray() {
    Foo[][] foo = new Foo[0][0];
    assertEquals("func Foo[][]", new Overloads(foo).func(foo));
    assertEquals("sFunc Foo[][]", Overloads.sFunc(foo));

    Bar[][] bar = new Bar[0][0];
    assertEquals("func Bar[][]", new Overloads(bar).func(bar));
    assertEquals("sFunc Bar[][]", Overloads.sFunc(bar));
  }

  public native void testOverloadsArrayNative() /*-{
    var o = @com.google.gwt.dev.jjs.test.JsoTest.Overloads::new([[Lcom/google/gwt/dev/jjs/test/JsoTest$Foo;)(null);
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("func Foo[][]", o.@com.google.gwt.dev.jjs.test.JsoTest.Overloads::func([[Lcom/google/gwt/dev/jjs/test/JsoTest$Foo;)(null));
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("sFunc Foo[][]", @com.google.gwt.dev.jjs.test.JsoTest.Overloads::sFunc([[Lcom/google/gwt/dev/jjs/test/JsoTest$Foo;)(null));

    var o = @com.google.gwt.dev.jjs.test.JsoTest.Overloads::new([[Lcom/google/gwt/dev/jjs/test/JsoTest$Bar;)(null);
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("func Bar[][]", o.@com.google.gwt.dev.jjs.test.JsoTest.Overloads::func([[Lcom/google/gwt/dev/jjs/test/JsoTest$Bar;)(null));
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("sFunc Bar[][]", @com.google.gwt.dev.jjs.test.JsoTest.Overloads::sFunc([[Lcom/google/gwt/dev/jjs/test/JsoTest$Bar;)(null));
  }-*/;

  public native void testOverloadsNative() /*-{
    var o = @com.google.gwt.dev.jjs.test.JsoTest.Overloads::new(Lcom/google/gwt/dev/jjs/test/JsoTest$Foo;)(null);
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("func Foo", o.@com.google.gwt.dev.jjs.test.JsoTest.Overloads::func(Lcom/google/gwt/dev/jjs/test/JsoTest$Foo;)(null));
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("sFunc Foo", @com.google.gwt.dev.jjs.test.JsoTest.Overloads::sFunc(Lcom/google/gwt/dev/jjs/test/JsoTest$Foo;)(null));

    var o = @com.google.gwt.dev.jjs.test.JsoTest.Overloads::new(Lcom/google/gwt/dev/jjs/test/JsoTest$Bar;)(null);
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("func Bar", o.@com.google.gwt.dev.jjs.test.JsoTest.Overloads::func(Lcom/google/gwt/dev/jjs/test/JsoTest$Bar;)(null));
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("sFunc Bar", @com.google.gwt.dev.jjs.test.JsoTest.Overloads::sFunc(Lcom/google/gwt/dev/jjs/test/JsoTest$Bar;)(null));
  }-*/;

  public void testStaticAccess() {
    Foo.field = 3;
    assertEquals(3, Foo.field--);
    assertEquals("Foo2", Foo.staticValue());
    assertEquals("nativeFoo", Foo.staticNative());
    assertEquals("FooSub", Foo.staticNativeToSub());

    Bar.field = 10;
    assertEquals(11, ++Bar.field);
    assertEquals("Bar11", Bar.staticValue());
    assertEquals("nativeBar", Bar.staticNative());
  }

  public native void testStaticAccessNative() /*-{
    @com.google.gwt.dev.jjs.test.JsoTest.Foo::field = 3;
    @junit.framework.Assert::assertEquals(II)(3, @com.google.gwt.dev.jjs.test.JsoTest.Foo::field--);
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("Foo2", @com.google.gwt.dev.jjs.test.JsoTest.Foo::staticValue()());
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("nativeFoo", @com.google.gwt.dev.jjs.test.JsoTest.Foo::staticNative()());

    @com.google.gwt.dev.jjs.test.JsoTest.Bar::field = 10;
    @junit.framework.Assert::assertEquals(II)(11, ++@com.google.gwt.dev.jjs.test.JsoTest.Bar::field);
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("Bar11", @com.google.gwt.dev.jjs.test.JsoTest.Bar::staticValue()());
    @junit.framework.Assert::assertEquals(Ljava/lang/Object;Ljava/lang/Object;)("nativeBar", @com.google.gwt.dev.jjs.test.JsoTest.Bar::staticNative()());
  }-*/;

  public void testStaticAccessSubclass() {
    FooSub.field = 3;
    assertEquals(3, FooSub.field--);
    assertEquals("Foo2", FooSub.staticValue());
    assertEquals("nativeFoo", FooSub.staticNative());
  }

  static JavaScriptObject someObject;
  static class JSO1 extends JavaScriptObject {
    protected JSO1() {
    }
    static native JSO1 create() /*-{
      @com.google.gwt.dev.jjs.test.JsoTest::someObject =
          @com.google.gwt.dev.jjs.test.JsoTest::someObject || [];
      return @com.google.gwt.dev.jjs.test.JsoTest::someObject;
    }-*/;
  }

  static class JSO2 extends JavaScriptObject {
    protected JSO2() {
    }
    static native JSO2 create() /*-{
      @com.google.gwt.dev.jjs.test.JsoTest::someObject =
          @com.google.gwt.dev.jjs.test.JsoTest::someObject || [];
      return @com.google.gwt.dev.jjs.test.JsoTest::someObject;
    }-*/;
  }

  /**
   * Some final JSOs classes to make sure exactness (inferred from final) does not affect
   * JSO cast semantics.
   */
  static final class FinalJSO1 extends JavaScriptObject {
    protected FinalJSO1() {
    }
    static native FinalJSO1 create() /*-{
      @com.google.gwt.dev.jjs.test.JsoTest::someObject =
          @com.google.gwt.dev.jjs.test.JsoTest::someObject || [];
      return @com.google.gwt.dev.jjs.test.JsoTest::someObject;
    }-*/;
  }

  static final class FinalJSO2 extends JavaScriptObject {
    protected FinalJSO2() {
    }
    static native FinalJSO2 create() /*-{
      @com.google.gwt.dev.jjs.test.JsoTest::someObject =
          @com.google.gwt.dev.jjs.test.JsoTest::someObject || [];
      return @com.google.gwt.dev.jjs.test.JsoTest::someObject;
    }-*/;
  }

  interface I {
  }

  /**
   * A JSO class that is the sole implementor of an interface to make sure that the logic in
   * {@link TypeTightener#getSingleConcreteType()} does not create an EXACT JSO type.
   */
  static class JSOImplementingI extends JavaScriptObject implements I {
    protected JSOImplementingI() {
    }
    static native I create() /*-{
      @com.google.gwt.dev.jjs.test.JsoTest::someObject =
          @com.google.gwt.dev.jjs.test.JsoTest::someObject || [];
      return @com.google.gwt.dev.jjs.test.JsoTest::someObject;
    }-*/;
  }

  /**
   * Tests various crosscasting scenarios to make sure that casts are erased and not transformed
   * into throws.
   */
  public void testCrossCasting() {
    JSO1 jso1 = JSO1.create();
    JSO2 jso2 = JSO2.create();
    FinalJSO1 finalJso1 = FinalJSO1.create();
    FinalJSO2 finalJso2 = FinalJSO2.create();
    I i = JSOImplementingI.create();

    assertTrue(jso1 == (JSO1) (trueFn() ? jso2 : (JavaScriptObject) null));
    assertTrue(jso1 == (JSO1) (trueFn() ? i : (JavaScriptObject) null));
    assertTrue(jso1 == (JSO1) (trueFn() ? finalJso1 : (JavaScriptObject) null));
    assertTrue(jso1 == (JSO1) (trueFn() ? finalJso2 : (JavaScriptObject) null));

    assertTrue(jso2 == (JSO2) (trueFn() ? jso1 : (JavaScriptObject) null));
    assertTrue(jso2 == (JSO2) (trueFn() ? i : (JavaScriptObject) null));
    assertTrue(jso2 == (JSO2) (trueFn() ? finalJso1 : (JavaScriptObject) null));
    assertTrue(jso2 == (JSO2) (trueFn() ? finalJso2 : (JavaScriptObject) null));

    assertTrue(finalJso1 == (FinalJSO1) (trueFn() ? jso1 : (JavaScriptObject) null));
    assertTrue(finalJso1 == (FinalJSO1) (trueFn() ? i : (JavaScriptObject) null));
    assertTrue(finalJso1 == (FinalJSO1) (trueFn() ? jso2 : (JavaScriptObject) null));
    assertTrue(finalJso1 == (FinalJSO1) (trueFn() ? finalJso2 : (JavaScriptObject) null));

    assertTrue(finalJso2 == (FinalJSO2) (trueFn() ? jso1 : (JavaScriptObject) null));
    assertTrue(finalJso2 == (FinalJSO2) (trueFn() ? i : (JavaScriptObject) null));
    assertTrue(finalJso2 == (FinalJSO2) (trueFn() ? jso2 : (JavaScriptObject) null));
    assertTrue(finalJso2 == (FinalJSO2) (trueFn() ? finalJso1 : (JavaScriptObject) null));

    assertTrue(i == (I) (trueFn() ? jso1 : (JavaScriptObject) null));
    assertTrue(i == (I) (trueFn() ? jso2 : (JavaScriptObject) null));
    assertTrue(i == (I) (trueFn() ? finalJso1 : (JavaScriptObject) null));
    assertTrue(i == (I) (trueFn() ? finalJso2 : (JavaScriptObject) null));
  }

  /**
   *
   * Test that nullability optimizations don't break the assumption that null is considered a JSO.
   */
  public void testNonNullableOptimization() {
    // TODO(rluble): fix semantics to be more coherent by adding a nullness check for devirtualized
    // methods in draft mode.
    JavaScriptObject nullJso =  (trueFn() ? null : JavaScriptObject.createArray());
    // This statement should throw NPE (at least in draft mode) but it does not due to JSO
    // devirtualization. These semantics are exploited in GWT standard library. Improvemnts and
    // fixes to optimization passes might break these assumptions.
    JSO1 someJso = nullJso.cast();
    assertTrue(someJso == null);
  }

  private boolean trueFn() {
    return true;
  }
}
