/*
 * Copyright 2012 Google Inc. All rights reserved.
 *
 * 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.errorprone.apply;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyMapOf;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.ErrorProneEndPosMap;

import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCImport;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Unit tests for {@link ImportStatements}
 *
 * @author [email protected] (Eddie Aftandilian)
 */
@RunWith(JUnit4.class)
public class ImportStatementsTest {
  private static final ErrorProneEndPosMap FAKE_END_POS_MAP = new ErrorProneEndPosMap() {
    @Override
    public Integer getEndPosition(DiagnosticPosition pos) {
      return pos.getEndPosition(null);
    }

    @Override
    public Set<Entry<JCTree, Integer>> entrySet() {
      return ImmutableSet.of();
    }
  };

  /**
   * A stubbed package JCExpression to use for testing.
   */
  private final JCExpression basePackage = stubPackage(79);

  /**
   * An unsorted list of JCImport stubs to use for testing.
   */
  private final List<JCImport> baseImportList = ImmutableList.of(
      stubImport("com.google.common.base.Preconditions.checkNotNull", true, 82, 145),
      stubImport("com.google.ads.pebl.AdGroupCriterionPredicate.PAUSED", true, 147, 213),
      stubImport("com.google.common.collect.ImmutableMap", false, 215, 260),
      stubImport("com.google.common.collect.ImmutableList", false, 262, 308),
      stubImport("org.joda.time.Interval", false, 310, 339),
      stubImport("org.joda.time.DateTime", false, 341, 370),
      stubImport("org.joda.time.DateTimeZone", false, 372, 405),
      stubImport("com.sun.tools.javac.tree.JCTree", false, 407, 445),
      stubImport("com.sun.source.tree.ImportTree", false, 447, 484),
      stubImport("com.sun.tools.javac.tree.JCTree.JCExpression", false, 486, 537),
      stubImport("com.sun.source.tree.CompilationUnitTree", false, 539, 585),
      stubImport("java.io.File", false, 587, 606),
      stubImport("java.util.Iterator", false, 608, 633),
      stubImport("java.io.IOException", false, 635, 661),
      stubImport("javax.tools.StandardJavaFileManager", false, 663, 705),
      stubImport("javax.tools.JavaFileObject", false, 707, 740),
      stubImport("javax.tools.JavaCompiler", false, 742, 773),
      stubImport("javax.tools.ToolProvider", false, 775, 806));


  /**
   * A helper method to create a stubbed package JCExpression.
   *
   * @param endPos the end position of the package JCExpression
   * @return a new package JCExpression stub
   */
  private static JCExpression stubPackage(int endPos) {
    JCExpression result = mock(JCExpression.class);
    when(result.getEndPosition(anyMapOf(JCTree.class, Integer.class))).thenReturn(endPos);
    return result;
  }

  /**
   * A helper method to create a JCImport stub.
   *
   * @param typeName the fully-qualified name of the type being imported
   * @param isStatic whether the import is static
   * @param startPos the start position of the import statement
   * @param endPos the end position of the import statement
   * @return a new JCImport stub
   */
  private static JCImport stubImport(String typeName, boolean isStatic, int startPos, int endPos) {
    JCImport result = mock(JCImport.class);
    when(result.isStatic()).thenReturn(isStatic);
    when(result.getStartPosition()).thenReturn(startPos);
    when(result.getEndPosition(anyMapOf(JCTree.class, Integer.class))).thenReturn(endPos);

    // craft import string
    StringBuilder returnSB = new StringBuilder("import ");
    if (isStatic) {
      returnSB.append("static ");
    }
    returnSB.append(typeName);
    returnSB.append(";\n");
    when(result.toString()).thenReturn(returnSB.toString());
    return result;
  }


  /**
   * Test extracting the top-level package from a non-static import statement.
   */
  @Test
  public void shouldGetTopLevelFromImport() {
    assertEquals("com",
        ImportStatements.getTopLevel("import com.google.ads.pebl.AdGroupCriterionPredicate"));
  }

  /**
   * Test extracting the top-level package from a non-static import statement that
   * ends with a wildcard.
   */
  @Test
  public void shouldGetTopLevelFromImportWithWildcard() {
    assertEquals("com",
        ImportStatements.getTopLevel("import com.google.ads.pebl.*"));
  }

  /**
   * Test extracting the top-level package from a static import statement.
   */
  @Test
  public void shouldGetTopLevelFromStaticImport() {
    assertEquals("org",
        ImportStatements.getTopLevel("import static org.junit.Assert.assertEquals"));
  }

