/*
 * Copyright 2014 The Closure Compiler Authors.
 *
 * 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.javascript.refactoring;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.SourceFile;
import com.google.javascript.rhino.Node;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.List;
import java.util.Map;

/**
 * Test case for {@link ApplySuggestedFixes}.
 */
@RunWith(JUnit4.class)
public class ApplySuggestedFixesTest {

  @Test
  public void testApplyCodeReplacements_overlapsAreErrors() throws Exception {
    List<CodeReplacement> replacements = ImmutableList.of(
        new CodeReplacement(0, 10, ""),
        new CodeReplacement(5, 3, ""));
    try {
      ApplySuggestedFixes.applyCodeReplacements(replacements, "");
      fail("Overlaps in Code replacements should be an error.");
    } catch (IllegalArgumentException expected) {}
  }

  @Test
  public void testApplyCodeReplacements_noOverlapsSucceed() throws Exception {
    List<CodeReplacement> replacements = ImmutableList.of(
        new CodeReplacement(0, 3, ""),
        new CodeReplacement(5, 3, ""));
    ApplySuggestedFixes.applyCodeReplacements(replacements, "abcdef");
  }

  @Test
  public void testApplyCodeReplacements() throws Exception {
    List<CodeReplacement> replacements = ImmutableList.of(
        new CodeReplacement(0, 1, "z"),
        new CodeReplacement(3, 2, "qq"));
    String newCode = ApplySuggestedFixes.applyCodeReplacements(replacements, "abcdef");
    assertEquals("zbcqqf", newCode);
  }

  @Test
  public void testApplyCodeReplacements_insertion() throws Exception {
    List<CodeReplacement> replacements = ImmutableList.of(new CodeReplacement(0, 0, "z"));
    String newCode = ApplySuggestedFixes.applyCodeReplacements(replacements, "abcdef");
    assertEquals("zabcdef", newCode);
  }

  @Test
  public void testApplyCodeReplacements_deletion() throws Exception {
    List<CodeReplacement> replacements = ImmutableList.of(new CodeReplacement(0, 6, ""));
    String newCode = ApplySuggestedFixes.applyCodeReplacements(replacements, "abcdef");
    assertThat(newCode).isEmpty();
  }

  @Test
  public void testApplyCodeReplacements_boundaryCases() throws Exception {
    List<CodeReplacement> replacements = ImmutableList.of(new CodeReplacement(5, 1, "z"));
    String newCode = ApplySuggestedFixes.applyCodeReplacements(replacements, "abcdef");
    assertEquals("abcdez", newCode);
  }

  @Test
  public void testApplyCodeReplacements_multipleReplacements() throws Exception {
    List<CodeReplacement> replacements = ImmutableList.of(
        new CodeReplacement(0, 2, "z"),
        new CodeReplacement(2, 1, "y"),
        new CodeReplacement(3, 3, "xwvu"));
    String newCode = ApplySuggestedFixes.applyCodeReplacements(replacements, "abcdef");
    assertEquals("zyxwvu", newCode);
  }

  @Test
  public void testApplySuggestedFixes() throws Exception {
    String code = "var someNode;";
    Compiler compiler = getCompiler(code);
    Node root = compileToScriptRoot(compiler);
    List<SuggestedFix> fixes = ImmutableList.of(new SuggestedFix.Builder().delete(root).build());
    Map<String, String> codeMap = ImmutableMap.of("test", code);
    Map<String, String> newCodeMap = ApplySuggestedFixes.applySuggestedFixesToCode(
        fixes, codeMap);
    assertThat(newCodeMap).hasSize(1);
    assertThat(newCodeMap).containsEntry("test", "");
  }

