/*
 * Copyright 2010 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.gwt.rebind.model;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.CompilerContext;
import com.google.gwt.dev.javac.CompilationState;
import com.google.gwt.dev.javac.CompilationStateBuilder;
import com.google.gwt.dev.javac.testing.impl.JavaResourceBase;
import com.google.gwt.dev.javac.testing.impl.MockJavaResource;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.dev.util.UnitTestTreeLogger;
import com.google.gwt.dev.util.Util;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
import com.google.web.bindery.autobean.shared.Splittable;
import com.google.web.bindery.requestfactory.server.TestContextImpl;
import com.google.web.bindery.requestfactory.shared.EntityProxy;
import com.google.web.bindery.requestfactory.shared.EntityProxyId;
import com.google.web.bindery.requestfactory.shared.InstanceRequest;
import com.google.web.bindery.requestfactory.shared.Locator;
import com.google.web.bindery.requestfactory.shared.ProxyFor;
import com.google.web.bindery.requestfactory.shared.Receiver;
import com.google.web.bindery.requestfactory.shared.Request;
import com.google.web.bindery.requestfactory.shared.RequestContext;
import com.google.web.bindery.requestfactory.shared.RequestFactory;
import com.google.web.bindery.requestfactory.shared.Service;
import com.google.web.bindery.requestfactory.shared.ServiceLocator;
import com.google.web.bindery.requestfactory.shared.ValueProxy;

import junit.framework.TestCase;

import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;

/**
 * Test case for
 * {@link com.google.web.bindery.requestfactory.gwt.rebind.model.RequestFactoryModel} that
 * uses mock CompilationStates.
 */
public class RequestFactoryModelTest extends TestCase {

  /**
   * Constructs an empty interface representation of a type.
   */
  private static class EmptyMockJavaResource extends MockJavaResource {

    private final StringBuilder code = new StringBuilder();

    public EmptyMockJavaResource(Class<?> clazz) {
      super(clazz.getName());

      code.append("package ").append(clazz.getPackage().getName()).append(";\n");
      code.append("public interface ").append(clazz.getSimpleName());

      int numParams = clazz.getTypeParameters().length;
      if (numParams != 0) {
        code.append("<");
        for (int i = 0; i < numParams; i++) {
          if (i != 0) {
            code.append(",");
          }
          code.append("T").append(i);
        }
        code.append(">");
      }

      code.append("{}\n");
    }

    @Override
    public CharSequence getContent() {
      return code;
    }
  }

  /**
   * Loads the actual source of a type. This should be used only for types
   * directly tested by this test. Note that use of this class requires your
   * source files to be on your classpath.
   */
  private static class RealJavaResource extends MockJavaResource {

    public RealJavaResource(Class<?> clazz) {
      super(clazz.getName());
    }

    @Override
    public CharSequence getContent() {
      String resourceName = getTypeName().replace('.', '/') + ".java";
      InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(
          resourceName);
      assertNotNull("Could not open " + resourceName, stream);
      return Util.readStreamAsString(stream);
    }
  }

  private static TreeLogger createCompileLogger() {
    PrintWriterTreeLogger logger = new PrintWriterTreeLogger(new PrintWriter(
        System.err, true));
    logger.setMaxDetail(TreeLogger.ERROR);
    return logger;
  }

  private TreeLogger logger;

  @Override
  public void setUp() throws Exception {
    super.setUp();
    logger = createCompileLogger();
  }

  public void testEntityProxyIdNotParameterized() {
      testModelWithMethodDecl("Request<String> okMethod(EntityProxyId p);",
          "EntityProxyId must be parameterized");
  }

  public void testBadCollectionType() {
    testModelWithMethodDecl(
        "Request<SortedSet<Integer>> badReturnType();",
        "Requests that return collections may be declared with java.util.List or java.util.Set only");
  }

  public void testBadCollectionTypeNotParameterized() {
    testModelWithMethodDecl("Request<SortedSet> badReturnType();",
        "Requests that return collections of List or Set must be parameterized");
  }

  public void testBadReturnType() {
    testModelWithMethodDecl("Request<Iterable> badReturnType();",
        "Invalid Request parameterization java.lang.Iterable");
  }

  public void testDuplicateBooleanGetters() {
    testModelWithMethodDecl("Request<t.ProxyWithRepeatedGetters> method();",
        "Duplicate accessors for property foo: getFoo() and isFoo()");
  }

  public void testMissingProxyFor() {
    testModelWithMethodDeclArgs("Request<TestProxy> okMethodProxy();",
        TestContextImpl.class.getName(), null,
        "The t.TestProxy type does not have a @ProxyFor, "
            + "@ProxyForName, or @JsonRpcProxy annotation");
  }

  public void testMissingService() {
    testModelWithMethodDeclArgs("Request<String> okMethod();", null,
        TestContextImpl.class.getName(),
        "RequestContext subtype t.TestContext is missing a "
            + "@Service or @JsonRpcService annotation");
  }

  public void testModelWithMethodDecl(final String clientMethodDecls,
      String... expected) {
    testModelWithMethodDeclArgs(clientMethodDecls,
        TestContextImpl.class.getName(), TestContextImpl.class.getName(),
        expected);
  }