  /**
   * Test extracting the top-level package from a static import statement that
   * ends with a wildcard.
   */
  @Test
  public void shouldGetTopLevelFromStaticImportWithWildcard() {
    assertEquals("org",
        ImportStatements.getTopLevel("import static org.junit.Assert.*"));
  }

  /**
   * Test extracting the top-level package from an import statement with
   * extra whitespace.
   */
  @Test
  public void shouldGetTopLevelFromImportWithWhitespace() {
    assertEquals("com",
        ImportStatements.getTopLevel("import   com.google.ads.pebl.AdGroupCriterionPredicate   "));
  }


  /**
   * Test that getting the top-level package from an improperly-formatted
   * import statement throws an IllegalArgumentException.
   */
  @Test(expected = IllegalArgumentException.class)
  public void shouldThrowExceptionForImproperlyFormattedImport() {
    ImportStatements.getTopLevel("not an import");
    fail("Should have thrown IllegalArgumentException");
  }

  /**
   * Test that the import statements are sorted correctly.
   */
  @Test
  public void shouldSortImports() {
    ImportStatements imports = new ImportStatements(basePackage, baseImportList, FAKE_END_POS_MAP);

    assertEquals(
        "import static com.google.ads.pebl.AdGroupCriterionPredicate.PAUSED;\n"
        + "import static com.google.common.base.Preconditions.checkNotNull;\n"
        + "\n"
        + "import com.google.common.collect.ImmutableList;\n"
        + "import com.google.common.collect.ImmutableMap;\n"
        + "\n"
        + "import com.sun.source.tree.CompilationUnitTree;\n"
        + "import com.sun.source.tree.ImportTree;\n"
        + "import com.sun.tools.javac.tree.JCTree;\n"
        + "import com.sun.tools.javac.tree.JCTree.JCExpression;\n"
        + "\n"
        + "import org.joda.time.DateTime;\n"
        + "import org.joda.time.DateTimeZone;\n"
        + "import org.joda.time.Interval;\n"
        + "\n"
        + "import java.io.File;\n"
        + "import java.io.IOException;\n"
        + "import java.util.Iterator;\n"
        + "\n"
        + "import javax.tools.JavaCompiler;\n"
        + "import javax.tools.JavaFileObject;\n"
        + "import javax.tools.StandardJavaFileManager;\n"
        + "import javax.tools.ToolProvider;",
        imports.toString());

  }

  /**
   * Test that adding a new import inserts it in the correct position.
   */
  @Test
  public void shouldAddImportInCorrectPosition() {
    ImportStatements imports = new ImportStatements(basePackage, baseImportList, FAKE_END_POS_MAP);
    boolean added = imports.add("import static org.junit.Assert.assertEquals");

    assertTrue(added);
    assertEquals(
        "import static com.google.ads.pebl.AdGroupCriterionPredicate.PAUSED;\n"
        + "import static com.google.common.base.Preconditions.checkNotNull;\n"
        + "import static org.junit.Assert.assertEquals;\n"
        + "\n"
        + "import com.google.common.collect.ImmutableList;\n"
        + "import com.google.common.collect.ImmutableMap;\n"
        + "\n"
        + "import com.sun.source.tree.CompilationUnitTree;\n"
        + "import com.sun.source.tree.ImportTree;\n"
        + "import com.sun.tools.javac.tree.JCTree;\n"
        + "import com.sun.tools.javac.tree.JCTree.JCExpression;\n"
        + "\n"
        + "import org.joda.time.DateTime;\n"
        + "import org.joda.time.DateTimeZone;\n"
        + "import org.joda.time.Interval;\n"
        + "\n"
        + "import java.io.File;\n"
        + "import java.io.IOException;\n"
        + "import java.util.Iterator;\n"
        + "\n"
        + "import javax.tools.JavaCompiler;\n"
        + "import javax.tools.JavaFileObject;\n"
        + "import javax.tools.StandardJavaFileManager;\n"
        + "import javax.tools.ToolProvider;",
        imports.toString());
  }

