/*
 * Copyright 2014 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.dev.codeserver;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.MinimalRebuildCacheManager;
import com.google.gwt.dev.codeserver.Job.Result;
import com.google.gwt.dev.javac.UnitCache;
import com.google.gwt.dev.javac.UnitCacheSingleton;
import com.google.gwt.dev.javac.testing.impl.JavaResourceBase;
import com.google.gwt.dev.javac.testing.impl.MockJavaResource;
import com.google.gwt.dev.javac.testing.impl.MockResource;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
import com.google.gwt.thirdparty.guava.common.base.Charsets;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.google.gwt.thirdparty.guava.common.collect.Lists;

import junit.framework.TestCase;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Tests for {@link Recompiler}
 */
public class RecompilerTest extends TestCase {

  private static File findCompiledJsFile(Result result) {
    File outputDir = new File(result.outputDir.getWarDir(), result.outputModuleName);
    File[] files = outputDir.listFiles(new FilenameFilter() {
      @Override
      public boolean accept(File dir, String name) {
        return name.endsWith(".js") && !name.endsWith("nocache.js");
      }
    });
    Arrays.sort(files, new Comparator<File>() {
      @Override
      public int compare(File thisFile, File thatFile) {
        return -Long.compare(thisFile.length(), thatFile.length());
      }
    });
    return files[0];
  }

  private static void writeResourcesTo(List<MockResource> resources, File dir) throws IOException {
    for (MockResource applicationResource : resources) {
      File resourceFile =
          new File(dir.getAbsolutePath() + File.separator + applicationResource.getPath());
      resourceFile.getParentFile().mkdirs();
      Files.write(resourceFile.toPath(), List.of(applicationResource.getContent()), Charsets.UTF_8);
    }
  }

  private MockJavaResource barReferencesBazResource =
      JavaResourceBase.createMockJavaResource("com.foo.Bar",
          "package com.foo;",
          "public class Bar {",
          "  Baz baz = new Baz();",
          "}");

  private MockJavaResource bazReferencesFooResource =
      JavaResourceBase.createMockJavaResource("com.foo.Baz",
          "package com.foo;",
          "public class Baz {",
          "  Foo foo = new Foo();",
          "}");

  private MockJavaResource fooResource =
      JavaResourceBase.createMockJavaResource("com.foo.Foo",
          "package com.foo;",
          "public class Foo {}");

  private MockJavaResource nonCompilableFooResource =
      JavaResourceBase.createMockJavaResource("com.foo.Foo",
          "package com.foo;",
          "import com.google.gwt.core.client.impl.SpecializeMethod;",
          "public class Foo {",
          "  // This will throw an error in UnifyAst.",
          "  @SpecializeMethod()",
          "  public void run() {}",
          "}");

  private MockJavaResource referencesBarEntryPointResource =
      JavaResourceBase.createMockJavaResource("com.foo.TestEntryPoint",
          "package com.foo;",
          "import com.google.gwt.core.client.EntryPoint;",
          "public class TestEntryPoint implements EntryPoint {",
          "  @Override",
          "  public void onModuleLoad() {",
          "    Bar bar = new Bar();",
          "  }",
          "}");

  private MockResource simpleModuleResource =
      JavaResourceBase.createMockResource("com/foo/SimpleModule.gwt.xml",
          "<module>",
          "<source path=''/>",
          "<entry-point class='com.foo.TestEntryPoint'/>",
          "</module>");

  private MockResource propertyIsFooModuleResource =
      JavaResourceBase.createMockResource("com/foo/PropertyModule.gwt.xml",
          "<module>",
          "<source path=''/>",
          "<entry-point class='com.foo.PropertyEntryPoint'/>",
          "<define-property name=\"target\" values=\"foo,bar\" />",
          "<set-property name=\"target\" value=\"foo\" />",
          "<replace-with class=\"com.foo.Foo\">",
          "  <when-type-is class=\"com.foo.Bar\"/>",
          "  <when-property-is name=\"target\" value=\"foo\"/>",
          "</replace-with>",
          "</module>");

  private MockResource propertyIsBarModuleResource =
      JavaResourceBase.createMockResource("com/foo/PropertyModule.gwt.xml",
          "<module>",
          "<source path=''/>",
          "<entry-point class='com.foo.PropertyEntryPoint'/>",
          "<define-property name=\"target\" values=\"foo,bar\" />",
          "<set-property name=\"target\" value=\"bar\" />",
          "<replace-with class=\"com.foo.Foo\">",
          "  <when-type-is class=\"com.foo.Bar\"/>",
          "  <when-property-is name=\"target\" value=\"foo\"/>",
          "</replace-with>",
          "</module>");

  private MockJavaResource performsRebindEntryPointResource =
      JavaResourceBase.createMockJavaResource("com.foo.PropertyEntryPoint",
          "package com.foo;",
          "import com.google.gwt.core.client.EntryPoint;",
          "import com.google.gwt.core.client.GWT;",
          "public class PropertyEntryPoint implements EntryPoint {",
          "  @Override",
          "  public void onModuleLoad() {",
          "    GWT.create(Bar.class);",
          "  }",
          "}");

