package com.jetbrains.env.python.testing;

import com.intellij.execution.configurations.RuntimeConfigurationWarning;
import com.intellij.execution.testframework.AbstractTestProxy;
import com.intellij.execution.testframework.sm.runner.ui.MockPrinter;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.testFramework.fixtures.CodeInsightTestFixture;
import com.intellij.util.PathUtil;
import com.jetbrains.env.EnvTestTagsRequired;
import com.jetbrains.env.PyEnvTestCase;
import com.jetbrains.env.PyProcessWithConsoleTestTask;
import com.jetbrains.env.ut.PyTestTestProcessRunner;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.sdkTools.SdkCreationType;
import com.jetbrains.python.testing.PythonTestConfigurationsModel;
import com.jetbrains.python.testing.universalTests.PyUniversalPyTestConfiguration;
import com.jetbrains.python.testing.universalTests.PyUniversalPyTestFactory;
import com.jetbrains.python.testing.universalTests.TestTargetType;
import org.hamcrest.Matchers;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import org.junit.Test;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

import static com.jetbrains.env.ut.PyScriptTestProcessRunner.TEST_TARGET_PREFIX;
import static org.junit.Assert.assertEquals;

/**
 * User : catherine
 */
@EnvTestTagsRequired(tags = "pytest")
public final class PythonPyTestingTest extends PyEnvTestCase {


  // Ensures setup/teardown does not break anything
  @Test
  public void testSetupTearDown() throws Exception {
    runPythonTest(new SetupTearDownTestTask<PyTestTestProcessRunner>() {
      @NotNull
      @Override
      protected PyTestTestProcessRunner createProcessRunner() throws Exception {
        return new PyTestTestProcessRunner("test_test.py", 1);
      }
    });
  }

  /**
   * Ensures that python target pointing to module works correctly
   */
  @Test
  public void testRunModuleAsFile() throws Exception {
    runPythonTest(new RunModuleAsFileTask<PyTestTestProcessRunner>() {
      @NotNull
      @Override
      protected PyTestTestProcessRunner createProcessRunner() throws Exception {
        return new PyTestTestProcessRunner(TARGET, 0);
      }
    });
  }


  @Test
  public void testRerunSubfolder() throws Exception {
    runPythonTest(new RerunSubfolderTask<PyTestTestProcessRunner>(2) {
      @NotNull
      @Override
      protected PyTestTestProcessRunner createProcessRunner() throws Exception {
        return new PyTestTestProcessRunner(".", 1);
      }
    });
  }


