/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * 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.intellij.codeInsight.actions;

import com.intellij.lang.LanguageFormatting;
import com.intellij.lang.LanguageImportStatements;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.components.impl.ComponentManagerImpl;
import com.intellij.openapi.fileTypes.PlainTextLanguage;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.actions.VcsContextFactory;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.SimpleContentRevision;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.testFramework.LightPlatformTestCase;
import com.intellij.testFramework.TestActionEvent;
import com.intellij.testFramework.vcs.MockChangeListManager;
import com.intellij.testFramework.vcs.MockVcsContextFactory;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.picocontainer.MutablePicoContainer;

import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class ReformatOnlyVcsChangedTextTest extends LightPlatformTestCase {
  private static final String TEMP_DIR_NAME = "dir";
  private PsiDirectory myWorkingDirectory;

  private MockChangeListManager myMockChangeListManager;
  private MockCodeStyleManager myMockCodeStyleManager;
  private MockPlainTextFormattingModelBuilder myMockPlainTextFormattingModelBuilder;
  private MockPlainTextImportOptimizer myMockPlainTextImportOptimizer;

  private ChangeListManager myRealChangeListManager;
  private CodeStyleManager myRealCodeStyleManger;
  private VcsContextFactory myRealVcsContextFactory;

  private final static String COMMITTED =
    "class Test {\n" +
    "          int a      =       22;\n" +
    "        public String getName() { return \"Test\"; }\n" +
    "}";

  private final static String MODIFIED =
    "class Test {\n" +
    "          int a      =       22;\n" +
    "              long l;\n" +
    "              double d;\n" +
    "              int i;\n" +
    "        public String getName() { return \"Test\"; }\n" +
    "            String test1;\n" +
    "            String test2;\n" +
    "}";

  private final static ChangedLines[] CHANGED_LINES = new ChangedLines[] { line(2, 4), line(6, 7) };
  private final static ChangedLines[] NO_CHANGED_LINES = new ChangedLines[0];

  @Override
  public void setUp() throws Exception {
    super.setUp();
    myWorkingDirectory = TestFileStructure.createDirectory(getProject(), getSourceRoot(), TEMP_DIR_NAME);

    myRealChangeListManager = ChangeListManager.getInstance(getProject());
    myMockChangeListManager = new MockChangeListManager();
    registerChangeListManager(myMockChangeListManager);

    myRealCodeStyleManger = CodeStyleManager.getInstance(getProject());
    myMockCodeStyleManager = new MockCodeStyleManager();
    registerCodeStyleManager(myMockCodeStyleManager);

    myRealVcsContextFactory = ServiceManager.getService(VcsContextFactory.class);
    registerVcsContextFactory(new MockVcsContextFactory(getSourceRoot().getFileSystem()));

    myMockPlainTextFormattingModelBuilder = new MockPlainTextFormattingModelBuilder();
    LanguageFormatting.INSTANCE.addExplicitExtension(PlainTextLanguage.INSTANCE, myMockPlainTextFormattingModelBuilder);
    
    myMockPlainTextImportOptimizer = new MockPlainTextImportOptimizer();
    LanguageImportStatements.INSTANCE.addExplicitExtension(PlainTextLanguage.INSTANCE, myMockPlainTextImportOptimizer);
  }

  @Override
  public void tearDown() throws Exception {
    try {
      registerChangeListManager(myRealChangeListManager);
      registerCodeStyleManager(myRealCodeStyleManger);
      registerVcsContextFactory(myRealVcsContextFactory);
      LanguageFormatting.INSTANCE.removeExplicitExtension(PlainTextLanguage.INSTANCE, myMockPlainTextFormattingModelBuilder);
      LanguageImportStatements.INSTANCE.removeExplicitExtension(PlainTextLanguage.INSTANCE, myMockPlainTextImportOptimizer);

      TestFileStructure.delete(myWorkingDirectory.getVirtualFile());
    } finally {
      super.tearDown();
    }
  }

  public void testInsertion() throws IOException {
    doTest(
      "public class B {\n" +
      "       int a = 3;\n" +
      "                 String text;\n" +
      "                               Object last = null;\n" +
      "}",

      "public class B {\n" +
      "       int a = 3;\n" +
      "                               int toIndent1 = 1;\n" +
      "                 String text;\n" +
      "                               int toIndent2\n" +
      "                               Object last = null;\n" +
      "}",

      line(2, 2), line(4, 4)
    );
  }

  private static ChangedLines line(int from, int to) {
    return new ChangedLines(from, to);
  }

  public void testDeletion() throws IOException {
    doTest(
      "public class B {\n" +
      "           int a = 3;\n" +
      "           String text;\n" +
      "           Object last = null;\n" +
      "           Object first = null;\n" +
      "           Object second = null;\n" +
      "}",

      "public class B {\n" +
      "           int newInt = 1;\n" +
      "           Object last = null;\n" +
      "}",

      line(1, 1)
    );
  }

  public void testNoReformatOn_DeletionModification() throws IOException {
    doTest(
      "public class B {\n" +
      "           int a = 3;\n" +
      "           String text;\n" +
      "           Object last = null;\n" +
      "           Object first = null;\n" +
      "           Object second = null;\n" +
      "}",

      "public class B {\n" +
      "           int a = 3;\n" +
      "           String text;\n" +
      "           Object last = null;\n" +
      "}"
    );
  }

  public void testModification() throws IOException {
    doTest(
      "public class B {\n" +
      "           int a = 3;\n" +
      "           String text;\n" +
      "           Object last = null;\n" +
      "           Object first = null;\n" +
      "           Object second = null;\n" +
      "}",

      "public class B {\n" +
      "           int a = 33;\n" +
      "           String text;\n" +
      "           Object last = new Object();\n" +
      "           Object first = null;\n" +
      "           Object second = new Object();\n" +
      "}",

      line(1, 1), line(3,3), line(5,5)
    );
  }

  public void testModificationCRLF() throws IOException {
    doTest(
      "public class B {\r\n" +
      "           int a = 3;\r\n" +
      "           String text;\r\n" +
      "           Object last = null;\r\n" +
      "           Object first = null;\r\n" +
      "           Object second = null;\r\n" +
      "}",

      "public class B {\r\n" +
      "           int a = 33;\r\n" +
      "           String text;\r\n" +
      "           Object last = new Object();\r\n" +
      "           Object first = null;\r\n" +
      "           Object second = new Object();\r\n" +
      "}",

      line(1, 1), line(3,3), line(5,5)
    );
  }

  public void testReformatFiles() throws IOException {
    ChangedFilesStructure fs = new ChangedFilesStructure(myWorkingDirectory);

    PsiFile m1 = fs.createFile("Test1.java", COMMITTED, MODIFIED);
    PsiFile u1 = fs.createFile("Test2.java", COMMITTED, COMMITTED);

    fs.createDirectoryAndMakeItCurrent();
    PsiFile m2 = fs.createFile("Test3.java", COMMITTED, MODIFIED);
    PsiFile u2 = fs.createFile("Test4.java", COMMITTED, COMMITTED);

    new ReformatCodeProcessor(getProject(), new PsiFile[] {m1, m2, u1, u2}, null, true).run();

    assertFormattedLines(CHANGED_LINES, m1, m2);
    assertFormattedLines(NO_CHANGED_LINES, u1, u2);
  }

  public void testNoChangesNoFormatting() throws IOException {
    ChangedFilesStructure fs = new ChangedFilesStructure(myWorkingDirectory);

    PsiFile u1 = fs.createFile("Test1.java", COMMITTED, COMMITTED);
    PsiFile u2 = fs.createFile("Test2.java", COMMITTED, COMMITTED);

    reformatDirectory(fs.getCurrentDirectory());

    assertFormattedLines(NO_CHANGED_LINES, u1, u2);
  }


  public void testReformatOnlyChanged() throws IOException {
    ChangedFilesStructure fs = new ChangedFilesStructure(myWorkingDirectory);

    PsiFile untouched1 = fs.createFile("Test1.java", COMMITTED, COMMITTED);
    PsiFile untouched2 = fs.createFile("Test2.java", COMMITTED, COMMITTED);

    PsiFile modified1 = fs.createFile("Test4.java", COMMITTED, MODIFIED);
    PsiFile modified2 = fs.createFile("Test5.java", COMMITTED, MODIFIED);
    PsiFile modified3 = fs.createFile("Test6.java", COMMITTED, MODIFIED);

    reformatDirectory(fs.getCurrentDirectory());


    assertFormattedLines(CHANGED_LINES, modified1, modified2, modified3);
    assertFormattedLines(NO_CHANGED_LINES, untouched1, untouched2);
  }

  public void testReformatInAllSubtree() throws IOException {
    ChangedFilesStructure fs = new ChangedFilesStructure(myWorkingDirectory);
    PsiFile modified11 = fs.createFile("Test4.java", COMMITTED, MODIFIED);
    PsiFile modified12 = fs.createFile("Test5.java", COMMITTED, MODIFIED);

    fs.createDirectoryAndMakeItCurrent();
    PsiDirectory dirToReformat = fs.getCurrentDirectory();
    PsiFile modified21 = fs.createFile("Test6.java", COMMITTED, MODIFIED);
    PsiFile modified22 = fs.createFile("Test7.java", COMMITTED, MODIFIED);

    fs.createDirectoryAndMakeItCurrent();
    PsiFile modified31 = fs.createFile("Test8.java", COMMITTED, MODIFIED);
    PsiFile modified32 = fs.createFile("Test9.java", COMMITTED, MODIFIED);

    reformatDirectory(dirToReformat);

    assertFormattedLines(CHANGED_LINES, modified21, modified22, modified31, modified32);
    assertFormattedLines(NO_CHANGED_LINES, modified11, modified12);
  }

  public void testOptimizeImportsInModule() throws IOException {
    ChangedFilesStructure fs = new ChangedFilesStructure(myWorkingDirectory);

    String initialFile = "initial file";
    PsiFile toModify = fs.createFile("Modified.txt", initialFile, initialFile  + " + unused import");
    PsiFile toModify2 = fs.createFile("Modified2.txt", initialFile, initialFile + " + another unused import");

    PsiFile toKeep = fs.createFile("NonModified.txt", initialFile, initialFile);
    PsiFile toKeep2 = fs.createFile("NonModified2.txt", initialFile, initialFile);

    OptimizeImportsAction optimizeImports = new OptimizeImportsAction();
    OptimizeImportsAction.setProcessVcsChangedFilesInTests(true);
    try {
      optimizeImports.actionPerformed(new TestActionEvent(dataId -> {
        if (CommonDataKeys.PROJECT.is(dataId)) {
          return getProject();
        }
        if (LangDataKeys.MODULE_CONTEXT.is(dataId)) {
          return getModule();
        }
        return null;
      }));
    }
    finally {
      OptimizeImportsAction.setProcessVcsChangedFilesInTests(false);
    }
    
    assertTrue(isImportsOptimized(toModify));
    assertTrue(isImportsOptimized(toModify2));
    assertTrue(!isImportsOptimized(toKeep));
    assertTrue(!isImportsOptimized(toKeep2));
  }

  private static void registerChangeListManager(@NotNull ChangeListManager manager) {
    Project project = getProject();
    assert (project instanceof ComponentManagerImpl);
    ComponentManagerImpl projectComponentManager = (ComponentManagerImpl)project;
    projectComponentManager.registerComponentInstance(ChangeListManager.class, manager);
  }

  private static void registerCodeStyleManager(@NotNull CodeStyleManager manager) {
    String componentKey = CodeStyleManager.class.getName();
    MutablePicoContainer container = (MutablePicoContainer)getProject().getPicoContainer();
    container.unregisterComponent(componentKey);
    container.registerComponentInstance(componentKey, manager);
  }

  private static void registerVcsContextFactory(@NotNull VcsContextFactory factory) {
    String key = VcsContextFactory.class.getName();
    MutablePicoContainer container = (MutablePicoContainer)ApplicationManager.getApplication().getPicoContainer();
    container.unregisterComponent(key);
    container.registerComponentInstance(key, factory);
  }

  private void doTest(@NotNull String committed, @NotNull String modified, @NotNull ChangedLines... lines) throws IOException {
    ChangedFilesStructure fs = new ChangedFilesStructure(myWorkingDirectory);
    PsiFile file = fs.createFile("Test.java", committed, modified);
    reformatDirectory(myWorkingDirectory);
    assertFormattedRangesEqualsTo(file, lines);
  }

  private void assertFormattedLines(@NotNull ChangedLines[] expectedLines, @NotNull PsiFile... files) {
    for (PsiFile file : files)
      assertFormattedRangesEqualsTo(file, expectedLines);
  }

  private void assertFormattedRangesEqualsTo(@NotNull PsiFile file, ChangedLines... expected) {
    ChangedLines[] formatted = myMockCodeStyleManager.getFormattedLinesFor(file);

    Comparator<ChangedLines> cmp = (o1, o2) -> o1.from < o2.from ? -1 : 1;
    Arrays.sort(expected, cmp);
    Arrays.sort(formatted, cmp);

    assertTrue(getErrorMessage(expected, formatted), Arrays.equals(expected, formatted));
  }
  
  private boolean isImportsOptimized(@NotNull PsiFile file) {
    return myMockPlainTextImportOptimizer.getProcessedFiles().contains(file);
  }

  @NotNull
  private static String getErrorMessage(ChangedLines[] expected, ChangedLines[] actual) {
    return "Expected: " + Arrays.toString(expected) + " Actual: " + Arrays.toString(actual);
  }

  private static void reformatDirectory(@NotNull PsiDirectory dir) {
    ReformatCodeProcessor processor = new ReformatCodeProcessor(getProject(), dir, true, true);
    processor.run();
  }

  class ChangedFilesStructure {
    private TestFileStructure myFileStructure;

    public ChangedFilesStructure(@NotNull PsiDirectory directory) {
      myFileStructure = new TestFileStructure(getModule(), directory);
    }

    public void createDirectoryAndMakeItCurrent() throws IOException {
      myFileStructure.createDirectoryAndMakeItCurrent("inner");
    }

    @NotNull
    public PsiFile createFile(@NotNull String fileName,
                              @NotNull String committedContent,
                              @NotNull String actualContent) throws IOException
    {
      PsiFile file = myFileStructure.addTestFile(fileName, actualContent);
      if (committedContent != actualContent) {
        registerCommittedRevision(committedContent, file);
      }
      return file;
    }

    private void registerCommittedRevision(@NotNull String committedContent, @NotNull PsiFile... files) {
      List<Change> changes = createChanges(committedContent, files);
      injectChanges(changes);
    }

    @NotNull
    private List<Change> createChanges(@NotNull String committed, @NotNull PsiFile... files) {
      List<Change> changes = ContainerUtil.newArrayList();
      for (PsiFile file : files) {
        changes.add(createChange(committed, file));
      }
      return changes;
    }

    private void injectChanges(@NotNull List<Change> changes) {
      Change[] arr = new Change[changes.size()];
      ContainerUtil.toArray(changes, arr);
      myMockChangeListManager.addChanges(arr);
    }

    @NotNull
    private Change createChange(@NotNull String committed, @NotNull PsiFile file) {
      FilePath filePath = VcsUtil.getFilePath(file.getVirtualFile());
      ContentRevision before = new SimpleContentRevision(committed, filePath, "");
      ContentRevision after = new SimpleContentRevision("", filePath, "");
      return new Change(before, after);
    }

    @NotNull
    public PsiDirectory getCurrentDirectory() {
      return myFileStructure.getCurrentDirectory();
    }
  }
}