  @Test
  public void testApplySuggestedFixes_insideJSDoc() throws Exception {
    String code = "/** @type {Foo} */\nvar foo = new Foo()";
    Compiler compiler = getCompiler(code);
    Node root = compileToScriptRoot(compiler);
    Node varNode = root.getFirstChild();
    Node jsdocRoot =
        Iterables.getOnlyElement(varNode.getJSDocInfo().getTypeNodes());
    SuggestedFix fix = new SuggestedFix.Builder()
        .insertBefore(jsdocRoot, "!")
        .build();
    List<SuggestedFix> fixes = ImmutableList.of(fix);
    Map<String, String> codeMap = ImmutableMap.of("test", code);
    Map<String, String> newCodeMap = ApplySuggestedFixes.applySuggestedFixesToCode(
        fixes, codeMap);
    assertThat(newCodeMap).hasSize(1);
    assertThat(newCodeMap).containsEntry("test", "/** @type {!Foo} */\nvar foo = new Foo()");
  }

  @Test
  public void testApplySuggestedFixes_multipleFixesInJsdoc() throws Exception {
    String code = "/** @type {Array<Foo>} */\nvar arr = [new Foo()];";
    Compiler compiler = getCompiler(code);
    Node root = compileToScriptRoot(compiler);
    Node varNode = root.getFirstChild();
    Node jsdocRoot =
        Iterables.getOnlyElement(varNode.getJSDocInfo().getTypeNodes());
    SuggestedFix fix1 = new SuggestedFix.Builder()
        .insertBefore(jsdocRoot, "!")
        .build();
    Node foo = jsdocRoot.getFirstFirstChild();
    SuggestedFix fix2 = new SuggestedFix.Builder()
        .insertBefore(foo, "!")
        .build();
    List<SuggestedFix> fixes = ImmutableList.of(fix1, fix2);
    Map<String, String> codeMap = ImmutableMap.of("test", code);
    Map<String, String> newCodeMap = ApplySuggestedFixes.applySuggestedFixesToCode(
        fixes, codeMap);
    assertThat(newCodeMap).hasSize(1);
    assertThat(newCodeMap)
        .containsEntry("test", "/** @type {!Array<!Foo>} */\nvar arr = [new Foo()];");
  }

  @Test
  public void testApplySuggestedFixes_noFixes() throws Exception {
    Map<String, String> codeMap = ImmutableMap.of(
        "file1", "abcdef",
        "file2", "abcdef");
    Map<String, String> expectedNewCodeMap = ImmutableMap.of();
    List<SuggestedFix> fixes = ImmutableList.of();
    Map<String, String> newCodeMap = ApplySuggestedFixes.applySuggestedFixesToCode(
        fixes, codeMap);
    assertEquals(expectedNewCodeMap, newCodeMap);
  }

  @Test
  public void testApplySuggestedFixes_missingCodeForFile() throws Exception {
    Map<String, String> codeMap = ImmutableMap.of();
    String code = "var someNode;";
    Compiler compiler = getCompiler(code);
    Node root = compileToScriptRoot(compiler);
    List<SuggestedFix> fixes = ImmutableList.of(new SuggestedFix.Builder().delete(root).build());
    try {
      ApplySuggestedFixes.applySuggestedFixesToCode(fixes, codeMap);
      fail("applySuggestedFixesToCode should have failed since file is missing from code map.");
    } catch (IllegalArgumentException expected) {}
  }

  /**
   * Returns the root script node produced from the compiled JS input.
   */
  private Node compileToScriptRoot(Compiler compiler) {
    Node root = compiler.getRoot();
    // The last child of the compiler root is a Block node, and the first child
    // of that is the Script node.
    return root.getLastChild().getFirstChild();
  }

  private Compiler getCompiler(String jsInput) {
    Compiler compiler = new Compiler();
    CompilerOptions options = RefactoringDriver.getCompilerOptions();
    compiler.init(
        ImmutableList.<SourceFile>of(), // Externs
        ImmutableList.of(SourceFile.fromCode("test", jsInput)),
        options);
    compiler.parse();
    return compiler;
  }
}
