/*
 * Copyright (C) 2011 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.jboss.errai.codegen.test;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.enterprise.util.TypeLiteral;
import javax.inject.Inject;

import org.jboss.errai.codegen.AssignmentOperator;
import org.jboss.errai.codegen.Cast;
import org.jboss.errai.codegen.Context;
import org.jboss.errai.codegen.Statement;
import org.jboss.errai.codegen.Variable;
import org.jboss.errai.codegen.VariableReference;
import org.jboss.errai.codegen.builder.ContextualStatementBuilder;
import org.jboss.errai.codegen.builder.impl.ObjectBuilder;
import org.jboss.errai.codegen.builder.impl.StatementBuilder;
import org.jboss.errai.codegen.exception.InvalidTypeException;
import org.jboss.errai.codegen.exception.OutOfScopeException;
import org.jboss.errai.codegen.exception.UndefinedFieldException;
import org.jboss.errai.codegen.literal.LiteralFactory;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.codegen.test.model.BeanWithTypeParmedMeths;
import org.jboss.errai.codegen.test.model.Foo;
import org.jboss.errai.codegen.test.model.TEnum;
import org.jboss.errai.codegen.util.Bitwise;
import org.jboss.errai.codegen.util.Bool;
import org.jboss.errai.codegen.util.Expr;
import org.jboss.errai.codegen.util.Refs;
import org.jboss.errai.codegen.util.Stmt;
import org.junit.Assert;
import org.junit.Test;

/**
 * Tests the {@link StatementBuilder} API.
 * 
 * @author Christian Sadilek <csadilek@redhat.com>
 */
public class StatementBuilderTest extends AbstractCodegenTest {

  @Test
  public void testDeclareVariableWithExactTypeProvided() {
    final Context ctx = Context.create();
    final String s = StatementBuilder.create().declareVariable("n", Integer.class, 10).generate(ctx);

    assertEquals("failed to generate variable declaration with type provided",
            "Integer n = 10;", s);

    final VariableReference n = ctx.getVariable("n");
    assertEquals("Wrong variable name", "n", n.getName());
    Assert.assertEquals("Wrong variable type", MetaClassFactory.get(Integer.class), n.getType());
    Assert.assertEquals("Wrong variable value", LiteralFactory.getLiteral(10), n.getValue());
  }

  @Test
  public void testDeclareVariableWithIntegerTypeInference() {
    final Context ctx = Context.create();
    final String s = StatementBuilder.create().declareVariable("n", 10).generate(ctx);

    assertEquals("failed to generate variable declaration with Integers type inference",
            "Integer n = 10;", s);

    final VariableReference n = ctx.getVariable("n");
    assertEquals("Wrong variable name", "n", n.getName());
    Assert.assertEquals("Wrong variable type", MetaClassFactory.get(Integer.class), n.getType());
    Assert.assertEquals("Wrong variable value", LiteralFactory.getLiteral(10), n.getValue());
  }

  @Test
  public void testDeclareVariableWithStringTypeInference() {
    final Context ctx = Context.create();
    final String s = StatementBuilder.create().declareVariable("n", "10").generate(ctx);

    assertEquals("failed to generate variable declaration with =String type inference",
            "String n = \"10\";", s);

    final VariableReference n = ctx.getVariable("n");
    assertEquals("Wrong variable name", "n", n.getName());
    Assert.assertEquals("Wrong variable type", MetaClassFactory.get(String.class), n.getType());
    Assert.assertEquals("Wrong variable value", LiteralFactory.getLiteral("10"), n.getValue());
  }

  @Test
  public void testDeclareVariableWithImplicitTypeConversion() {
    final Context ctx = Context.create();
    final String s = StatementBuilder.create().declareVariable("n", Integer.class, "10").generate(ctx);

    assertEquals("failed to generate variable declaration with implicit type conversion",
            "Integer n = 10;", s);

    final VariableReference n = ctx.getVariable("n");
    assertEquals("Wrong variable name", "n", n.getName());
    Assert.assertEquals("Wrong variable type", MetaClassFactory.get(Integer.class), n.getType());
    Assert.assertEquals("Wrong variable value", LiteralFactory.getLiteral(10), n.getValue());

    try {
      StatementBuilder.create().declareVariable("n", Integer.class, "abc").toJavaString();
      fail("Expected InvalidTypeException");
    }
    catch (InvalidTypeException ive) {
      // expected
      assertTrue(ive.getCause() instanceof NumberFormatException);
    }
  }