  public void testIncrementalRecompile_compileErrorDoesntCorruptMinimalRebuildCache()
      throws UnableToCompleteException, IOException, InterruptedException {
    String moduleName = "com.foo.SimpleModule";
    PrintWriterTreeLogger logger = new PrintWriterTreeLogger();
    logger.setMaxDetail(TreeLogger.ERROR);

    File sourcePath = createTempDir();
    // Setup options to perform a per-file compile and compile the given module.
    Options options = new Options();
    options.parseArgs(new String[] {
        "-incremental", "-src", sourcePath.getAbsolutePath(), moduleName});

    // Prepare the basic resources in the test application.
    List<MockResource> originalResources = Lists.newArrayList(simpleModuleResource,
        referencesBarEntryPointResource, barReferencesBazResource, bazReferencesFooResource,
        fooResource);
    writeResourcesTo(originalResources, sourcePath);

    File baseCacheDir = createTempDir();
    UnitCache unitCache = UnitCacheSingleton.get(
        logger, null, baseCacheDir, new CompilerOptionsImpl(options));
    MinimalRebuildCacheManager minimalRebuildCacheManager =
        new MinimalRebuildCacheManager(logger, baseCacheDir, ImmutableMap.<String, String>of());
    Recompiler recompiler = new Recompiler(OutboxDir.create(createTempDir(), logger), null,
        moduleName, options, unitCache, minimalRebuildCacheManager);
    Outbox outbox = new Outbox("Transactional Cache", recompiler, options, logger);
    OutboxTable outboxTable = new OutboxTable();
    outboxTable.addOutbox(outbox);
    JobRunner runner = new JobRunner(new JobEventTable(), minimalRebuildCacheManager);

    // Perform a first compile. This should pass since all resources are valid.
    Result result =
        compileWithChanges(logger, runner, outbox, sourcePath, Lists.<MockResource> newArrayList());
    assertTrue(result.isOk());

    // Recompile should fail since the provided Foo is not compilable.
    result = compileWithChanges(logger, runner, outbox, sourcePath,
        Lists.<MockResource> newArrayList(nonCompilableFooResource));
    assertFalse(result.isOk());

    // Recompile with a modified entry point. This should fail again since Foo is still
    // bad, but if transactionality protection failed on the minimalRebuildCache this compile will
    // succeed because it will think that it has "already processed" Foo.
    result = compileWithChanges(logger, runner, outbox, sourcePath,
        Lists.<MockResource> newArrayList(referencesBarEntryPointResource));
    assertFalse(result.isOk());
  }

  public void testIncrementalRecompile_modulePropertyEditsWork() throws UnableToCompleteException,
      IOException, InterruptedException {
    String moduleName = "com.foo.PropertyModule";
    PrintWriterTreeLogger logger = new PrintWriterTreeLogger();
    logger.setMaxDetail(TreeLogger.ERROR);

    File sourcePath = createTempDir();
    // Setup options to perform a per-file compile and compile the given module.
    Options options = new Options();
    options.parseArgs(new String[] {
        "-incremental", "-src", sourcePath.getAbsolutePath(), moduleName});

    // Prepare the basic resources in the test application.
    List<MockResource> originalResources = Lists.newArrayList(propertyIsFooModuleResource,
        performsRebindEntryPointResource, barReferencesBazResource, bazReferencesFooResource,
        fooResource);
    writeResourcesTo(originalResources, sourcePath);

    File baseCacheDir = createTempDir();
    UnitCache unitCache = UnitCacheSingleton.get(
        logger, null, baseCacheDir, new CompilerOptionsImpl(options));
    MinimalRebuildCacheManager minimalRebuildCacheManager =
        new MinimalRebuildCacheManager(logger, baseCacheDir, ImmutableMap.<String, String>of());
    Recompiler recompiler = new Recompiler(OutboxDir.create(createTempDir(), logger), null,
        moduleName, options, unitCache, minimalRebuildCacheManager);
    Outbox outbox = new Outbox("Transactional Cache", recompiler, options, logger);
    OutboxTable outboxTable = new OutboxTable();
    outboxTable.addOutbox(outbox);
    JobRunner runner = new JobRunner(new JobEventTable(), minimalRebuildCacheManager);

    // Perform a first compile with configuration to rebind Bar to Foo.
    Result result =
        compileWithChanges(logger, runner, outbox, sourcePath, Lists.<MockResource> newArrayList());
    assertTrue(result.isOk());
    File compiledJsFile1 = findCompiledJsFile(result);

    // Perform a second compile with a changed property value that will result in NOT rebinding Bar
    // to Foo.
    result = compileWithChanges(logger, runner, outbox, sourcePath,
        Lists.<MockResource> newArrayList(propertyIsBarModuleResource));
    assertTrue(result.isOk());
    File compiledJsFile2 = findCompiledJsFile(result);

    // The compiled Js files are different files on disk and their contents are not the same as
    // evidenced by their names being different (the names are a hash of content).
    assertFalse(compiledJsFile1.equals(compiledJsFile2));
    assertFalse(compiledJsFile1.getName().equals(compiledJsFile2.getName()));
  }

  @Override
  protected void setUp() throws Exception {
    super.setUp();
    // Make sure we're using a MemoryUnitCache.
    System.setProperty(UnitCacheSingleton.GWT_PERSISTENTUNITCACHE, "false");
  }

  private Result compileWithChanges(TreeLogger logger, JobRunner runner, Outbox outbox,
      File sourcePath, List<MockResource> changedResources) throws InterruptedException,
      IOException {
    // Wait 1 second so that any new file modification times are actually different.
    Thread.sleep(1001);

    // Write the Java/XML/etc resources that make up the test application.
    writeResourcesTo(changedResources, sourcePath);

    // Compile and return success status.
    Map<String, String> bindingProperties = new HashMap<String, String>();
    Job job = outbox.makeJob(bindingProperties, logger);
    runner.submit(job);
    return job.waitForResult();
  }

  private static File createTempDir() throws IOException {
    return Files.createTempDirectory("RecompilerTest").toFile();
  }
}
