/*
 * 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;

import com.intellij.psi.PsiElement;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.documentation.docstrings.*;
import com.jetbrains.python.documentation.docstrings.SectionBasedDocString.Section;
import com.jetbrains.python.documentation.docstrings.SectionBasedDocString.SectionField;
import com.jetbrains.python.fixtures.PyTestCase;
import com.jetbrains.python.psi.PyStringLiteralExpression;
import com.jetbrains.python.toolbox.Substring;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;

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

  public void testSimpleGoogleDocString() {
    checkSimpleDocstringStructure(findAndParseGoogleStyleDocString());
  }

  public void testSimpleNumpyDocstring() {
    checkSimpleDocstringStructure(findAndParseNumpyStyleDocString());
  }

  private static void checkSimpleDocstringStructure(@NotNull SectionBasedDocString docString) {
    assertEquals("Summary", docString.getSummary());
    final List<Section> sections = docString.getSections();
    assertSize(3, sections);

    assertEquals("parameters", sections.get(0).getNormalizedTitle());
    final List<SectionField> paramFields = sections.get(0).getFields();
    assertSize(2, paramFields);
    final SectionField param1 = paramFields.get(0);
    assertEquals("x", param1.getName());
    assertEquals("int", param1.getType());
    assertEquals("first parameter", param1.getDescription());

    final SectionField param2 = paramFields.get(1);
    assertEquals("y", param2.getName());
    assertEmpty(param2.getType());
    assertEquals("second parameter\n" +
                 "with longer description", param2.getDescription());

    assertEquals("raises", sections.get(1).getNormalizedTitle());
    final List<SectionField> exceptionFields = sections.get(1).getFields();
    assertSize(1, exceptionFields);
    final SectionField exception1 = exceptionFields.get(0);
    assertEmpty(exception1.getName());
    assertEquals("Exception", exception1.getType());
    assertEquals("if anything bad happens", exception1.getDescription());

    assertEquals("returns", sections.get(2).getNormalizedTitle());
    final List<SectionField> returnFields = sections.get(2).getFields();
    assertSize(1, returnFields);
    final SectionField return1 = returnFields.get(0);
    assertEmpty(return1.getName());
    assertEquals("None", return1.getType());
    assertEquals("always", return1.getDescription());
  }

  @NotNull
  private Substring findAndParseDocString() {
    myFixture.configureByFile(getTestName(true) + ".py");
    final String docStringText = findFirstDocString();

    assertNotNull(docStringText);
    return new Substring(docStringText);
  }

  @NotNull
  private GoogleCodeStyleDocString findAndParseGoogleStyleDocString() {
    return new GoogleCodeStyleDocString(findAndParseDocString());
  }

  @NotNull
  private NumpyDocString findAndParseNumpyStyleDocString() {
    return new NumpyDocString(findAndParseDocString());
  }

  @Nullable
  private String findFirstDocString() {
    final PsiElementProcessor.FindElement<PsiElement> processor = new PsiElementProcessor.FindElement<>() {
      @Override
      public boolean execute(@NotNull PsiElement element) {
        if (element instanceof PyStringLiteralExpression && element.getFirstChild().getNode().getElementType() == PyTokenTypes.DOCSTRING) {
          return setFound(element);
        }
        return true;
      }
    };
    PsiTreeUtil.processElements(myFixture.getFile(), processor);
    if (!processor.isFound()) {
      return null;
    }
    final PsiElement foundElement = processor.getFoundElement();
    assertNotNull(foundElement);
    return ((PyStringLiteralExpression)foundElement).getStringValue();
  }

  public void testSectionStartAfterQuotes() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertEmpty(docString.getSummary());

    assertSize(2, docString.getSections());

    final Section examplesSection = docString.getSections().get(0);
    assertEquals("examples", examplesSection.getNormalizedTitle());
    assertSize(1, examplesSection.getFields());
    final SectionField example1 = examplesSection.getFields().get(0);
    assertEmpty(example1.getName());
    assertEmpty(example1.getType());
    assertEquals("Useless call\n" +
                 "func() == func()", example1.getDescription());

    final Section notesSection = docString.getSections().get(1);
    assertEquals("notes", notesSection.getNormalizedTitle());
    assertSize(1, notesSection.getFields());
    final SectionField note1 = notesSection.getFields().get(0);
    assertEmpty(note1.getName());
    assertEmpty(note1.getType());
    assertEquals("some\n" +
                 "notes", note1.getDescription());
  }

  public void testTypeReferences() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertEmpty(docString.getSummary());
    assertSize(2, docString.getSections());

    final Section paramSection = docString.getSections().get(0);
    assertSize(1, paramSection.getFields());
    final SectionField param1 = paramSection.getFields().get(0);
    assertEquals("a1", param1.getName());
    assertEquals(":class:`MyClass`", param1.getType());
    assertEquals("used to call :def:`my_function` and access :attr:`my_attr`", param1.getDescription());

    final Section raisesSection = docString.getSections().get(1);
    assertSize(1, raisesSection.getFields());
    final SectionField exception1 = raisesSection.getFields().get(0);
    assertEmpty(exception1.getName());
    assertEquals(":class:`MyException`", exception1.getType());
    assertEquals("thrown in case of any error", exception1.getDescription());
  }

  public void testNestedIndentation() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertSize(1, docString.getSections());
    final Section section1 = docString.getSections().get(0);
    assertEquals("parameters", section1.getNormalizedTitle());
    assertSize(1, section1.getFields());
    final SectionField param1 = section1.getFields().get(0);
    assertEquals("x", param1.getName());
    assertEquals("int", param1.getType());
    assertEquals("""
                   first line of the description
                   second line
                     third line

                   Example::

                       assert func(42) is None""", param1.getDescription());
  }

  public void testMultilineSummary() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertEquals("""
                   First line
                   Second line
                   Third line""", docString.getSummary());
  }

  public void testNumpyNamedReturnsAndYields() {
    final NumpyDocString docString = findAndParseNumpyStyleDocString();
    assertEmpty(docString.getSummary());
    assertSize(2, docString.getSections());

    final Section returnSection = docString.getSections().get(0);
    assertSize(2, returnSection.getFields());

    final SectionField return1 = returnSection.getFields().get(0);
    assertEquals("status_code", return1.getName());
    assertEquals("int", return1.getType());
    assertEquals("HTTP status code", return1.getDescription());

    final SectionField return2 = returnSection.getFields().get(1);
    assertEquals("template", return2.getName());
    assertEquals("str", return2.getType());
    assertEquals("path to template in template roots", return2.getDescription());

    final Section yieldSection = docString.getSections().get(1);
    assertSize(1, yieldSection.getFields());
    final SectionField yield1 = yieldSection.getFields().get(0);
    assertEquals("progress", yield1.getName());
    assertEquals("float", yield1.getType());
    assertEquals("floating point value in range [0, 1) indicating progress\n" +
                 "of the task", yield1.getDescription());
  }

  public void testNumpySignature() {
    final NumpyDocString docString = findAndParseNumpyStyleDocString();
    assertEquals("a.diagonal(offset=0, axis1=0, axis2=1)", docString.getSignature());
    assertEquals("Return specified diagonals.", docString.getSummary());
  }

  public void testNumpySectionBlockBreaksOnDoubleEmptyLine() {
    final NumpyDocString docString = findAndParseNumpyStyleDocString();
    assertSize(1, docString.getSections());
    final Section paramSection = docString.getSections().get(0);
    assertEquals("parameters", paramSection.getNormalizedTitle());
    assertSize(1, paramSection.getFields());
    final SectionField param1 = paramSection.getFields().get(0);
    assertEquals("x", param1.getName());
    assertEmpty(param1.getType());
    assertEquals("""
                   First line
                   Second line

                   Line after single break""", param1.getDescription());
  }

  // PY-21883
  public void testNumpyCombinesParamsAndOtherParamsSections() {
    final NumpyDocString docString = findAndParseNumpyStyleDocString();
    final List<String> parameters = docString.getParameters();
    assertEquals(List.of("x", "y", "z"), parameters);
  }

  public void testGoogleEmptyParamTypeInParenthesis() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertSize(1, docString.getSections());
    final Section paramSection = docString.getSections().get(0);
    assertEquals("parameters", paramSection.getNormalizedTitle());
    assertSize(1, paramSection.getFields());
    final SectionField param1 = paramSection.getFields().get(0);
    assertEquals("x", param1.getName());
    assertEmpty(param1.getDescription());
    assertEmpty(param1.getType());
    assertNotNull(param1.getTypeAsSubstring());
    assertEquals(26, param1.getTypeAsSubstring().getStartOffset());
    assertEquals(26, param1.getTypeAsSubstring().getEndOffset());
  }

  public void testGoogleReturnTypeNoDescription() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertSize(1, docString.getSections());
    final Section returnSection = docString.getSections().get(0);
    assertEquals("returns", returnSection.getNormalizedTitle());
    assertSize(1, returnSection.getFields());
    final SectionField return1 = returnSection.getFields().get(0);
    assertEmpty(return1.getName());
    assertEmpty(return1.getDescription());
    assertEquals("object", return1.getType());
    assertNotNull(return1.getTypeAsSubstring());
    assertEquals(20, return1.getTypeAsSubstring().getStartOffset());
    assertEquals(26, return1.getTypeAsSubstring().getEndOffset());
  }

  public void testGoogleNoEmptyLineAfterSummary() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertEquals("Summary.", docString.getSummary());
    assertSize(1, docString.getSections());
    assertSize(1, docString.getSections().get(0).getFields());
  }

  public void testGoogleParametersSectionWithoutSummary() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertEmpty(docString.getSummary());
    assertSize(1, docString.getSections());
    final Section paramSection = docString.getSections().get(0);
    assertEquals("parameters", paramSection.getNormalizedTitle());
    assertSize(1, paramSection.getFields());
  }

  public void testGoogleKeywordArgumentsSection() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertEmpty(docString.getSummary());
    assertSize(1, docString.getSections());
    assertEquals("keyword arguments", docString.getSections().get(0).getNormalizedTitle());
  }

  // PY-16766
  public void testGoogleDocStringContentDetection() {
    assertTrue(DocStringUtil.isLikeGoogleDocString(
      """

            My Section:
                some user defined section
           \s
            Parameters:
                param1:\s

            Returns:
        """));
  }

  public void testNumpyEmptySectionIndent() {
    final NumpyDocString docString = findAndParseNumpyStyleDocString();
    assertSize(3, docString.getSections());
    final Section paramSection = docString.getSections().get(0);
    assertEquals("parameters", paramSection.getNormalizedTitle());
    assertSize(2, paramSection.getFields());
    final Section exampleSection = docString.getSections().get(1);
    assertSize(1, exampleSection.getFields());
    assertEquals("First sentence.\n" +
                 "Second sentence.", exampleSection.getFields().get(0).getDescription());
    final Section returnSection = docString.getSections().get(2);
    assertSize(1, returnSection.getFields());
    assertEquals("Something", returnSection.getFields().get(0).getType());
  }

  public void testGoogleParamNamedLikeSection() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertSize(1, docString.getSections());
    final Section paramSection = docString.getSections().get(0);
    assertSize(2, paramSection.getFields());
    assertEquals("x", paramSection.getFields().get(0).getName());
    assertEquals("args", paramSection.getFields().get(1).getName());
  }

  public void testGoogleNoColonAfterParameter() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertSize(1, docString.getSections());
    final Section paramSection = docString.getSections().get(0);
    assertSize(2, paramSection.getFields());
    final SectionField x = paramSection.getFields().get(0);
    assertEquals("x", x.getName());
    assertEmpty(x.getType());
    assertEmpty(x.getDescription());

    final SectionField y = paramSection.getFields().get(1);
    assertEquals("y", y.getName());
    assertEquals("int", y.getType());
    assertEmpty(y.getDescription());
  }

  public void testNumpyMultipleReturns() {
    final NumpyDocString docString = findAndParseNumpyStyleDocString();
    assertSize(1, docString.getSections());
    final Section returnSection = docString.getSections().get(0);
    assertSize(2, returnSection.getFields());
  }

  // PY-16908
  public void testNumpyCombinedParamDeclarations() {
    final NumpyDocString docString = findAndParseNumpyStyleDocString();
    assertSize(1, docString.getSections());
    final Section paramSection = docString.getSections().get(0);
    assertSize(1, paramSection.getFields());
    final SectionField firstField = paramSection.getFields().get(0);
    assertSameElements(firstField.getNames(), "x", "y", "args", "kwargs");
    assertEquals(firstField.getType(), "Any");
    assertEquals(firstField.getDescription(), "description");
  }

  // PY-16991
  public void testGoogleMandatoryIndentationInsideSection() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertSize(3, docString.getSections());
    assertEmpty(docString.getSections().get(0).getFields());
    assertSize(1, docString.getSections().get(1).getFields());
    final Section thirdSection = docString.getSections().get(2);
    assertSize(1, thirdSection.getFields());
    final SectionField firstExample = thirdSection.getFields().get(0);
    assertEmpty(firstExample.getName());
    assertEmpty(firstExample.getType());
    assertEquals("first line\n" +
                 "second line", firstExample.getDescription());
  }

  // PY-17002
  public void testGoogleNoClosingParenthesisAfterParamType() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertSize(1, docString.getSections());
    final List<SectionField> params = docString.getSections().get(0).getFields();
    assertSize(2, params);
    assertEquals("Foo", params.get(0).getType());
    assertEquals("Bar", params.get(1).getType());
  }

  // PY-17657, PY-16303
  public void testNotGoogleFormatIfDocstringContainTags() {
    assertEquals(DocStringFormat.REST, DocStringUtil.guessDocStringFormat("""
                                                                            ""\"
                                                                            :type sub_field: FieldDescriptor | () -> FieldDescriptor
                                                                            :param sub_field: The type of field in this collection
                                                                                Tip: You can pass a ValueObject class here to ...
                                                                                Example:
                                                                                    addresses = field.Collection(AddressObject)
                                                                            ""\""""));

    assertEquals(DocStringFormat.REST, DocStringUtil.guessDocStringFormat("""
                                                                            ""\"
                                                                            Args:
                                                                                :param Tuple[int, int] name: Some description
                                                                            ""\""""));
  }

  // PY-31025
  public void testGoogleDescriptionOfReturnValueWithoutType() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertEquals("return value description", docString.getReturnDescription());
  }

  // PY-31025
  public void testGoogleMultilineDescriptionOfReturnValueWithoutType() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertEquals("return value description\n" +
                 "with continuation", docString.getReturnDescription());
  }

  // PY-31025
  public void testGoogleDescriptionOfReturnValueOnNextLine() {
    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
    assertEquals("return value description", docString.getReturnDescription());
  }

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