  @Test
  public void testDeclareVariableWithObjectInitializationWithExactTypeProvided() {
    final Context ctx = Context.create();
    final String s = StatementBuilder.create().declareVariable("str", String.class,
            ObjectBuilder.newInstanceOf(String.class)).generate(ctx);

    assertEquals("failed to generate variable declaration with object initialization and type provided",
            "String str = new String();", s);

    final VariableReference str = ctx.getVariable("str");
    assertEquals("Wrong variable name", "str", str.getName());
    Assert.assertEquals("Wrong variable type", MetaClassFactory.get(String.class), str.getType());
  }

  @Test
  public void testDeclareVariableWithObjectInitializationWithStringTypeInference() {
    final Context ctx = Context.create();
    final String s = StatementBuilder.create(ctx)
            .declareVariable("str", ObjectBuilder.newInstanceOf(String.class)).toJavaString();

    assertEquals("failed to generate variable declaration with object initialization and string type inference",
            "String str = new String();", s);

    final VariableReference str = ctx.getVariable("str");
    assertEquals("Wrong variable name", "str", str.getName());
    Assert.assertEquals("Wrong variable type", MetaClassFactory.get(String.class), str.getType());
  }

  @Test
  public void testDeclareVariableWithStatementInitialization() {
    final Context ctx = Context.create();
    final String s = Stmt.declareVariable("str", String.class,
            Stmt.nestedCall(Stmt.newObject(Integer.class).withParameters(2)).invoke("toString"))
            .generate(ctx);

    assertEquals("failed to generate variable declaration with statement initialization",
            "String str = new Integer(2).toString();", s);

    final VariableReference str = ctx.getVariable("str");
    assertEquals("Wrong variable name", "str", str.getName());
    Assert.assertEquals("Wrong variable type", MetaClassFactory.get(String.class), str.getType());
  }

  @Test
  public void testDeclareFinalVariable() {
    final Context ctx = Context.create();
    final String s = StatementBuilder.create(ctx)
            .declareVariable(String.class).asFinal().named("str").initializeWith("10").toJavaString();

    assertEquals("failed to generate final variable declaration", "final String str = \"10\";", s);

    final VariableReference str = ctx.getVariable("str");
    assertEquals("Wrong variable name", "str", str.getName());
    Assert.assertTrue("Variable should be final", ctx.getVariables().get("str").isFinal());
    Assert.assertEquals("Wrong variable type", MetaClassFactory.get(String.class), str.getType());
  }

  @Test
  public void testLoadUndefinedVariable() {
    try {
      StatementBuilder.create().loadVariable("n").toJavaString();
      fail("Expected OutOfScopeException");
    }
    catch (OutOfScopeException e) {
      // expected
    }
  }

  @Test
  public void testCreateAndInitializeArray() {
    final String s = StatementBuilder.create().newArray(String.class).initialize("1", "2").toJavaString();
    assertEquals("Failed to generate 1-dimensional String array", "new String[] { \"1\", \"2\" }", s);
  }

  @Test
  public void testCreateAndInitializeArrayWithInvalidInitialization() {
    try {
      StatementBuilder.create().newArray(Annotation.class)
              .initialize("1", "2")
              .toJavaString();
      fail("Expected InvalidTypeException");
    }
    catch (InvalidTypeException oose) {
      // expected
    }
  }

  @Test
  public void testCreateAndInitializeArrayWithMissingInitializationAndDimensions() {
    try {
      StatementBuilder.create().newArray(String.class).toJavaString();
      fail("Expected RuntimeException");
    }
    catch (Exception e) {
      // expected
      assertEquals("Wrong exception details",
              "Must provide either dimension expressions or an array initializer", e.getMessage());
    }
  }

