/*
 * Copyright 2000-2015 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.jetbrains.python.refactoring;

import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.fixtures.PyTestCase;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.refactoring.move.PyMoveSymbolDelegate;
import com.jetbrains.python.refactoring.move.makeFunctionTopLevel.PyMakeLocalFunctionTopLevelProcessor;
import com.jetbrains.python.refactoring.move.makeFunctionTopLevel.PyMakeMethodTopLevelProcessor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;

/**
 * @author Mikhail Golubev
 */
public class PyMakeFunctionTopLevelTest extends PyTestCase {

  public void doTest(@Nullable String errorMessage) {
    myFixture.configureByFile(getTestName(true) + ".py");
    runRefactoring(null, errorMessage);
    if (errorMessage == null) {
      myFixture.checkResultByFile(getTestName(true) + ".after.py");
    }
  }

  private void doTestSuccess() {
    doTest(null);
  }

  private void doTestFailure(@NotNull String message) {
    doTest(message);
  }

  private void runRefactoring(@Nullable String destination, @Nullable String errorMessage) {
    final PyFunction function = assertInstanceOf(myFixture.getElementAtCaret(), PyFunction.class);
    if (destination == null) {
      destination = PyPsiUtils.getContainingFilePath(function);
    }
    else {
      final VirtualFile srcRoot = ModuleRootManager.getInstance(myFixture.getModule()).getSourceRoots()[0];
      destination = FileUtil.join(srcRoot.getPath(), destination);
    }
    assertNotNull(destination);
    final String finalDestination = destination;
    try {
      if (function.getContainingClass() != null) {
        new PyMakeMethodTopLevelProcessor(function, finalDestination).run();
      }
      else {
        new PyMakeLocalFunctionTopLevelProcessor(function, finalDestination).run();
      }
    }
    catch (IncorrectOperationException e) {
      if (errorMessage == null) {
        fail("Refactoring failed unexpectedly with message: " + e.getMessage());
      }
      assertEquals(errorMessage, e.getMessage());
    }
  }

  private void doMultiFileTest(@Nullable String destination, @Nullable String errorMessage) throws IOException {
    doMultifileTest("main.py", destination, errorMessage);
  }

  private boolean isActionEnabled() {
    final int offset = myFixture.getCaretOffset();
    PsiElement element = myFixture.getFile().findElementAt(offset);

    // Duplicates the logic of MoveHandler#invoke(), since it doesn't allow to check
    // whether MoveHandlerDelegate#tryMove() returns true or false without actually
    // invoking it.
    while (element != null) {
      if (PyMoveSymbolDelegate.isMovableLocalFunctionOrMethod(element)) {
        return true;
      }
      final TextRange range = element.getTextRange();
      if (range != null) {
        final int relative = offset - range.getStartOffset();
        final PsiReference reference = element.findReferenceAt(relative);
        if (reference != null) {
          final PsiElement refElement = reference.resolve();
          if (refElement != null && PyMoveSymbolDelegate.isMovableLocalFunctionOrMethod(myFixture.getElementAtCaret())) {
            return true;
          }
        }
      }
      element = element.getParent();
    }
    return false;
  }

  private void doMultifileTest(@NotNull String pathToOriginFunction, @Nullable String destination, @Nullable String errorMessage) throws IOException {
    final String rootBeforePath = getTestName(true) + "/before";
    final String rootAfterPath = getTestName(true) + "/after";
    final VirtualFile copiedDirectory = myFixture.copyDirectoryToProject(rootBeforePath, "");
    myFixture.configureByFile(pathToOriginFunction);
    runRefactoring(destination, errorMessage);
    if (errorMessage == null) {
      PlatformTestUtil.assertDirectoriesEqual(getVirtualFileByName(getTestDataPath() + rootAfterPath), copiedDirectory);
    }
  }

  //PY-44858
  public void testRefactoringNotCreateInitInAnotherDir() throws IOException {
    String pathToOriginFunction = "mypkg/a/main.py";
    String destination = "mypkg/b/other.py";
    doMultifileTest(pathToOriginFunction, destination, null);
  }

  //PY-44858
  public void testRefactoringNotCreateInitInSameDir() throws IOException {
    String pathToOriginFunction = "mypkg/a/main.py";
    String destination = "mypkg/a/other.py";
    doMultifileTest(pathToOriginFunction, destination, null);
  }

  //PY-44858
  public void testRefactoringNotCreateInitInParentDir() throws IOException {
    String pathToOriginFunction = "mypkg/a/b/main.py";
    String destination = "mypkg/a/other.py";
    doMultifileTest(pathToOriginFunction, destination, null);
  }

  //PY-44858
  public void testRefactoringNotCreateInitInChildDir() throws IOException {
    String pathToOriginFunction = "mypkg/a/main.py";
    String destination = "mypkg/a/b/other.py";
    doMultifileTest(pathToOriginFunction, destination, null);
  }

  // PY-6637
  public void testLocalFunctionSimple() {
    doTestSuccess();
  }

