/*
 * Copyright 2011 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.web.bindery.requestfactory.apt;

import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryPolymorphicTest;
import com.google.web.bindery.requestfactory.shared.LoggingRequest;
import com.google.web.bindery.requestfactory.shared.SimpleRequestFactory;
import com.google.web.bindery.requestfactory.shared.TestRequestFactory;
import com.google.web.bindery.requestfactory.shared.impl.FindRequest;

import junit.framework.TestCase;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;

/**
 * Integration test of {@link RfValidator} using the Java6 tools API to invoke
 * the Java compiler. This test requires that the gwt-user source is available
 * on the classpath.
 */
public class RfValidatorTest extends TestCase {
  /**
   * Provides access to a source file from the classpath.
   */
  private static class UriJavaFileObject extends SimpleJavaFileObject {
    public static UriJavaFileObject create(Class<?> toLoad) {
      try {
        String path = toLoad.getName().replace('.', '/') + ".java";
        /*
         * SimpleJavaFileObject does not like the URI's created from jar URLs
         * since their path does not start with a leading forward-slash. The
         * choice of "classpath" as the scheme is arbitrary and meaningless.
         */
        URI fakeLocation = new URI("classpath:/" + path);
        return new UriJavaFileObject(fakeLocation, Thread.currentThread().getContextClassLoader()
            .getResource(path));
      } catch (URISyntaxException e) {
        throw new RuntimeException(e);
      }
    }

    private final URL contents;

    public UriJavaFileObject(URI fakeLocation, URL contents) throws URISyntaxException {
      super(fakeLocation, JavaFileObject.Kind.SOURCE);
      this.contents = contents;
    }

    @Override
    public CharSequence getCharContent(boolean ignored) throws IOException {
      try (InputStream is = contents.openStream()) {
        return new String(is.readAllBytes(), StandardCharsets.UTF_8);
      }
    }
  }

  /**
   * Smoke test to ensure that appropriate errors and warnings are emitted.
   */
  public void testErrorsAndWarnings() throws IOException {
    testGeneratedMessages(EntityProxyCheckDomainMapping.class);
    testGeneratedMessages(EntityProxyMismatchedFind.class);
    testGeneratedMessages(EntityProxyMissingDomainLocatorMethods.class);
    testGeneratedMessages(EntityProxyMissingDomainType.class);
    testGeneratedMessages(MyRequestContext.class);
    testGeneratedMessages(MyRequestFactory.class);
    testGeneratedMessages(RequestContextMissingDomainType.class);
    testGeneratedMessages(RequestContextUsingUnmappedProxy.class,
        EntityProxyMissingDomainType.class);
    testGeneratedMessages(RequestContextWithMismatchedBoxes.class);
    testGeneratedMessages(RequestContextWithMismatchedInstance.class);
  }

  /**
   * The target classes for this method don't contain any {@code @Expect}
   * annotations, so this test will verify that they are error- and
   * warning-free.
   */
  public void testTestClasses() throws IOException {
    testGeneratedMessages(RequestFactoryPolymorphicTest.class);
    testGeneratedMessages(FindRequest.class);
    testGeneratedMessages(LoggingRequest.class);
    testGeneratedMessages(SimpleRequestFactory.class);
    testGeneratedMessages(TestRequestFactory.class);
  }

  /**
   * The target classes for this method don't contain any {@code @Expect}
   * annotations, so this test will verify that they are error- and
   * warning-free.
   * 
   * @throws IOException
   */
  public void testTestClassesClientOnly() throws IOException {
    testGeneratedMessages(true, RequestFactoryPolymorphicTest.class);
    testGeneratedMessages(true, FindRequest.class);
    testGeneratedMessages(true, LoggingRequest.class);
    testGeneratedMessages(true, SimpleRequestFactory.class);
    testGeneratedMessages(true, TestRequestFactory.class);
  }

  private void assertEquals(TreeSet<Diagnostic<? extends JavaFileObject>> expected,
      TreeSet<Diagnostic<? extends JavaFileObject>> actual) {
    List<Diagnostic<?>> unmatched = new ArrayList<Diagnostic<?>>();

    // Remove actual elements from expect, saving any unexpected elements
    for (Diagnostic<? extends JavaFileObject> d : actual) {
      if (!expected.remove(d)) {
        unmatched.add(d);
      }
    }

    assertTrue("Did not see expected errors: " + expected + "\n\nLeftovers :" + unmatched, expected
        .isEmpty());
    assertTrue("Unexpected errors: " + unmatched, unmatched.isEmpty());
  }

  private void testGeneratedMessages(Class<?>... classes) throws IOException {
    testGeneratedMessages(false, classes);
  }

  /**
   * Run the annotation processor over one or more classes and verify that the
   * appropriate messages are generated.
   */
  private void testGeneratedMessages(boolean clientOnly, Class<?>... classes) throws IOException {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    if (compiler == null) {
      // This test is being run without a full JDK
      return;
    }

    // Don't spray files in random locations
    File tempFile = File.createTempFile(RfValidatorTest.class.getSimpleName(), ".jar");
    tempFile.deleteOnExit();
    JavaFileManager fileManager =
        new ValidationTool.JarOrDirectoryOutputFileManager(tempFile, compiler
            .getStandardFileManager(null, null, null));

    List<JavaFileObject> files = new ArrayList<JavaFileObject>(classes.length);
    for (Class<?> clazz : classes) {
      JavaFileObject obj = UriJavaFileObject.create(clazz);
      files.add(obj);
    }
    StringWriter errorWriter = new StringWriter();
    RfValidator rfValidator = new RfValidator();
    rfValidator.setForceErrors(true);
    rfValidator.setClientOnly(clientOnly);

    DiagnosticCollector<JavaFileObject> expectedCollector =
        new DiagnosticCollector<JavaFileObject>();
    CompilationTask expectedTask =
        compiler.getTask(errorWriter, fileManager, expectedCollector, Arrays.asList("-proc:only"),
            null, files);
    expectedTask.setProcessors(Arrays.asList(new ExpectCollector()));
    expectedTask.call();

    DiagnosticCollector<JavaFileObject> actualCollector = new DiagnosticCollector<JavaFileObject>();
    CompilationTask actualTask =
        compiler.getTask(errorWriter, fileManager, actualCollector, Arrays.asList("-proc:only"),
            null, files);
    actualTask.setProcessors(Arrays.asList(rfValidator));
    actualTask.call();

    TreeSet<Diagnostic<? extends JavaFileObject>> expected =
        new TreeSet<Diagnostic<? extends JavaFileObject>>(new DiagnosticComparator());
    expected.addAll(expectedCollector.getDiagnostics());
    TreeSet<Diagnostic<? extends JavaFileObject>> actual =
        new TreeSet<Diagnostic<? extends JavaFileObject>>(new DiagnosticComparator());
    actual.addAll(actualCollector.getDiagnostics());
    assertEquals(expected, actual);
  }
}