  @Test
  public void testCreateAndInitializeAnnotationArray() {
    final Statement annotation1 = ObjectBuilder.newInstanceOf(Annotation.class)
            .extend()
            .publicOverridesMethod("annotationType")
            .append(StatementBuilder.create().load(Inject.class).returnValue())
            .finish()
            .finish();

    final Statement annotation2 = ObjectBuilder.newInstanceOf(Annotation.class)
            .extend()
            .publicOverridesMethod("annotationType")
            .append(StatementBuilder.create().load(PostConstruct.class).returnValue())
            .finish()
            .finish();

    final String s = StatementBuilder.create().newArray(Annotation.class)
            .initialize(annotation1, annotation2)
            .toJavaString();

    assertEquals("failed to generate Annotation array",
            "new java.lang.annotation.Annotation[] { " +
                    "new java.lang.annotation.Annotation() {\n" +
                    " public Class annotationType() {\n" +
                    "   return javax.inject.Inject.class;\n" +
                    " }\n" +
                    "}" +
                    ", new java.lang.annotation.Annotation() {\n" +
                    "   public Class annotationType() {\n" +
                    "     return javax.annotation.PostConstruct.class;\n" +
                    "   }\n" +
                    " }\n" +
                    "}", s);
  }

  @Test
  @SuppressWarnings(value = { "all" })
  public void testCreateAndInitializeTwoDimensionalArray() {
    final String s = StatementBuilder.create().newArray(Integer.class)
            .initialize(new Integer[][] { { 1, 2 }, { 3, 4 } })
            .toJavaString();

    assertEquals("Failed to generate two dimensional array", "new Integer[][] { { 1, 2 }, { 3, 4 } }", s);
  }

  @Test
  @SuppressWarnings(value = { "all" })
  public void testCreateAndInitializeTwoDimensionalArrayWithSingleValue() {
    final String s = StatementBuilder.create().newArray(Integer.class)
            .initialize(new Object[][] { { 1, 2 } })
            .toJavaString();

    assertEquals("Failed to generate two dimensional array", "new Integer[][] { { 1, 2 } }", s);
  }

  @Test
  @SuppressWarnings(value = { "all" })
  public void testCreateAndInitializeTwoDimensionalObjectArrayWithIntegers() {
    final String s = StatementBuilder.create().newArray(Object.class)
            .initialize(new Object[][] { { 1, 2 } })
            .toJavaString();

    assertEquals("Failed to generate two dimensional array", "new Object[][] { { 1, 2 } }", s);
  }

  @Test
  @SuppressWarnings(value = { "all" })
  public void testCreateAndInitializeTwoDimensionalArrayWithStatements() {
    final String s = StatementBuilder.create().newArray(String.class)
            .initialize(new Statement[][] {
                    { StatementBuilder.create().invokeStatic(Integer.class, "toString", 1),
                            StatementBuilder.create().invokeStatic(Integer.class, "toString", 2) },
                    { StatementBuilder.create().invokeStatic(Integer.class, "toString", 3),
                            StatementBuilder.create().invokeStatic(Integer.class, "toString", 4) } })
            .toJavaString();

    assertEquals("Failed to generate two dimensional array using statements",
            "new String[][] { { Integer.toString(1), Integer.toString(2) }, " +
                    "{ Integer.toString(3), Integer.toString(4) } }", s);
  }

  @Test
  @SuppressWarnings(value = { "all" })
  public void testCreateAndInitializeTwoDimensionalArrayWithStatementsAndLiterals() {
    final String s = StatementBuilder.create().newArray(String.class)
            .initialize(new Object[][] {
                    { StatementBuilder.create().invokeStatic(Integer.class, "toString", 1), "2" },
                    { StatementBuilder.create().invokeStatic(Integer.class, "toString", 3), "4" } })
            .toJavaString();

    assertEquals("Failed to generate two dimensional array using statements and objects",
            "new String[][] { { Integer.toString(1), \"2\" }," +
                    " { Integer.toString(3), \"4\" } }", s);
  }

