/*
 * Copyright 2017 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.i18n.rebind;

import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.core.ext.typeinfo.TypeOracleException;
import com.google.gwt.dev.javac.TypeOracleTestingUtils;
import com.google.gwt.dev.javac.testing.impl.MockJavaResource;
import com.google.gwt.dev.shell.FailErrorLogger;
import com.google.gwt.i18n.rebind.AbstractResource.ResourceList;
import com.google.gwt.i18n.server.GwtLocaleFactoryImpl;
import com.google.gwt.i18n.shared.GwtLocale;
import com.google.gwt.i18n.shared.GwtLocaleFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.rebind.StringSourceWriter;

import junit.framework.TestCase;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Tests for {@link ConstantsWithLookupImplCreator}.
 */
public class ConstantsWithLookupImplCreatorTest extends TestCase {

  private static final int TEST_PARTITION_SIZE = 3;

  private static final MockJavaResource SINGLE_ENTRY_MESSAGES = new MockJavaResource(
      "foo.SingleEntryMessage") {
    @Override
    public CharSequence getContent() {
      StringBuffer code = new StringBuffer();
      code.append("package foo;\n");
      code.append("public interface SingleEntryMessage extends foo.Lookup {\n");
      code.append(" String stringEntry();\n");
      code.append(" int intEntry();\n");
      code.append("}");
      return code;
    }
  };

  private static final MockJavaResource FOUR_ENTRY_MESSAGES = new MockJavaResource(
      "foo.FourEntryMessage") {
    @Override
    public CharSequence getContent() {
      StringBuffer code = new StringBuffer();
      code.append("package foo;\n");
      code.append("public interface FourEntryMessage extends foo.Lookup {\n");
      code.append(" String first();\n");
      code.append(" String second();\n");
      code.append(" String third();\n");
      code.append(" String fourth();\n");
      code.append("}");
      return code;
    }
  };
  private static final MockJavaResource PARTITION_SIZE_ENTRY_MESSAGES = new MockJavaResource(
      "foo.PartitionSizeEntryMessage") {
    @Override
    public CharSequence getContent() {
      StringBuffer code = new StringBuffer();
      code.append("package foo;\n");
      code.append("public interface PartitionSizeEntryMessage extends foo.Lookup {\n");
      for (int i = 1; i <= TEST_PARTITION_SIZE; i++) {
        code.append(" String lookupMethod");
        code.append(i);
        code.append("();\n");
      }
      code.append("}");
      return code;
    }
  };

  private static final MockJavaResource LOOKUP = new MockJavaResource("foo.Lookup") {
    @Override
    public CharSequence getContent() {
      StringBuffer code = new StringBuffer();
      code.append("package foo;\n");
      code.append("public interface Lookup {\n");
      code.append(" String getString(String arg0);\n");
      code.append(" int getInt(String arg0);\n");
      code.append("}");
      return code;
    }
  };

  private TreeLogger logger = new FailErrorLogger();

  private GwtLocale locale;

  private ConstantsWithLookupImplCreator constantsWithLookupImplCreator;

  private SourceWriter sw = new StringSourceWriter();

  private TypeOracle oracle;

  private JMethod stringMethod;

  private JMethod intMethod;

  @Override
  public void setUp() throws TypeOracleException, UnableToCompleteException {
    GwtLocaleFactory factory = new GwtLocaleFactoryImpl();
    locale = factory.fromString("en");
    oracle = TypeOracleTestingUtils.buildStandardTypeOracleWith(logger, SINGLE_ENTRY_MESSAGES,
        FOUR_ENTRY_MESSAGES, LOOKUP, PARTITION_SIZE_ENTRY_MESSAGES);
    initLookupMethodCreator(SINGLE_ENTRY_MESSAGES);
  }

  public void testClassEpilogCallCreatesNeededPartitonLookups() {
    List<JMethod> missingPartition0 = Arrays.asList(stringMethod, intMethod);
    List<List<JMethod>> methodToCreatePartitionLookups = new ArrayList<>();
    methodToCreatePartitionLookups.add(missingPartition0);
    constantsWithLookupImplCreator.addNeededPartitionLookups(stringMethod,
        methodToCreatePartitionLookups);

    constantsWithLookupImplCreator.classEpilog();

    String actual = sw.toString().trim();

    assertTrue("Missing partition lookup method (getStringFromPartition0).", actual.contains(
        "java.lang.String getStringFromPartition0(java.lang.String arg0) {"));
  }