  @Test
  public void testParametrized() throws Exception {
    runPythonTest(
      new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest/parametrized", SdkCreationType.EMPTY_SDK) {

        @NotNull
        @Override
        protected PyTestTestProcessRunner createProcessRunner() throws Exception {
          return new PyTestTestProcessRunner("test_pytest_parametrized.py", 1);
        }

        @Override
        protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
                                        @NotNull final String stdout,
                                        @NotNull final String stderr,
                                        @NotNull final String all) {
          Assert.assertEquals("Parametrized test produced bad tree",
                              "Test tree:\n" +
                              "[root]\n" +
                              ".test_pytest_parametrized\n" +
                              "..test_eval\n" +
                              "...(three plus file-8)(-)\n" +
                              "...((2)+(4)-6)(+)\n" +
                              "...( six times nine_-42)(-)\n", runner.getFormattedTestTree());
        }
      });
  }


  // Ensure test survives patched strftime
  @Test
  public void testMonkeyPatch() throws Exception {
    runPythonTest(
      new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest/monkeyPatch", SdkCreationType.EMPTY_SDK) {

        @NotNull
        @Override
        protected PyTestTestProcessRunner createProcessRunner() throws Exception {
          return new PyTestTestProcessRunner("test_test.py", 0);
        }

        @Override
        protected void checkTestResults(@NotNull PyTestTestProcessRunner runner,
                                        @NotNull String stdout,
                                        @NotNull String stderr,
                                        @NotNull String all) {
          assertEquals("Monkeypatch broke the test: " + stderr, 1, runner.getPassedTestsCount());
        }
      });
  }

  // Ensure slow test is not run when -m "not slow" is provided
  @Test
  public void testMarkerWithSpaces() throws Exception {
    runPythonTest(
      new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest/test_with_markers", SdkCreationType.EMPTY_SDK) {

        @NotNull
        @Override
        protected PyTestTestProcessRunner createProcessRunner() throws Exception {
          return new PyTestTestProcessRunner("test_with_markers.py", 0) {
            @Override
            protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalPyTestConfiguration configuration) throws IOException {
              super.configurationCreatedAndWillLaunch(configuration);
              configuration.setAdditionalArguments("-m 'not slow'");
            }
          };
        }


        @Override
        protected void checkTestResults(@NotNull PyTestTestProcessRunner runner,
                                        @NotNull String stdout,
                                        @NotNull String stderr,
                                        @NotNull String all) {
          Assert.assertEquals("Marker support broken", "Test tree:\n" +
                                                       "[root]\n" +
                                                       ".test_with_markers\n" +
                                                       "..test_fast(+)\n",
                              runner.getFormattedTestTree());
        }
      });
  }


  /**
   * New configuration should have closest src set as its working dir
   */
  @Test
  public void testClosestSrcIsWorkDirOnNewConfig() throws Exception {
    runPythonTest(
      new CreateConfigurationTestTask<PyUniversalPyTestConfiguration>(PythonTestConfigurationsModel.PY_TEST_NAME,
                                                                      PyUniversalPyTestConfiguration.class) {
        @NotNull
        @Override
        protected List<PsiElement> getPsiElementsToRightClickOn() {
          configureSrcFolder(myFixture);

          myFixture.configureByFile("test_with_src/foo/src/test_test.py");
          final PyFunction test = myFixture.findElementByText("test_test", PyFunction.class);
          assert test != null;
          return Collections.singletonList(test);
        }

        @Override
        protected void checkConfiguration(@NotNull PyUniversalPyTestConfiguration configuration,
                                          @NotNull PsiElement elementToRightClickOn) {
          super.checkConfiguration(configuration, elementToRightClickOn);
          Assert
            .assertThat("Wrong configuration directory set on new config", configuration.getWorkingDirectory(), Matchers.endsWith("src"));
        }
      });
  }

  /**
   * In case when workdir is not set we should use closest src
   */
  @Test
  public void testClosestSrcIsWorkDirDynamically() throws Exception {
    runPythonTest(
      new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/createConfigurationTest/", SdkCreationType.EMPTY_SDK) {
        @NotNull
        @Override
        protected PyTestTestProcessRunner createProcessRunner() throws Exception {
          return new PyTestTestProcessRunner(TEST_TARGET_PREFIX + "test_test.test_test", 0) {
            @Override
            protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalPyTestConfiguration configuration) throws IOException {
              super.configurationCreatedAndWillLaunch(configuration);
              // Reset dir to check it is calculated correctly
              configuration.setWorkingDirectory(null);
              configureSrcFolder(myFixture);
              Assert
                .assertThat("Wrong configuration directory calculated", configuration.getWorkingDirectorySafe(), Matchers.endsWith("src"));
            }
          };
        }


        @Override
        protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
                                        @NotNull final String stdout,
                                        @NotNull final String stderr,
                                        @NotNull final String all) {
          Assert.assertEquals("Failed to run test" + stderr, 1, runner.getPassedTestsCount());
        }
      });
  }

  //TODO: DOC
  private static void configureSrcFolder(@NotNull final CodeInsightTestFixture fixture) {
    final ModuleRootManager manager = ModuleRootManager.getInstance(fixture.getModule());
    final ModifiableRootModel model = manager.getModifiableModel();
    final VirtualFile srcToMark = fixture.getTempDirFixture().getFile("test_with_src/foo/src");
    assert srcToMark != null;
    model.addContentEntry(srcToMark);
    model.commit();
  }

  @Test
  public void testConfigurationProducer() throws Exception {
    runPythonTest(
      new CreateConfigurationByFileTask<>(PythonTestConfigurationsModel.PY_TEST_NAME, PyUniversalPyTestConfiguration.class));
  }

  @Test
  public void testMultipleCases() throws Exception {
    runPythonTest(
      new CreateConfigurationMultipleCasesTask<>(PythonTestConfigurationsModel.PY_TEST_NAME, PyUniversalPyTestConfiguration.class));
  }

  /**
   * Checks tests are resolved when launched from subfolder
   */
  @Test
  public void testTestsInSubFolderResolvable() throws Exception {
    runPythonTest(
      new PyUnitTestProcessWithConsoleTestTask.PyTestsInSubFolderRunner<PyTestTestProcessRunner>("test_metheggs", "test_funeggs",
                                                                                                 "test_first") {
        @NotNull
        @Override
        protected PyTestTestProcessRunner createProcessRunner() throws Exception {
          return new PyTestTestProcessRunner(toFullPath("tests"), 0) {
            @Override
            protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalPyTestConfiguration configuration) throws IOException {
              super.configurationCreatedAndWillLaunch(configuration);
              configuration.setWorkingDirectory(getWorkingFolderForScript());
            }
          };
        }
      });
  }

  /**
   * Ensures test output works
   */
  @Test
  public void testOutput() throws Exception {
    runPythonTest(
      new PyUnitTestProcessWithConsoleTestTask.PyTestsOutputRunner<PyTestTestProcessRunner>("test_metheggs", "test_funeggs", "test_first") {
        @NotNull
        @Override
        protected PyTestTestProcessRunner createProcessRunner() throws Exception {
          return new PyTestTestProcessRunner(toFullPath("tests"), 0) {
            @Override
            protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalPyTestConfiguration configuration) throws IOException {
              super.configurationCreatedAndWillLaunch(configuration);
              configuration.setWorkingDirectory(getWorkingFolderForScript());
            }
          };
        }
      });
  }

  @Test(expected = RuntimeConfigurationWarning.class)
  public void testValidation() throws Exception {

    final CreateConfigurationTestTask.PyConfigurationCreationTask<PyUniversalPyTestConfiguration> task =
      new CreateConfigurationTestTask.PyConfigurationCreationTask<PyUniversalPyTestConfiguration>() {
        @NotNull
        @Override
        protected PyUniversalPyTestFactory createFactory() {
          return PyUniversalPyTestFactory.INSTANCE;
        }
      };
    runPythonTest(task);
    task.checkEmptyTarget();
  }

  @Test
  public void testConfigurationProducerOnDirectory() throws Exception {
    runPythonTest(
      new CreateConfigurationByFileTask.CreateConfigurationTestAndRenameFolderTask<>(PythonTestConfigurationsModel.PY_TEST_NAME,
                                                                                     PyUniversalPyTestConfiguration.class));
  }

  @Test
  public void testProduceConfigurationOnFile() throws Exception {
    runPythonTest(
      new CreateConfigurationByFileTask<PyUniversalPyTestConfiguration>(PythonTestConfigurationsModel.PY_TEST_NAME,
                                                                        PyUniversalPyTestConfiguration.class, "spam.py") {
        @NotNull
        @Override
        protected PsiElement getElementToRightClickOnByFile(@NotNull final String fileName) {
          return myFixture.configureByFile(fileName);
        }
      });
  }

  @Test
  public void testRenameClass() throws Exception {
    runPythonTest(
      new CreateConfigurationByFileTask.CreateConfigurationTestAndRenameClassTask<>(
        PythonTestConfigurationsModel.PY_TEST_NAME,
        PyUniversalPyTestConfiguration.class));
  }

  /**
   * Ensure dots in test names do not break anything (PY-13833)
   */
  @Test
  public void testEscape() throws Exception {
    runPythonTest(new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest/", SdkCreationType.EMPTY_SDK) {
      @NotNull
      @Override
      protected PyTestTestProcessRunner createProcessRunner() throws Exception {
        return new PyTestTestProcessRunner("test_escape_me.py", 0);
      }

      @Override
      protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
                                      @NotNull final String stdout,
                                      @NotNull final String stderr,
                                      @NotNull final String all) {
        final String resultTree = runner.getFormattedTestTree().trim();
        final String expectedTree = myFixture.configureByFile("test_escape_me.tree.txt").getText().trim();
        Assert.assertEquals("Test result wrong tree", expectedTree, resultTree);
      }
    });
  }

  // Import error should lead to test failure
  @Test
  public void testFailInCaseOfError() {
    runPythonTest(new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest/failTest", SdkCreationType.EMPTY_SDK) {

      @NotNull
      @Override
      protected PyTestTestProcessRunner createProcessRunner() throws Exception {
        return new PyTestTestProcessRunner(".", 0);
      }


      @Override
      protected void checkTestResults(@NotNull PyTestTestProcessRunner runner,
                                      @NotNull String stdout,
                                      @NotNull String stderr,
                                      @NotNull String all) {
        Assert.assertThat("Import error is not marked as error", runner.getFailedTestsCount(), Matchers.greaterThanOrEqualTo(1));
      }
    });
  }

  /**
   * Ensure element dir is used as curdir even if not set explicitly
   */
  @Test
  public void testCurrentDir() throws Exception {
    runPythonTest(new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest/", SdkCreationType.EMPTY_SDK) {
      @NotNull
      @Override
      protected PyTestTestProcessRunner createProcessRunner() throws Exception {
        return new PyTestTestProcessRunner("", 0) {
          @Override
          protected void configurationCreatedAndWillLaunch(@NotNull final PyUniversalPyTestConfiguration configuration) throws IOException {
            super.configurationCreatedAndWillLaunch(configuration);
            configuration.setWorkingDirectory(null);
            final VirtualFile fullFilePath = myFixture.getTempDirFixture().getFile("dir_test.py");
            assert fullFilePath != null : String.format("No dir_test.py in %s", myFixture.getTempDirFixture().getTempDirPath());
            configuration.getTarget().setTarget(fullFilePath.getPath());
            configuration.getTarget().setTargetType(TestTargetType.PATH);
          }
        };
      }

      @Override
      protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
                                      @NotNull final String stdout,
                                      @NotNull final String stderr,
                                      @NotNull final String all) {
        final String projectDir = myFixture.getTempDirFixture().getTempDirPath();
        Assert.assertThat("No directory found in output", runner.getConsole().getText(),
                          Matchers.containsString(String.format("Directory %s", PathUtil.toSystemDependentName(projectDir))));
      }
    });
  }

  @Test
  public void testPytestRunner() {

    runPythonTest(new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest", SdkCreationType.EMPTY_SDK) {
      @NotNull
      @Override
      protected PyTestTestProcessRunner createProcessRunner() throws Exception {
        return new PyTestTestProcessRunner("test1.py", 0);
      }

      @Override
      protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
                                      @NotNull final String stdout,
                                      @NotNull final String stderr,
                                      @NotNull final String all) {
        assertEquals(3, runner.getAllTestsCount());
        assertEquals(3, runner.getPassedTestsCount());
        runner.assertAllTestsPassed();


        // This test has "sleep(1)", so duration should be >=1000
        final AbstractTestProxy testForOneSecond = runner.findTestByName("testOne");
        Assert.assertThat("Wrong duration", testForOneSecond.getDuration(), Matchers.greaterThanOrEqualTo(1000L));
      }
    });
  }

  /**
   * Ensure we can run path like "spam.bar" where "spam" is folder with out of init.py
   */
  @Test
  public void testPyTestFolderNoInitPy() {
    runPythonTest(new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest", SdkCreationType.EMPTY_SDK) {
      @NotNull
      @Override
      protected PyTestTestProcessRunner createProcessRunner() throws Exception {
        if (getLevelForSdk().isPy3K()) {
          return new PyTestTestProcessRunner("folder_no_init_py/test_test.py", 2);
        }
        else {
          return new PyTestTestProcessRunner(toFullPath("folder_no_init_py/test_test.py"), 2) {
            @Override
            protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalPyTestConfiguration configuration) throws IOException {
              super.configurationCreatedAndWillLaunch(configuration);
              configuration.setWorkingDirectory(getWorkingFolderForScript());
            }
          };
        }
      }

      @Override
      protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
                                      @NotNull final String stdout,
                                      @NotNull final String stderr,
                                      @NotNull final String all) {
        assertEquals(runner.getFormattedTestTree(), 1, runner.getFailedTestsCount());
        if (runner.getCurrentRerunStep() == 0) {
          assertEquals(runner.getFormattedTestTree(), 2, runner.getAllTestsCount());
          assertEquals(runner.getFormattedTestTree(), 1, runner.getPassedTestsCount());
        }
        else {
          assertEquals(runner.getFormattedTestTree(), 1, runner.getAllTestsCount());
          assertEquals(runner.getFormattedTestTree(), 0, runner.getPassedTestsCount());
        }
      }
    });
  }

  @Test
  public void testPytestRunner2() {
    runPythonTest(new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest", SdkCreationType.EMPTY_SDK) {
      @NotNull
      @Override
      protected PyTestTestProcessRunner createProcessRunner() throws Exception {
        return new PyTestTestProcessRunner("test2.py", 1);
      }

      @Override
      protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
                                      @NotNull final String stdout,
                                      @NotNull final String stderr,
                                      @NotNull final String all) {
        if (runner.getCurrentRerunStep() > 0) {
          // We rerun all tests, since running parametrized tests is broken until
          // https://github.com/JetBrains/teamcity-messages/issues/121
          assertEquals(runner.getFormattedTestTree(), 7, runner.getAllTestsCount());
          assertEquals(runner.getFormattedTestTree(), 3, runner.getPassedTestsCount());
          assertEquals(runner.getFormattedTestTree(), 4, runner.getFailedTestsCount());
          return;
        }
        assertEquals(runner.getFormattedTestTree(), 9, runner.getAllTestsCount());
        assertEquals(runner.getFormattedTestTree(), 5, runner.getPassedTestsCount());
        assertEquals(runner.getFormattedTestTree(), 4, runner.getFailedTestsCount());
        // Py.test may report F before failed test, so we check string contains, not starts with
        Assert
          .assertThat("No test stdout", MockPrinter.fillPrinter(runner.findTestByName("testOne")).getStdOut(),
                      Matchers.containsString("I am test1"));

        // Ensure test has stdout even it fails
        final AbstractTestProxy testFail = runner.findTestByName("testFail");
        Assert.assertThat("No stdout for fail", MockPrinter.fillPrinter(testFail).getStdOut(),
                          Matchers.containsString("I will fail"));

        // This test has "sleep(1)", so duration should be >=1000
        Assert.assertThat("Wrong duration", testFail.getDuration(), Matchers.greaterThanOrEqualTo(1000L));
      }
    });
  }


  /**
   * Ensures file references are highlighted for pytest traceback
   */
  @Test
  public void testPyTestFileReferences() {
    final String fileName = "reference_tests.py";

    runPythonTest(new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/unit", SdkCreationType.EMPTY_SDK) {
      @NotNull
      @Override
      protected PyTestTestProcessRunner createProcessRunner() throws Exception {
        return new PyTestTestProcessRunner(fileName, 0);
      }

      @Override
      protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
                                      @NotNull final String stdout,
                                      @NotNull final String stderr,
                                      @NotNull final String all) {
        final List<String> fileNames = runner.getHighlightedStringsInConsole().second;
        Assert.assertThat("No lines highlighted", fileNames, Matchers.not(Matchers.empty()));
        // PyTest highlights file:line_number
        Assert.assertTrue("Assert fail not marked", fileNames.contains("reference_tests.py:7"));
        Assert.assertTrue("Failed test not marked", fileNames.contains("reference_tests.py:12"));
        Assert.assertTrue("Failed test not marked", fileNames.contains("reference_tests.py"));
      }
    });
  }
}
