/*
 * 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.devtools.j2objc.translate;

import com.google.devtools.j2objc.GenerationTest;

import java.io.IOException;

/**
 * Tests for {@link NilCheckResolver}.
 *
 * @author Keith Stanger
 */
public class NilCheckResolverTest extends GenerationTest {

  public void testNilCheckArrayLength() throws IOException {
    String translation = translateSourceFile(
      "public class A { int length(char[] s) { return s.length; } void test() { length(null);} }",
      "A", "A.m");
    assertTranslation(translation, "return ((IOSCharArray *) nil_chk(s))->size_;");
  }

  public void testNilCheckOnCastExpression() throws IOException {
    String translation = translateSourceFile(
        "class Test { int i; void test(Object o) { int i = ((Test) o).i; } }", "Test", "Test.m");
    assertTranslation(translation,
        "((Test *) nil_chk(((Test *) cast_chk(o, [Test class]))))->i_");
  }

  public void testNoNilCheckOnSecondDereference() throws IOException {
    String translation = translateSourceFile(
        "class Test { void test(Object o) { o.toString(); o.toString(); } }", "Test", "Test.m");
    assertTranslatedLines(translation,
        "[nil_chk(o) description];",
        "[o description];");
  }

  public void testNilCheckAfterReassignment() throws IOException {
    String translation = translateSourceFile(
        "class Test { void test(Object o, boolean b) { o.toString(); if (b) { o = null; } "
        + "o.toString(); o = new Object(); o.toString(); } }", "Test", "Test.m");
    assertTranslatedLines(translation,
        "nil_chk(o) description];",
        "if (b) {",
        "  o = nil;",
        "}",
        "[nil_chk(o) description];",
        "o = create_NSObject_init();",
        "[o description];");
  }

  public void testNilCheckAfterIfStatement() throws IOException {
    String translation = translateSourceFile(
        "class Test { void test(Object o, boolean b) { if (b) { o.toString(); } o.toString(); } }",
        "Test", "Test.m");
    assertTranslatedLines(translation,
        "if (b) {",
        "[nil_chk(o) description];",
        "}",
        "[nil_chk(o) description];");
  }

  public void testEqualsNullTest() throws IOException {
    String translation = translateSourceFile(
        "class Test { "
        + "void test1(Object o1) { if (o1 == null) { o1.toString(); } else { o1.toString(); } }"
        + "void test2(Object o2) { if (o2 != null) { o2.toString(); } else { o2.toString(); } }"
        + "void test3(Object o3, boolean b) { "
        + "if (o3 != null || b) { o3.toString(); } else { o3.toString(); } } }",
        "Test", "Test.m");
    assertTranslatedLines(translation,
        "if (o1 == nil) {",
        "[nil_chk(o1) description];",
        "}",
        "else {",
        "[o1 description];",
        "}");
    assertTranslatedLines(translation,
        "if (o2 != nil) {",
        "[o2 description];",
        "}",
        "else {",
        "[nil_chk(o2) description];",
        "}");
    assertTranslatedLines(translation,
        "if (o3 != nil || b) {",
        "[nil_chk(o3) description];",
        "}",
        "else {",
        "[nil_chk(o3) description];",
        "}");
  }

  public void testNilCheckAfterWhileLoop() throws IOException {
    String translation = translateSourceFile(
        "class Test { void test(Object o, boolean b) { "
        + "while (b) { o.toString(); } o.toString(); } }",
        "Test", "Test.m");
    assertTranslatedLines(translation,
        "while (b) {",
        "[nil_chk(o) description];",
        "}",
        "[nil_chk(o) description];");
  }

  public void testNilCheckAfterConditionalInfix() throws IOException {
    String translation = translateSourceFile(
        "abstract class Test { abstract boolean foo(); void test(Test t1, Test t2, boolean b) { "
        + "boolean b1 = b && t1.foo(); t1.foo(); boolean b2 = b || t2.foo(); t2.foo(); } }",
        "Test", "Test.m");
    assertTranslatedLines(translation,
        "jboolean b1 = b && [((Test *) nil_chk(t1)) foo];",
        "[((Test *) nil_chk(t1)) foo];",
        "jboolean b2 = b || [((Test *) nil_chk(t2)) foo];",
        "[((Test *) nil_chk(t2)) foo];");
  }

  public void testNilCheckEnhancedForExpression() throws IOException {
    String translation = translateSourceFile(
        "public class Test {"
        + "  boolean test(String target, java.util.List<String> strings) {"
        + "    for (String s : strings) {"
        + "      if (s.equals(target)) return true;"
        + "    }"
        + "    return false;"
        + "  }}",
        "Test", "Test.m");
    assertTranslation(translation, "nil_chk(strings)");
  }