  public void testCodeForIntIsGeneratedSameAsWithoutPartitions() throws TypeOracleException,
      UnableToCompleteException {
    initLookupMethodCreator(SINGLE_ENTRY_MESSAGES);
    constantsWithLookupImplCreator.emitMethodBody(logger, intMethod, locale);

    // Same generated code as in version 2.8.1
    SourceWriter expected = new StringSourceWriter();
    expected.println("Integer target = (Integer) cache.get(arg0);");
    expected.println("if (target != null) {");
    expected.indent();
    expected.println("return target.intValue();");
    expected.outdent();
    expected.println("}");

    expected.println("if (arg0.equals(\"intEntry\")) {");
    expected.indent();
    expected.println("int answer = intEntry();");
    // Needed because the return template use '\n'
    expected.outdent();
    expected.println("cache.put(\"intEntry\",new Integer(answer));");
    expected.println("return answer;");
    expected.outdent();
    expected.println("}");

    expected.println("throw new java.util.MissingResourceException("
        + "\"Cannot find constant '\" +arg0 + \"'; expecting a method name\", \"foo.SingleEntryMessage\", arg0);");
    expected.outdent();

    String actual = sw.toString();
    assertEquals("Wrong source Lookup created.", expected.toString(), actual);
  }

  public void testCodeIsGeneratedSameAsWithoutPartitions() throws TypeOracleException,
      UnableToCompleteException {
    initLookupMethodCreator(PARTITION_SIZE_ENTRY_MESSAGES);
    constantsWithLookupImplCreator.emitMethodBody(logger, stringMethod, locale);

    // Same generated code as in version 2.8.1
    SourceWriter expected = new StringSourceWriter();
    expected.println("java.lang.String target = (java.lang.String) cache.get(arg0);");
    expected.println("if (target != null) {");
    expected.indent();
    expected.println("return target;");
    expected.outdent();
    expected.println("}");

    expected.println("if (arg0.equals(\"lookupMethod1\")) {");
    expected.indent();
    expected.print("String answer = lookupMethod1();");
    expected.print("\n");
    expected.print("cache.put(\"lookupMethod1\",answer);");
    expected.print("\n");
    expected.println("return answer;");
    expected.outdent();
    expected.println("}");

    expected.println("if (arg0.equals(\"lookupMethod2\")) {");
    expected.indent();
    expected.print("String answer = lookupMethod2();");
    expected.print("\n");
    expected.print("cache.put(\"lookupMethod2\",answer);");
    expected.print("\n");
    expected.println("return answer;");
    expected.outdent();
    expected.println("}");

    expected.println("if (arg0.equals(\"lookupMethod3\")) {");
    expected.indent();
    expected.print("String answer = lookupMethod3();");
    expected.print("\n");
    expected.print("cache.put(\"lookupMethod3\",answer);");
    expected.print("\n");
    expected.println("return answer;");
    expected.outdent();
    expected.println("}");

    expected.println("throw new java.util.MissingResourceException("
        + "\"Cannot find constant '\" +arg0 + \"'; expecting a method name\", \"foo.PartitionSizeEntryMessage\", arg0);");
    expected.outdent();

    String actual = sw.toString();
    assertEquals("Wrong source Lookup created.", expected.toString(), actual);
  }

  public void testCreateMethodForJMethodForSingleEntry() throws UnableToCompleteException {
    constantsWithLookupImplCreator.emitMethodBody(logger, stringMethod, locale);

    assertFalse("No partition lookup should created.", sw.toString().contains(
        "java.lang.String getString0(java.lang.String arg0) {"));
  }

  public void testCreateMethodForJMethodForSingleIntEntry() throws UnableToCompleteException {
    constantsWithLookupImplCreator.emitMethodBody(logger, intMethod, locale);

    String createdSource = sw.toString();
    assertTrue("Lookup for intMethod not created.", createdSource.contains(
        "int answer = intEntry();"));
  }

  public void testCreateMethodForPartitionSizeEntriesNoPartitionNeeded() throws TypeOracleException,
      UnableToCompleteException {
    initLookupMethodCreator(PARTITION_SIZE_ENTRY_MESSAGES);
    constantsWithLookupImplCreator.emitMethodBody(logger, stringMethod, locale);

    String actual = sw.toString();

    assertFalse("Missing partition lookup method (getStringFromPartitionN).", actual.contains(
        "getStringFromPartition"));
  }

