/*******************************************************************************
 * Copyright 2011 Google Inc. All Rights Reserved.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * 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.eclipse.core.validators.java;

import com.google.gdt.eclipse.core.SWTUtilities;
import com.google.gdt.eclipse.core.jobs.JobsUtilities;
import com.google.gwt.eclipse.core.markers.GWTJavaProblem;
import com.google.gwt.eclipse.core.markers.GWTProblemType;
import com.google.gwt.eclipse.core.nature.GWTNature;
import com.google.gwt.eclipse.core.search.JavaRefIndex;
import com.google.gwt.eclipse.core.test.AbstractGWTPluginTestCase;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitDocumentProvider;
import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitDocumentProvider.ProblemAnnotation;
import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Tests the {@link JavaCompilationParticipant} class.
 */
@SuppressWarnings("restriction")
public class JavaCompilationParticipantTest extends AbstractGWTPluginTestCase {
  // NOTE: This test is disabled in the pom.xml file.
  // See https://code.google.com/p/google-plugin-for-eclipse/issues/detail?id=329 for details.

  private TestClass testClass;

  public void testBuildAddError() throws Exception {
    IProject project = getTestProject().getProject();

    // Verify that we have 1 existing GWT problem marker
    IMarker[] markers = getGWTProblemMarkers(project);
    assertEquals(1, markers.length);

    ICompilationUnit cu = testClass.getCompilationUnit();

    // Open the test class in the editor
    CompilationUnitEditor editor = (CompilationUnitEditor) JavaUI.openInEditor(cu);
    IEditorInput editorInput = editor.getEditorInput();

    // Edit the document to create a new error (add 'foobar' to the front of
    // the class name in a Java reference)
    IDocument document = editor.getDocumentProvider().getDocument(editorInput);
    TextEdit errorEdit = new InsertEdit(254, "foobar");
    errorEdit.apply(document);
    // Save the changes
    editor.doSave(null);

    // Rebuild the project
    rebuildTestProject();

    // Verify that we now have 2 GWT problem markers
    markers = getGWTProblemMarkers(project);
    assertEquals(2, markers.length);
  }

  public void testBuildTypeDependencies() throws Exception {
    IProject project = getTestProject().getProject();

    // Verify that we have 1 existing GWT problem marker (it's about the
    // unresolved type com.hello.client.A.B)
    IMarker[] markers = getGWTProblemMarkers(project);
    assertEquals(1, markers.length);

    // Now go ahead and add the missing A.java with nested class B
    String[] aSource = new String[]{
        "package com.hello.client;",
        "public class A {",
        "  public static class B {",
        "    public static int getNumber() {",
        "      return 777;",
        "    }",
        "  }",
        "}"
    };
    TestClass aClass = new TestClass(aSource, "A");
    aClass.addToTestProject();
    SWTUtilities.delay(2000);

    // Verify that we cleared the unresolved type error (the original
    // test class should have rebuilt automatically when we added A.java, thanks
    // to its type dependency on com.hello.client.A.B)
    markers = getGWTProblemMarkers(project);
    assertEquals(0, markers.length);
  }

  public void testClean() throws CoreException {
    IProject project = getTestProject().getProject();
    IEclipsePreferences prefs = new InstanceScope().getNode("org.eclipse.core.resources");

    // Disable auto-building so we don't race with the build process.
    prefs.putBoolean("description.autobuilding", false);

    // Verify that we have existing problem markers
    IMarker[] markers = project.findMarkers(GWTJavaProblem.MARKER_ID, true,
        IResource.DEPTH_INFINITE);
    assertTrue(markers.length > 0);

    // Verify that we have existing JavaRefIndex entries
    assertEquals(3, JavaRefIndex.getInstance().size());

    // Clean the test project
    project.build(IncrementalProjectBuilder.CLEAN_BUILD, null);

    // Wait for the validation job to complete.
    JobsUtilities.waitForIdle();

    // Verify that the problems went away
    markers = project.findMarkers(GWTJavaProblem.MARKER_ID, true,
        IResource.DEPTH_INFINITE);
    assertEquals(0, markers.length);

    // Verify that the JavaRefIndex entries went away
    assertEquals(0, JavaRefIndex.getInstance().size());

    // Re-enable auto-building
    prefs.putBoolean("description.autobuilding", true);
  }

  public void testIsActiveGWTProject() {
    assertTrue(new JavaCompilationParticipant().isActive(getTestProject()));
  }

  public void testIsActiveNonGWTProject() throws CoreException {
    IProject project = getTestProject().getProject();
    GWTNature.removeNatureFromProject(project);

    assertFalse(new JavaCompilationParticipant().isActive(getTestProject()));

    GWTNature.addNatureToProject(project);
  }