  // Method invocations can have side-effects
  public void testFieldsNeedCheckAfterInvocation() throws IOException {
    String translation = translateSourceFile(
        "public class Test { class Foo { int i; } Foo foo;"
        + " void test() { int i; i = foo.i; i = foo.i; System.gc(); i = foo.i; super.toString();"
        + " i = foo.i; new Test(); i = foo.i; } }",
        "Test", "Test.m");
    assertTranslatedLines(translation,
        "i = ((Test_Foo *) nil_chk(foo_))->i_;",
        "i = foo_->i_;",
        "JavaLangSystem_gc();",
        "i = ((Test_Foo *) nil_chk(foo_))->i_;",
        "[super description];",
        "i = ((Test_Foo *) nil_chk(foo_))->i_;",
        "create_Test_init();",
        "i = ((Test_Foo *) nil_chk(foo_))->i_;");
  }

  public void testReassignedVariableInLoop() throws IOException {
    String translation = translateSourceFile(
        "public abstract class Test { abstract boolean getB();"
        + " void test(Object o, Iterable<Integer> ints) {"
        + " o.toString(); do { o.toString(); o = null; } while(getB());"
        + " o.toString(); while(getB()) { o.toString(); o = null; }"
        + " o.toString(); for(; getB();) { o.toString(); o = null; }"
        + " o.toString(); for(Integer i : ints) { o.toString(); o = null; } } }", "Test", "Test.m");
    assertTranslatedLines(translation,
        "[nil_chk(o) description];",
        "do {",
        "  [nil_chk(o) description];",
        "  o = nil;",
        "}",
        "while ([self getB]);",
        "[nil_chk(o) description];",
        "while ([self getB]) {",
        "  [nil_chk(o) description];",
        "  o = nil;",
        "}",
        "[nil_chk(o) description];",
        "for (; [self getB]; ) {",
        "  [nil_chk(o) description];",
        "  o = nil;",
        "}",
        "[nil_chk(o) description];",
        "for (JavaLangInteger * __strong i in nil_chk(ints)) {",
        "  [nil_chk(o) description];",
        "  o = nil;",
        "}");
  }

  public void testBreakAndContinue() throws IOException {
    String translation = translateSourceFile(
        "public class Test { void test(Object o, boolean b, boolean b2) { o.toString();"
        + " dummy: do { o.toString(); if (b) { o = null; break; } if (b2) { o = null; continue; }"
        + " o.toString(); } while(Math.random() < 0.5); o.toString(); } }", "Test", "Test.m");
    assertTranslatedLines(translation,
        "[nil_chk(o) description];",
        "dummy: do {",
        "  [nil_chk(o) description];",  // Needs nil_chk because of continue statement.
        "  if (b) {",
        "    o = nil;",
        "    break;",
        "  }",
        "  if (b2) {",
        "    o = nil;",
        "    continue;",
        "  }",
        // No nil_chk because of reassignments of "o" are followed by control
        // flow breaking statements.
        "  [o description];",
        "}",
        "while (JavaLangMath_random() < 0.5);",
        "[nil_chk(o) description];");  // Needs nil_chk because of break statement.
  }

  public void testTryCatch() throws IOException {
    String translation = translateSourceFile(
        "public class Test { class Foo { int i; } void test(Foo f) { int i; i = f.i; try {"
        + " f = null; System.gc(); i = f.i; } catch (Throwable t) { i = f.i; }"
        + " finally { i = f.i; } i = f.i; } }", "Test", "Test.m");
    assertTranslatedLines(translation,
        "i = ((Test_Foo *) nil_chk(f))->i_;",
        "@try {",
        "  f = nil;",
        "  JavaLangSystem_gc();",
        "  i = ((Test_Foo *) nil_chk(f))->i_;",
        "}",
        "@catch (JavaLangThrowable *t) {",
        "  i = ((Test_Foo *) nil_chk(f))->i_;",
        "}",
        "@finally {",
        "  i = ((Test_Foo *) nil_chk(f))->i_;",
        "}",
        "i = f->i_;");
  }