  /**
   * Test that adding multiple new imports using addAll() inserts them
   * in the correct positions.
   */
  @Test
  public void shouldAddMultipleImportsInCorrectPositions() {
    ImportStatements imports = new ImportStatements(basePackage, baseImportList, FAKE_END_POS_MAP);
    boolean added = imports.addAll(Arrays.asList("import static org.junit.Assert.assertEquals",
        "import javax.servlet.http.HttpServletRequest",
        "import com.google.common.flags.FlagSpec"));

    assertEquals(
        "import static com.google.ads.pebl.AdGroupCriterionPredicate.PAUSED;\n"
        + "import static com.google.common.base.Preconditions.checkNotNull;\n"
        + "import static org.junit.Assert.assertEquals;\n"
        + "\n"
        + "import com.google.common.collect.ImmutableList;\n"
        + "import com.google.common.collect.ImmutableMap;\n"
        + "import com.google.common.flags.FlagSpec;\n"
        + "\n"
        + "import com.sun.source.tree.CompilationUnitTree;\n"
        + "import com.sun.source.tree.ImportTree;\n"
        + "import com.sun.tools.javac.tree.JCTree;\n"
        + "import com.sun.tools.javac.tree.JCTree.JCExpression;\n"
        + "\n"
        + "import org.joda.time.DateTime;\n"
        + "import org.joda.time.DateTimeZone;\n"
        + "import org.joda.time.Interval;\n"
        + "\n"
        + "import java.io.File;\n"
        + "import java.io.IOException;\n"
        + "import java.util.Iterator;\n"
        + "\n"
        + "import javax.servlet.http.HttpServletRequest;\n"
        + "import javax.tools.JavaCompiler;\n"
        + "import javax.tools.JavaFileObject;\n"
        + "import javax.tools.StandardJavaFileManager;\n"
        + "import javax.tools.ToolProvider;",
        imports.toString());
  }

  /**
   * Test that adding an already-existing import doesn't change anything.
   */
  @Test
  public void shouldNotAddExistingImport() {
    ImportStatements imports = new ImportStatements(basePackage, baseImportList, FAKE_END_POS_MAP);
    boolean added = imports.add("import com.google.common.collect.ImmutableMap");

    assertTrue(!added);
    assertEquals(
        "import static com.google.ads.pebl.AdGroupCriterionPredicate.PAUSED;\n"
        + "import static com.google.common.base.Preconditions.checkNotNull;\n"
        + "\n"
        + "import com.google.common.collect.ImmutableList;\n"
        + "import com.google.common.collect.ImmutableMap;\n"
        + "\n"
        + "import com.sun.source.tree.CompilationUnitTree;\n"
        + "import com.sun.source.tree.ImportTree;\n"
        + "import com.sun.tools.javac.tree.JCTree;\n"
        + "import com.sun.tools.javac.tree.JCTree.JCExpression;\n"
        + "\n"
        + "import org.joda.time.DateTime;\n"
        + "import org.joda.time.DateTimeZone;\n"
        + "import org.joda.time.Interval;\n"
        + "\n"
        + "import java.io.File;\n"
        + "import java.io.IOException;\n"
        + "import java.util.Iterator;\n"
        + "\n"
        + "import javax.tools.JavaCompiler;\n"
        + "import javax.tools.JavaFileObject;\n"
        + "import javax.tools.StandardJavaFileManager;\n"
        + "import javax.tools.ToolProvider;",
        imports.toString());
  }

  /**
   * Test that removing an import works and the resulting output is
   * correctly sorted.
   */
  @Test
  public void shouldRemoveImportAndSort() {
    ImportStatements imports = new ImportStatements(basePackage, baseImportList, FAKE_END_POS_MAP);
    boolean removed = imports.remove("import com.sun.tools.javac.tree.JCTree");

    assertTrue(removed);
    assertEquals(
        "import static com.google.ads.pebl.AdGroupCriterionPredicate.PAUSED;\n"
        + "import static com.google.common.base.Preconditions.checkNotNull;\n"
        + "\n"
        + "import com.google.common.collect.ImmutableList;\n"
        + "import com.google.common.collect.ImmutableMap;\n"
        + "\n"
        + "import com.sun.source.tree.CompilationUnitTree;\n"
        + "import com.sun.source.tree.ImportTree;\n"
        + "import com.sun.tools.javac.tree.JCTree.JCExpression;\n"
        + "\n"
        + "import org.joda.time.DateTime;\n"
        + "import org.joda.time.DateTimeZone;\n"
        + "import org.joda.time.Interval;\n"
        + "\n"
        + "import java.io.File;\n"
        + "import java.io.IOException;\n"
        + "import java.util.Iterator;\n"
        + "\n"
        + "import javax.tools.JavaCompiler;\n"
        + "import javax.tools.JavaFileObject;\n"
        + "import javax.tools.StandardJavaFileManager;\n"
        + "import javax.tools.ToolProvider;",
        imports.toString());
  }