  @Test
  @SuppressWarnings(value = { "all" })
  public void testCreateAndInitializeThreeDimensionalArray() {
    final String s = StatementBuilder.create().newArray(String.class)
            .initialize(new String[][][] { { { "1", "2" }, { "a", "b" } }, { { "3", "4" }, { "b", "c" } } })
            .toJavaString();

    assertEquals("Failed to generate three dimensional array",
            "new String[][][] { { { \"1\", \"2\" }, { \"a\", \"b\" } }, { { \"3\", \"4\" }, { \"b\", \"c\" } } }", s);
  }

  @Test
  public void testAssignArrayValue() {
    final String s = StatementBuilder.create()
            .declareVariable("twoDimArray", String[][].class)
            .loadVariable("twoDimArray", 1, 2)
            .assignValue("test")
            .toJavaString();

    assertEquals("Failed to generate array assignment", "twoDimArray[1][2] = \"test\";", s);
  }

  @Test
  public void testAssignArrayValueWithPreIncrementAssignment() {
    final String s = StatementBuilder.create()
            .declareVariable("twoDimArray", String[][].class)
            .loadVariable("twoDimArray", 1, 2)
            .assignValue(AssignmentOperator.PreIncrementAssign, "test")
            .toJavaString();

    assertEquals("Failed to generate array assignment", "twoDimArray[1][2] += \"test\";", s);
  }

  @Test
  public void testAssignArrayValueWithVariableIndexes() {
    final String s = StatementBuilder.create()
            .declareVariable("twoDimArray", String[][].class)
            .declareVariable("i", int.class)
            .declareVariable("j", int.class)
            .loadVariable("twoDimArray", Variable.get("i"), Variable.get("j"))
            .assignValue("test")
            .toJavaString();

    assertEquals("Failed to generate array assignment", "twoDimArray[i][j] = \"test\";", s);
  }

  @Test
  public void testAssignArrayValueWithInvalidArray() {
    try {
      StatementBuilder.create()
         .declareVariable("twoDimArray", String.class)
         .loadVariable("twoDimArray", 1, 2)
         .assignValue("test")
         .toJavaString();

      fail("Expected InvalidTypeExcpetion");
    }
    catch (InvalidTypeException e) {
      // Expected, variable is not an array.
    }
  }

  @Test
  public void testAssignArrayValueWithInvalidIndexType() {
    try {
      StatementBuilder.create()
              .declareVariable("twoDimArray", String[][].class)
              .declareVariable("i", float.class)
              .declareVariable("j", float.class)
              .loadVariable("twoDimArray", Variable.get("i"), Variable.get("j"))
              .assignValue("test")
              .toJavaString();
      fail("Expected InvalidTypeExcpetion");
    }
    catch (InvalidTypeException e) {
      // Expected, indexes are no integers
    }
  }

  @Test
  public void testObjectCreationWithLiteralParameter() {
    final String s = StatementBuilder.create().newObject(String.class).withParameters("original").toJavaString();
    assertEquals("failed to generate new object with parameters", "new String(\"original\")", s);
  }

  @Test
  public void testObjectCreationWithVariableParameter() {
    final String s = StatementBuilder.create()
            .declareVariable("original", String.class)
            .newObject(String.class).withParameters(Variable.get("original")).toJavaString();
    assertEquals("failed to generate new object with parameters", "new String(original)", s);
  }

  @Test
  public void testObjectCreationWithParameterizedType() {
    final String s = StatementBuilder.create().newObject(new TypeLiteral<ArrayList<String>>() {
        }).toJavaString();
    assertEquals("failed to generate new object with parameterized type", "new java.util.ArrayList<String>()", s);
  }

  @Test
  public void testObjectCreationWithAutoImportedParameterizedType() {
    final Context c = Context.create().autoImport();
    final String s = StatementBuilder.create(c).newObject(new TypeLiteral<ArrayList<Date>>() {
        }).toJavaString();
    assertEquals("failed to generate new object with parameterized type", "new ArrayList<Date>()", s);
  }