  public void testCreatePartitionMethodName() {
    String partitionMethodName = constantsWithLookupImplCreator.createPartitionMethodName(
        stringMethod, 1);
    String expectedPartitionMethodName = "getStringFromPartition1";
    assertEquals(expectedPartitionMethodName, partitionMethodName);
  }

  public void testEmitClassWithMultiMessageEntryCreateOneAdditionalPartition()
      throws TypeOracleException, UnableToCompleteException {
    initLookupMethodCreator(FOUR_ENTRY_MESSAGES);
    constantsWithLookupImplCreator.emitClass(logger, locale);

    String actual = sw.toString().trim();

    assertTrue("Missing partition lookup method (getStringFromPartition0).", actual.contains(
        "java.lang.String getStringFromPartition0(java.lang.String arg0) {"));
  }

  public void testEmitMethodBodyForJMethodForMultiMessageEntryNeedOneAdditionalPartition()
      throws TypeOracleException, UnableToCompleteException {
    initLookupMethodCreator(FOUR_ENTRY_MESSAGES);
    constantsWithLookupImplCreator.emitMethodBody(logger, stringMethod, locale);

    String actual = sw.toString().trim();

    assertTrue("Method should end with call partition lookup method.", actual.endsWith(
        "return getStringFromPartition0(arg0);"));
    assertFalse("Partition method should not be created", actual.contains(
        "java.lang.String getStringFromPartition0(java.lang.String arg0) {"));

    assertEquals(1, constantsWithLookupImplCreator.getNeededPartitionLookups().size());
  }

  public void testFindMethodsToCreate() throws TypeOracleException {
    initLookupMethodCreator(SINGLE_ENTRY_MESSAGES);

    JType intType = oracle.parse(int.class.getName());
    List<JMethod> intMethods = constantsWithLookupImplCreator.findAllMethodsToCreate(intMethod,
        intType);

    JMethod expectedIntMethod = oracle.findType(SINGLE_ENTRY_MESSAGES.getTypeName()).findMethod(
        "intEntry", new JType[0]);

    assertEquals(1, intMethods.size());
    assertEquals(expectedIntMethod, intMethods.get(0));
  }

  public void testFindMethodsToCreateWithDifferentTargetMethod() throws TypeOracleException {
    initLookupMethodCreator(SINGLE_ENTRY_MESSAGES);

    JType intType = oracle.parse(int.class.getName());
    List<JMethod> foundMethods = constantsWithLookupImplCreator.findAllMethodsToCreate(stringMethod,
        intType);

    JMethod expectedIntMethod = oracle.findType(SINGLE_ENTRY_MESSAGES.getTypeName()).findMethod(
        "intEntry", new JType[0]);

    assertEquals(2, foundMethods.size());
    assertTrue(foundMethods.contains(intMethod));
    assertTrue(foundMethods.contains(expectedIntMethod));
  }

  public void testGetReturnTypeNameForPrimitveTypes() {
    for (JPrimitiveType primitiveType : JPrimitiveType.values()) {
      LookupMethodCreator primitiveMethodCreator = new LookupMethodCreator(null, primitiveType);
      String returnType = primitiveMethodCreator.getReturnTypeName();
      String expectedType = primitiveType.getQualifiedBoxedSourceName().substring("java.lang."
          .length());
      assertEquals("Wrong Return Type for primitve type", expectedType, returnType);
    }
  }

  private void initLookupMethodCreator(MockJavaResource resource) throws TypeOracleException {
    initLookupMethodCreator(resource, TEST_PARTITION_SIZE);
  }

  private void initLookupMethodCreator(MockJavaResource resource, int partitionsSize)
      throws TypeOracleException {
    JClassType clazz = oracle.findType(resource.getTypeName());
    try {
      ResourceList resourceList = mock(ResourceList.class);
      when(resourceList.getRequiredStringExt(anyString(), anyString())).thenReturn(
          "Required value");
      constantsWithLookupImplCreator = new ConstantsWithLookupImplCreator(logger, sw, clazz,
          resourceList, oracle, partitionsSize);

      JType stringType = oracle.parse(String.class.getName());
      stringMethod = oracle.findType(LOOKUP.getTypeName()).findMethod("getString", new JType[] {
          stringType});
      intMethod = oracle.findType(LOOKUP.getTypeName()).findMethod("getInt", new JType[] {
          stringType});
    } catch (UnableToCompleteException e) {
      fail(e.getMessage());
    }
  }

}