  /**
   * Test that removing multiple imports using removeAll() works
   * and the resulting output is correctly sorted.
   */
  @Test
  public void shouldRemoveMultipleImportsAndSort() {
    ImportStatements imports = new ImportStatements(basePackage, baseImportList, FAKE_END_POS_MAP);
    boolean removed = imports.removeAll(Arrays.asList("import com.sun.tools.javac.tree.JCTree",
        "import static com.google.common.base.Preconditions.checkNotNull",
        "import org.joda.time.Interval"));

    assertEquals(
        "import static com.google.ads.pebl.AdGroupCriterionPredicate.PAUSED;\n"
        + "\n"
        + "import com.google.common.collect.ImmutableList;\n"
        + "import com.google.common.collect.ImmutableMap;\n"
        + "\n"
        + "import com.sun.source.tree.CompilationUnitTree;\n"
        + "import com.sun.source.tree.ImportTree;\n"
        + "import com.sun.tools.javac.tree.JCTree.JCExpression;\n"
        + "\n"
        + "import org.joda.time.DateTime;\n"
        + "import org.joda.time.DateTimeZone;\n"
        + "\n"
        + "import java.io.File;\n"
        + "import java.io.IOException;\n"
        + "import java.util.Iterator;\n"
        + "\n"
        + "import javax.tools.JavaCompiler;\n"
        + "import javax.tools.JavaFileObject;\n"
        + "import javax.tools.StandardJavaFileManager;\n"
        + "import javax.tools.ToolProvider;",
        imports.toString());
  }

  /**
   * Test that removing a non-existent import doesn't change anything.
   */
  @Test
  public void removingNonExistingImportShouldntChangeImports() {
    ImportStatements imports = new ImportStatements(basePackage, baseImportList, FAKE_END_POS_MAP);
    boolean removed = imports.remove("import org.joda.time.format.ISODateTimeFormat;\n");

    assertTrue(!removed);
    assertEquals(
        "import static com.google.ads.pebl.AdGroupCriterionPredicate.PAUSED;\n"
        + "import static com.google.common.base.Preconditions.checkNotNull;\n"
        + "\n"
        + "import com.google.common.collect.ImmutableList;\n"
        + "import com.google.common.collect.ImmutableMap;\n"
        + "\n"
        + "import com.sun.source.tree.CompilationUnitTree;\n"
        + "import com.sun.source.tree.ImportTree;\n"
        + "import com.sun.tools.javac.tree.JCTree;\n"
        + "import com.sun.tools.javac.tree.JCTree.JCExpression;\n"
        + "\n"
        + "import org.joda.time.DateTime;\n"
        + "import org.joda.time.DateTimeZone;\n"
        + "import org.joda.time.Interval;\n"
        + "\n"
        + "import java.io.File;\n"
        + "import java.io.IOException;\n"
        + "import java.util.Iterator;\n"
        + "\n"
        + "import javax.tools.JavaCompiler;\n"
        + "import javax.tools.JavaFileObject;\n"
        + "import javax.tools.StandardJavaFileManager;\n"
        + "import javax.tools.ToolProvider;",
        imports.toString());
  }

  /**
   * Test empty initial import list. Positions should match package end
   * positions.
   */
  @Test
  public void emptyImportListShouldGivePositionOfPackageStmt() {
    ImportStatements imports = new ImportStatements(
        basePackage, new ArrayList<JCImport>(), FAKE_END_POS_MAP);
    assertEquals(81, imports.getStartPos());
    assertEquals(81, imports.getEndPos());
  }

  /**
   * Test empty initial import list. The output string should start and
   * end with newlines because it is intended to be inserted after the
   * package statement.
   */
  @Test
  public void addingToEmptyImportListOutputShouldStartAndEndWithNewlines() {
    ImportStatements imports = new ImportStatements(
        basePackage, new ArrayList<JCImport>(), FAKE_END_POS_MAP);
    imports.add("import org.joda.time.Interval");
    assertEquals("\n"
        + "import org.joda.time.Interval;\n",
        imports.toString());
  }

  /**
   * Test start and end position calculations. The start position should be
   * the start offset of the first import statement, and the end position
   * should be the end position of the last import statement.
   */
  @Test
  public void startAndEndPositionsShouldComeFromImportStatements() {
    ImportStatements imports = new ImportStatements(
        basePackage, baseImportList, FAKE_END_POS_MAP);
    assertEquals(82, imports.getStartPos());
    assertEquals(806, imports.getEndPos());
  }
}