  @Test
  public void testObjectCreationWithParameterizedTypeAndClassImport() {
    final Context c = Context.create().addImport(MetaClassFactory.get(ArrayList.class));
    final String s = StatementBuilder.create(c).newObject(new TypeLiteral<ArrayList<String>>() {
        }).toJavaString();
    assertEquals("failed to generate new object with parameterized type", "new ArrayList<String>()", s);
  }

  @Test
  public void testObjectCreationWithFullyQualifiedParameterizedTypeAndClassImport() {
    final Context c = Context.create().addImport(MetaClassFactory.get(ArrayList.class));
    final String s = StatementBuilder.create(c).newObject(new TypeLiteral<ArrayList<Date>>() {
        }).toJavaString();
    assertEquals("failed to generate new object with parameterized type", "new ArrayList<java.util.Date>()", s);
  }

  @Test
  public void testObjectCreationWithNestedParameterizedTypeAndClassImports() {
    final Context c = Context.create()
            .addImport(MetaClassFactory.get(ArrayList.class))
            .addImport(MetaClassFactory.get(HashMap.class));

    final String s = StatementBuilder.create(c)
            .newObject(new TypeLiteral<ArrayList<ArrayList<HashMap<String, Integer>>>>() {
                }).toJavaString();
    assertEquals("failed to generate new object with parameterized type",
        "new ArrayList<ArrayList<HashMap<String, Integer>>>()", s);
  }
  
  @Test
  public void testObjectCreationOfUninstantiableType() {
    try {
      Stmt.newObject(List.class).toJavaString();
      fail("Expected InvalidTypeExcpetion");
    }
    catch (InvalidTypeException e) {
      // Expected, List is not instantiable
    }
  }

  @Test
  public void testThrowExceptionUsingNewInstance() {
    final Context c = Context.create().autoImport();
    final String s = StatementBuilder.create(c).throw_(InvalidTypeException.class).toJavaString();
    assertEquals("failed to generate throw statement using a new instance",
            "throw new InvalidTypeException()", s);
  }

  @Test
  public void testThrowExceptionUsingNewInstanceWithParameters() {
    final Context c = Context.create().autoImport();
    final String s = StatementBuilder.create(c).throw_(InvalidTypeException.class, "message").toJavaString();
    assertEquals("failed to generate throw statement using a new instance",
            "throw new InvalidTypeException(\"message\")", s);
  }

  @Test
  public void testThrowExceptionUsingVariable() {
    final String s = StatementBuilder.create().declareVariable("t", Throwable.class).throw_("t").toJavaString();
    assertEquals("failed to generate throw statement using a variable", "throw t", s);
  }

  @Test
  public void testThrowExceptionUsingInvalidVariable() {
    try {
      StatementBuilder.create()
              .declareVariable("t", Integer.class)
              .throw_("t")
              .toJavaString();
      fail("expected InvalidTypeException");
    }
    catch (InvalidTypeException e) {
      // expected
    }
  }

  @Test
  public void testThrowExceptionUsingUndefinedVariable() {
    try {
      StatementBuilder.create()
              .throw_("t")
              .toJavaString();
      fail("expected OutOfScopeException");
    }
    catch (OutOfScopeException e) {
      // expected
    }
  }

  @Test
  public void testNestedCall() {
    final String s = StatementBuilder.create()
            .nestedCall(
                    StatementBuilder.create().declareVariable("n", Integer.class).loadVariable("n").invoke("toString"))
            .invoke("getBytes")
            .toJavaString();

    assertEquals("failed to generate nested call", "n.toString().getBytes()", s);
  }

  @Test
  public void testAssignField() {
    final String s = Stmt.create(Context.create().autoImport()).nestedCall(
            Stmt.newObject(Foo.class)).loadField("bar").loadField("name").assignValue("test").toJavaString();

    assertEquals("failed to generate nested field assignment",
            "new Foo().bar.name = \"test\";", s);
  }

  @Test
  public void testAssignInvalidField() {
    try {
      final String s = Stmt.create(Context.create().autoImport()).nestedCall(
              Stmt.newObject(Foo.class))
              .loadField("invalid")
              .assignValue("test")
              .toJavaString();

      fail("expected UndefinedFieldException");
    }
    catch (UndefinedFieldException e) {
      // expected
    }
  }