  // PY-6637
  public void testRefactoringAvailability() {
    myFixture.configureByFile(getTestName(true) + ".py");

    final PsiFile file = myFixture.getFile();
    moveByText("func");
    assertFalse(isActionEnabled());
    moveByText("local");
    assertTrue(isActionEnabled());

    // move to "def" keyword
    myFixture.getEditor().getCaretModel().moveCaretRelatively(-3, 0, false, false, false);
    final PsiElement tokenAtCaret = file.findElementAt(myFixture.getCaretOffset());
    assertNotNull(tokenAtCaret);
    assertEquals(PyTokenTypes.DEF_KEYWORD, tokenAtCaret.getNode().getElementType());
    assertTrue(isActionEnabled());

    moveByText("method");
    assertTrue(isActionEnabled());

    moveByText("static_method");
    assertFalse(isActionEnabled());
    moveByText("class_method");
    assertFalse(isActionEnabled());

    // Overridden method
    moveByText("overridden_method");
    assertFalse(isActionEnabled());

    // Overriding method
    moveByText("upper");
    assertFalse(isActionEnabled());

    moveByText("property");
    assertFalse(isActionEnabled());
    moveByText("__magic__");
    assertFalse(isActionEnabled());
  }

  // PY-6637
  public void testLocalFunctionNonlocalReferenceToOuterScope() {
    runWithLanguageLevel(LanguageLevel.PYTHON34,
                         () -> doTestFailure(PyBundle.message("refactoring.make.function.top.level.error.nonlocal.writes")));
  }

  // PY-6637
  public void testLocalFunctionNonlocalReferencesInInnerFunction() {
    runWithLanguageLevel(LanguageLevel.PYTHON34, this::doTestSuccess);
  }

  // PY-6637
  public void testLocalFunctionReferenceToSelf() {
    doTestFailure(PyBundle.message("refactoring.make.function.top.level.error.self.reads"));
  }

  public void testMethodNonlocalReferenceToOuterScope() {
    runWithLanguageLevel(LanguageLevel.PYTHON34,
                         () -> doTestFailure(PyBundle.message("refactoring.make.function.top.level.error.nonlocal.writes")));
  }

  public void testMethodOuterScopeReads() {
    doTestFailure(PyBundle.message("refactoring.make.function.top.level.error.outer.scope.reads"));
  }

  public void testMethodOtherMethodCalls() {
    doTestFailure(PyBundle.message("refactoring.make.function.top.level.error.method.calls"));
  }

  public void testMethodAttributeWrites() {
    doTestFailure(PyBundle.message("refactoring.make.function.top.level.error.attribute.writes"));
  }

  public void testMethodReadPrivateAttributes() {
    doTestFailure(PyBundle.message("refactoring.make.function.top.level.error.private.attributes"));
  }

  public void testMethodSelfUsedAsOperand() {
    doTestFailure(PyBundle.message("refactoring.make.function.top.level.error.special.usage.of.self"));
  }

  public void testMethodOverriddenSelf() {
    doTestFailure(PyBundle.message("refactoring.make.function.top.level.error.special.usage.of.self"));
  }

  public void testMethodSingleAttributeRead() {
    doTestSuccess();
  }

  public void testMethodMultipleAttributesReadReferenceQualifier() {
    doTestSuccess();
  }

  public void testMethodMultipleAttributesConstructorQualifier() {
    doTestSuccess();
  }

  public void testMethodImportUpdates() throws IOException {
    doMultiFileTest(null, null);
  }

  public void testMethodMoveToOtherFile() throws IOException {
    doMultiFileTest("util.py", null);
  }

  public void testLocalFunctionMoveToOtherFile() throws IOException {
    doMultiFileTest("util.py", null);
  }

  public void testMethodCalledViaClass() {
    doTestSuccess();
  }

  public void testMethodUniqueNameOfExtractedQualifier() {
    doTestSuccess();
  }

  public void testMethodUniqueParamNames() {
    doTestSuccess();
  }

  public void testRecursiveMethod() {
    doTestSuccess();
  }

  public void testRecursiveLocalFunction() {
    doTestSuccess();
  }

  public void testMethodNoNewParams() {
    doTestSuccess();
  }

  public void testMethodNotImportableDestinationFile() throws IOException {
    doMultiFileTest("not-importable.py", PyBundle.message("refactoring.move.error.cannot.use.module.name", "not-importable.py"));
  }

  public void testLocalFunctionNameCollision() {
    doTestFailure(PyBundle.message("refactoring.move.error.destination.file.contains.function", "nested"));
  }

  public void testMethodInsertionPositionSameFileClassAndUsageNotTopLevel() {
    doTestSuccess();
  }

  public void testMethodInsertionPositionUsageInAnotherFile() throws IOException {
    doMultiFileTest("other.py", null);
  }

  public void testMethodInsertionPositionNoUsageInAnotherFile() throws IOException {
    doMultiFileTest("other.py", null);    
  }

  @Override
  protected String getTestDataPath() {
    return super.getTestDataPath() + "/refactoring/makeFunctionTopLevel/";
  }
}