  public void testScopeTermination() throws IOException {
    String translation = translateSourceFile(
        "public class Test { void test(Object o1, Object o2, boolean b) {"
        + " if (o1 == null) { return; } o1.toString();"
        + " if (o2 != null) System.gc(); else if (b) throw new RuntimeException(); else return;"
        + " o2.toString(); } }", "Test", "Test.m");
    assertTranslatedLines(translation,
        "if (o1 == nil) {",
        "  return;",
        "}",
        "[o1 description];",
        "if (o2 != nil) JavaLangSystem_gc();",
        "else if (b) @throw create_JavaLangRuntimeException_init();",
        "else return;",
        "[o2 description];");
  }

  public void testTrickyEqualsNullChecks() throws IOException {
    // Try to trick the translator into thinking that "o" is safe.
    String translation = translateSourceFile(
        "class Test { boolean foo(boolean b) { return true; } void test(Object o) {"
        + " if (o != null ? false : true) { o.toString(); }"
        + " if (foo(o != null)) { o.toString(); } } }", "Test", "Test.m");
    assertTranslatedLines(translation,
        "if (o != nil ? false : true) {",
        "  [nil_chk(o) description];",
        "}",
        "if ([self fooWithBoolean:o != nil]) {",
        "  [nil_chk(o) description];",
        "}");
  }

  public void testNullCheckInLoopContition() throws IOException {
    String translation = translateSourceFile(
        "abstract class Test { abstract Object getObj(); void test(Object o) {"
        + " while (o == null) { o = getObj(); } o.toString();"
        + " for (; o != null; ) { o.toString(); o = getObj(); } o.toString(); } }",
        "Test", "Test.m");
    assertTranslatedLines(translation,
        "while (o == nil) {",
        "  o = [self getObj];",
        "}",
        "[o description];",
        "for (; o != nil; ) {",
        "  [o description];",
        "  o = [self getObj];",
        "}",
        "[nil_chk(o) description];");
  }

  public void testSwitchStatement() throws IOException {
    String translation = translateSourceFile(
        "class Test { void test(Object o1, Object o2, int i) { o2.toString(); switch(i) {"
        + " case 1: o1.toString(); case 2: o1.toString(); break;"
        + " case 3: o2 = null; case 4: o2.toString(); } } }", "Test", "Test.m");
    assertTranslatedLines(translation,
        "[nil_chk(o2) description];",
        "switch (i) {",
        "  case 1:",
        "  [nil_chk(o1) description];",
        "  case 2:",
        "  [nil_chk(o1) description];",
        "  break;",
        "  case 3:",
        "  o2 = nil;",
        "  case 4:",
        "  [nil_chk(o2) description];",
        "}");
  }

  // Volatile fields should always nil_chk'ed.
  public void testVolatileField() throws IOException {
    String translation = translateSourceFile(
        "class Test { static class Foo { int i; } volatile Foo f;"
        + " void test() { int i; i = f.i; i = f.i; } }", "Test", "Test.m");
    assertTranslatedLines(translation,
        "i = ((Test_Foo *) nil_chk(JreLoadVolatileId(&f_)))->i_;",
        "i = ((Test_Foo *) nil_chk(JreLoadVolatileId(&f_)))->i_;");
  }

  public void testLabeledBlock() throws IOException {
    String translation = translateSourceFile(
        "class Test { void test(Object o, boolean b) { test_label: { o.toString();"
        + " if (b) { o = null; break test_label; } o.toString(); } o.toString(); } }",
        "Test", "Test.m");
    assertTranslatedLines(translation,
        "{",
        "  [nil_chk(o) description];",
        "  if (b) {",
        "    o = nil;",
        "    goto break_test_label;",
        "  }",
        "  [o description];",
        "}",
        "break_test_label: ;",
        "[nil_chk(o) description];");
  }

  public void testOuterExpressionOfClassInstanceCreation() throws IOException {
    String translation = translateSourceFile(
        "class Test { class Inner {} public Inner test(Test t) { return t.new Inner(); } }",
        "Test", "Test.m");
    // "t" needs a nil_chk.
    assertTranslation(translation, "return create_Test_Inner_initWithTest_(nil_chk(t));");
  }

  // Verify "throw null" throws a null pointer exception. JLS 14.18:
  //   "If [the throws expression produces] a null value, then an instance of
  //    class NullPointerException is created and thrown instead of null."
  public void testThrowNull() throws IOException {
    String translation = translateSourceFile(
        "class Test { "
        + "void test1() { throw null; }"
        + "void test2(RuntimeException e) { throw e; }"
        + "void test3() { throw new RuntimeException(); }}", "Test", "Test.m");
    assertTranslation(translation, "@throw nil_chk(nil);");
    assertTranslation(translation, "@throw nil_chk(e);");
    assertTranslation(translation, "@throw create_JavaLangRuntimeException_init();");
  }
}