  @Test
  public void testCastDown() {
    final Statement stmt = Cast.to(String.class, Stmt.declareVariable("obj", Object.class).loadVariable("obj"));
    assertEquals("failed to generate cast", "(String) obj", stmt.generate(Context.create()));
  }

  @Test
  public void testCastUp() {
    final Statement stmt = Cast.to(Object.class, Stmt.declareVariable("str", String.class).loadVariable("str"));
    assertEquals("created a redundant cast", "str", stmt.generate(Context.create()));
  }


  @Test
  public void testCastWithVariableGetAPI() {
    final Context ctx = Context.create();
    ctx.addVariable(Variable.create("str", String.class));

    final Statement stmt = Cast.to(Object.class, Variable.get("str"));
    assertEquals("created a redundant cast", "str", stmt.generate(ctx));
  }


  @Test
  public void testInvalidCast() {
    try {
      final Statement stmt = Cast.to(Integer.class, Stmt.declareVariable("str", String.class).loadVariable("str"));
      stmt.generate(Context.create());
      fail("expected InvalidTypeException");
    }
    catch (InvalidTypeException e) {
      // expected
      assertEquals("Wrong exception message", "java.lang.String cannot be cast to java.lang.Integer", e.getMessage());
    }
  }

  @Test
  public void testReturnVoid() {
    final Context ctx = Context.create();
    ctx.addVariable(Variable.create("foo", Object.class));

    final Statement stmt = Stmt.if_(Bool.isNull(Refs.get("foo")))
            .append(Stmt.returnVoid()).finish();

    assertEquals("failed to generate return statement", "if (foo == null) {\n" +
            "  return;\n" +
            "}", stmt.generate(ctx));
  }

  @Test
  public void testTypeInferenceWorksPropertyForParameterizedMethodTypes() {
    final String s =
        Stmt.loadStatic(BeanWithTypeParmedMeths.class, "INSTANCE")
            .invoke("setFooBarMap", Stmt.loadStatic(BeanWithTypeParmedMeths.class, "INSTANCE").invoke("getFooBarMap"))
            .toJavaString();
    
    assertEquals("org.jboss.errai.codegen.test.model.BeanWithTypeParmedMeths.INSTANCE" +
            ".setFooBarMap(org.jboss.errai.codegen.test.model.BeanWithTypeParmedMeths.INSTANCE.getFooBarMap())",
            s);
  }

  @Test
  public void testBitwiseOrExpression() {
    final String generate = Bitwise.or(Stmt.load(1), Stmt.load(2), Stmt.load(3)).generate(Context.create());

    assertEquals("1 | 2 | 3", generate);
  }

  @Test
  public void testBitwiseAndExpression() {
    final String generate = Bitwise.and(Stmt.load(1), Stmt.load(2), Stmt.load(3)).generate(Context.create());

    assertEquals("1 & 2 & 3", generate);
  }

  @Test
  public void testMixedBitwise() {
    final String generate = Bitwise.or(Stmt.load(1), Stmt.load(2),
            Expr.qualify(Bitwise.and(Stmt.load(10), Stmt.load(20)))).generate(Context.create());

    assertEquals("1 | 2 | (10 & 20)", generate);
  }

  @Test
  public void testPassBitwiseToMethodParameter() {
    final Statement bitwiseStatement = Bitwise.or(Stmt.load(1), Stmt.load(2),
            Expr.qualify(Bitwise.and(Stmt.load(10), Stmt.load(20))));

    final String generate = Stmt.newObject(Integer.class).withParameters(bitwiseStatement).generate(Context.create());

    assertEquals("new Integer(1 | 2 | (10 & 20))", generate);
  }

  @Test
  public void testEnumReference() {
    final ContextualStatementBuilder statementBuilder = Stmt.loadStatic(TEnum.class, "FIRST");

    assertEquals(TEnum.class.getName() + ".FIRST", statementBuilder.generate(Context.create()));
  }
}