  public void testReconcile() throws Exception {
    ICompilationUnit cu = testClass.getCompilationUnit();
    CompilationUnitEditor editor = null;

    try {
      editor = (CompilationUnitEditor) JavaUI.openInEditor(cu);
      IEditorInput editorInput = editor.getEditorInput();

      JobsUtilities.waitForIdle();

      // Initially, the compilation unit will have one GWT problem marker
      List<GWTJavaProblem> problems = getGWTProblemsInEditor(editor);
      assertEquals(1, problems.size());
      assertEquals(GWTProblemType.JSNI_JAVA_REF_UNRESOLVED_TYPE,
          problems.get(0).getProblemType());

      // Edit the document to create a new error (add 'foobar' to the front of
      // the class name in a Java reference)
      IDocument document = editor.getDocumentProvider().getDocument(editorInput);
      TextEdit errorEdit = new InsertEdit(254, "foobar");
      errorEdit.apply(document);

      // There should now be 2 GWT problem markers, wait up to 20 seconds for
      // the editor to reconcile
      for (int i = 0; i < 20 && problems.size() != 2; i++) {
        problems = getGWTProblemsInEditor(editor);
        SWTUtilities.delay(1000);
      }

      assertEquals(2, problems.size());
      assertEquals(GWTProblemType.JSNI_JAVA_REF_UNRESOLVED_TYPE,
          problems.get(0).getProblemType());
      assertEquals(GWTProblemType.JSNI_JAVA_REF_UNRESOLVED_TYPE,
          problems.get(1).getProblemType());

    } finally {
      if (editor != null) {
        editor.close(false);
      }
    }
  }

  public void testValidateCompilationUnit() throws Exception {
    ASTNode ast = parseTestClass();

    // Validate the compilation unit
    JavaValidationResult result = JavaCompilationParticipant.validateCompilationUnit(ast);

    // Verify that we added the refs to JavaRefIndex
    assertEquals(3, JavaRefIndex.getInstance().size());

    // Verify the problems returned from the validator
    List<GWTJavaProblem> problems = result.getProblems();
    assertEquals(1, problems.size());
    GWTJavaProblem problem = problems.get(0);
    assertEquals(GWTProblemType.JSNI_JAVA_REF_UNRESOLVED_TYPE,
        problem.getProblemType());
  }

  @Override
  protected TestClass[] getTestClasses() {
    String[] lines = new String[]{
        "package com.hello.client;",
        "",
        "public class JavaCompilationParticipantTest {",
        "",
        "  private int counter;",
        "",
        "  public native void jsniMethod()/*-{",
        "    // References to some Java types",
        "    var num = [email protected]$B::getNumber()();",
        "    num += [email protected]::getSum(II)(2, 2);",
        "    num += [email protected]::counter;",
        "  }-*/;",
        "",
        "  public static int getSum(int op1, int op2) {",
        "    return op1 + op2; ",
        "  }",
        "",
        "}"
        };

    testClass = new TestClass(lines, "JavaCompilationParticipantTest");
    return new TestClass[] {testClass};
  }

  @Override
  protected boolean requiresTestProject() {
    return true;
  }

  private IMarker[] getGWTProblemMarkers(IProject project) throws CoreException {
    return project.findMarkers(GWTJavaProblem.MARKER_ID, true,
        IResource.DEPTH_INFINITE);
  }

  private List<GWTJavaProblem> getGWTProblemsInEditor(CompilationUnitEditor editor)
      throws Exception {
    List<GWTJavaProblem> problems = new ArrayList<GWTJavaProblem>();

    Field annotationProblemField = CompilationUnitDocumentProvider.ProblemAnnotation.class.getDeclaredField("fProblem");
    annotationProblemField.setAccessible(true);

    IEditorInput editorInput = editor.getEditorInput();
    IDocumentProvider documentProvider = editor.getDocumentProvider();
    IAnnotationModel annotationModel = documentProvider.getAnnotationModel(editorInput);
    Iterator<?> iter = annotationModel.getAnnotationIterator();
    while (iter.hasNext()) {
      Object annotation = iter.next();
      if (annotation instanceof CompilationUnitDocumentProvider.ProblemAnnotation) {
        CompilationUnitDocumentProvider.ProblemAnnotation problemAnnotation = (ProblemAnnotation) annotation;
        if (problemAnnotation.getMarkerType().equals(GWTJavaProblem.MARKER_ID)) {
          GWTJavaProblem problem = (GWTJavaProblem) annotationProblemField.get(problemAnnotation);
          problems.add(problem);
        }
      }
    }

    return problems;
  }

  private ASTNode parseTestClass() {
    // Have JDT parse the compilation unit
    ASTParser parser = ASTParser.newParser(AST.JLS3);
    parser.setProject(getTestProject());
    parser.setResolveBindings(false);
    parser.setSource(testClass.getCompilationUnit());
    return parser.createAST(null);
  }

}