/*
 * 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.gwt.core.ext.linker.impl;

import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.Artifact;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.ConfigurationProperty;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.SelectionProperty;
import com.google.gwt.core.ext.linker.Shardable;
import com.google.gwt.core.ext.linker.StatementRanges;
import com.google.gwt.thirdparty.guava.common.collect.Lists;

import junit.framework.TestCase;

import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * A regular JUnit test case for {@link SelectionScriptLinker}.
 */
public class SelectionScriptLinkerUnitTest extends TestCase {
  private static class MockLinkerContext implements LinkerContext {

    @Override
    public SortedSet<ConfigurationProperty> getConfigurationProperties() {
      return new TreeSet<ConfigurationProperty>();
    }

    @Override
    public String getModuleFunctionName() {
      return MOCK_MODULE_NAME;
    }

    @Override
    public long getModuleLastModified() {
      return MOCK_MODULE_LAST_MODIFIED;
    }

    @Override
    public String getModuleName() {
      return MOCK_MODULE_NAME;
    }

    @Override
    public SortedSet<SelectionProperty> getProperties() {
      return new TreeSet<SelectionProperty>();
    }

    @Override
    public boolean isOutputCompact() {
      return true;
    }

    @Override
    public String optimizeJavaScript(TreeLogger logger, String jsProgram) {
      return jsProgram;
    }
  }

  private static class NonShardableSelectionScriptLinker extends
      SelectionScriptLinker {
    @Override
    public String getDescription() {
      return getClass().getName();
    }

    @Override
    protected String getCompilationExtension(TreeLogger logger,
        LinkerContext context) {
      return ".js";
    }

    @Override
    protected String getModulePrefix(TreeLogger logger, LinkerContext context,
        String strongName) {
      return "MODULE_PREFIX";
    }

    @Override
    protected String getModuleSuffix2(TreeLogger logger, LinkerContext context, String strongName) {
      return "MODULE_SUFFIX";
    }

    @Override
    protected String getSelectionScriptTemplate(TreeLogger logger,
        LinkerContext context) {
      return SelectionScriptLinkerUnitTest.class.getPackage().getName().replace(
          '.', '/')
          + "/MockTemplate.js";
    }
  }

  @Shardable
  private static class ShardableSelectionScriptLinker extends
      NonShardableSelectionScriptLinker {
  }

  private static final long MOCK_MODULE_LAST_MODIFIED = 1234;

  private static final String MOCK_MODULE_NAME = "test";

  private static byte[] getBytes(String string) {
    try {
      return string.getBytes("UTF-8");
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Test that running a non-shardable linker in simulated sharding mode does
   * not lose the permutations.
   */
  public void testNonShardableHasPermutations()
      throws UnableToCompleteException {
    ArtifactSet artifacts = new ArtifactSet();

    StandardCompilationResult result = createCompilationResult();
    artifacts.add(result);

    ArtifactSet updated = new NonShardableSelectionScriptLinker().link(
        TreeLogger.NULL, new MockLinkerContext(), artifacts);

    SortedSet<SelectionInformation> selectionInfos = updated.find(SelectionInformation.class);
    assertEquals(1, selectionInfos.size());
  }

  /**
   * Test timestamps on the selection script. For Development Mode, it should match
   * the module's timestamp. For Production Mode, it should be current.
   */
  public void testTimestampOnSelectionScript() throws UnableToCompleteException {
    // Development Mode
    {
      ArtifactSet artifacts = new ArtifactSet();
      ArtifactSet updated = new ShardableSelectionScriptLinker().link(
          TreeLogger.NULL, new MockLinkerContext(), artifacts, false);
      EmittedArtifact selectionScript = findSelectionScript(updated);
      assertEquals(MOCK_MODULE_LAST_MODIFIED, selectionScript.getLastModified());
    }

    // Production Mode
    {
      ArtifactSet artifacts = new ArtifactSet();
      artifacts.add(createCompilationResult());
      ArtifactSet updated = new ShardableSelectionScriptLinker().link(
          TreeLogger.NULL, new MockLinkerContext(), artifacts, true);
      updated = transferThinning(updated);
      updated = new ShardableSelectionScriptLinker().link(TreeLogger.NULL,
          new MockLinkerContext(), updated, false);
      EmittedArtifact selectionScript = findSelectionScript(updated);
      assertTrue(MOCK_MODULE_LAST_MODIFIED != selectionScript.getLastModified());
    }
  }

  private StandardCompilationResult createCompilationResult() {
    byte[][] js = new byte[][] {getBytes("code for fragment 0"), getBytes("code for fragment 1")};
    String jsStrongName = "fooStrongName";
    StatementRanges[] statementRanges = new StatementRanges[] {
        new StandardStatementRanges(Lists.newArrayList(0), Lists.newArrayList(10)),
        new StandardStatementRanges(Lists.newArrayList(0), Lists.newArrayList(20))};
    byte[] symbolMapBytes = getBytes("symbol map");

    StandardCompilationResult result = new StandardCompilationResult(
        new MockPermutationResult(js, jsStrongName, statementRanges, symbolMapBytes));
    result.addSelectionPermutation(new TreeMap<SelectionProperty, String>());
    result.addSoftPermutation(Collections.<SelectionProperty, String>emptyMap());
    return result;
  }

  private EmittedArtifact findEmittedArtifact(ArtifactSet updated,
      String partialPath) {
    for (EmittedArtifact art : updated.find(EmittedArtifact.class)) {
      if (art.getPartialPath().equals(partialPath)) {
        return art;
      }
    }
    return null;
  }

  private EmittedArtifact findSelectionScript(ArtifactSet updated) {
    return findEmittedArtifact(updated, MOCK_MODULE_NAME + ".nocache.js");
  }

  /**
   * Thin down the artifact set as if it were being transmitted back. The method
   * in Link cannot be used because it wants a StandardLinkerContext, not an
   * arbitrary LinkerContext.
   */
  @SuppressWarnings("unchecked")
  private ArtifactSet transferThinning(ArtifactSet artifacts) {
    ArtifactSet updated = new ArtifactSet();
    // The raw type Artifact is to work around a Java compiler bug:
    // http://bugs.sun.com/view_bug.do?bug_id=6548436
    for (Artifact art : artifacts) {
      if (art.isTransferableFromShards() || (art instanceof EmittedArtifact)) {
        updated.add(art);
      }
    }
    return updated;
  }
}