  public void testModelWithMethodDeclArgs(final String clientMethodDecls,
      final String serviceClass, String proxyClass, String... expected) {
    Set<Resource> javaResources = getJavaResources(proxyClass);
    javaResources.add(new MockJavaResource("t.TestRequestFactory") {
      @Override
      public CharSequence getContent() {
        StringBuilder code = new StringBuilder();
        code.append("package t;\n");
        code.append("import " + RequestFactory.class.getName() + ";\n");
        code.append("interface TestRequestFactory extends RequestFactory {\n");
        code.append("TestContext testContext();");
        code.append("}");
        return code;
      }
    });
    javaResources.add(new MockJavaResource("t.TestContext") {
      @Override
      public CharSequence getContent() {
        StringBuilder code = new StringBuilder();
        code.append("package t;\n");
        code.append("import " + EntityProxyId.class.getName() + ";\n");
        code.append("import " + Request.class.getName() + ";\n");
        code.append("import " + InstanceRequest.class.getName() + ";\n");

        code.append("import " + RequestContext.class.getName() + ";\n");
        code.append("import " + SortedSet.class.getName() + ";\n");
        code.append("import " + List.class.getName() + ";\n");
        code.append("import " + Set.class.getName() + ";\n");
        code.append("import " + Service.class.getName() + ";\n");
        code.append("import " + TestContextImpl.class.getName() + ";\n");

        if (serviceClass != null) {
          code.append("@Service(" + serviceClass + ".class)");
        }
        code.append("interface TestContext extends RequestContext {\n");
        code.append(clientMethodDecls);
        code.append("}");
        return code;
      }
    });

    CompilationState state;
    try {
      state = CompilationStateBuilder.buildFrom(logger, new CompilerContext(), javaResources);
    } catch (UnableToCompleteException e) {
        throw new RuntimeException(e);
    }

    UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder();
    builder.setLowestLogLevel(TreeLogger.ERROR);
    for (String expectedMsg : expected) {
      builder.expectError(expectedMsg, null);
    }
    builder.expectError(RequestFactoryModel.poisonedMessage(), null);
    UnitTestTreeLogger testLogger = builder.createLogger();
    try {
      new RequestFactoryModel(testLogger, state.getTypeOracle().findType(
          "t.TestRequestFactory"));
      fail("Should have complained");
    } catch (UnableToCompleteException e) {
    }
    testLogger.assertCorrectLogEntries();
  }

  private Set<Resource> getJavaResources(final String proxyClass) {
    MockJavaResource[] javaFiles = {new MockJavaResource("t.AddressProxy") {
      @Override
      public CharSequence getContent() {
        StringBuilder code = new StringBuilder();
        code.append("package t;\n");
        code.append("import " + ProxyFor.class.getName() + ";\n");
        code.append("import " + EntityProxy.class.getName() + ";\n");
        if (proxyClass != null) {
          code.append("@ProxyFor(" + proxyClass + ".class)");
        }
        code.append("interface TestProxy extends EntityProxy {\n");
        code.append("}");
        System.out.println(code);
        return code;
      }
    }, new MockJavaResource("t.ProxyWithRepeatedGetters") {
      @Override
      public CharSequence getContent() {
        StringBuilder code = new StringBuilder();
        code.append("package t;\n");
        code.append("import " + ProxyFor.class.getName() + ";\n");
        code.append("import " + EntityProxy.class.getName() + ";\n");
        if (proxyClass != null) {
          code.append("@ProxyFor(" + proxyClass + ".class)");
        }
        code.append("interface ProxyWithRepeatedGetters extends EntityProxy {\n");
        code.append("  boolean getFoo();");
        code.append("  boolean isFoo();");
        code.append("}");
        return code;
      }
    }, new MockJavaResource("java.util.Set") {
        // Tests a Driver interface that extends more than RFED
      @Override
      public CharSequence getContent() {
        StringBuilder code = new StringBuilder();
        code.append("package java.util;\n");
        code.append("public interface Set<T> extends Collection<T> {\n");
        code.append("}");
        return code;
      }
    }, new MockJavaResource("java.util.SortedSet") {
        // Tests a Driver interface that extends more than RFED
      @Override
      public CharSequence getContent() {
        StringBuilder code = new StringBuilder();
        code.append("package java.util;\n");
        code.append("public interface SortedSet<T> extends Set<T> {\n");
        code.append("}");
        return code;
      }
    }};

    Set<Resource> toReturn = new HashSet<Resource>(Arrays.asList(javaFiles));

    toReturn.addAll(Arrays.asList(new Resource[] {
        new EmptyMockJavaResource(Iterable.class),
        new EmptyMockJavaResource(EntityProxy.class),
        new EmptyMockJavaResource(EntityProxyId.class),
        new EmptyMockJavaResource(InstanceRequest.class),
        new EmptyMockJavaResource(Locator.class),
        new EmptyMockJavaResource(RequestFactory.class),
        new EmptyMockJavaResource(Receiver.class),
        new EmptyMockJavaResource(ServiceLocator.class),
        new EmptyMockJavaResource(Splittable.class),
        new EmptyMockJavaResource(ValueProxy.class),

        new RealJavaResource(Request.class),
        new RealJavaResource(Service.class),
        new RealJavaResource(ProxyFor.class),
        new EmptyMockJavaResource(RequestContext.class),}));
    toReturn.addAll(Arrays.asList(JavaResourceBase.getStandardResources()));
    return toReturn;
  }
}
