import pytest
import numpydoc.validate
import numpydoc.tests


validate_one = numpydoc.validate.validate


class GoodDocStrings:
    """
    Collection of good doc strings.

    This class contains a lot of docstrings that should pass the validation
    script without any errors.

    See Also
    --------
    AnotherClass : With its description.

    Examples
    --------
    >>> result = 1 + 1
    """
    def one_liner(self):
        """Allow one liner docstrings (including quotes)."""
        # This should fail, but not because of the position of the quotes
        pass

    def plot(self, kind, color="blue", **kwargs):
        """
        Generate a plot.

        Render the data in the Series as a matplotlib plot of the
        specified kind.

        Parameters
        ----------
        kind : str
            Kind of matplotlib plot, e.g.::

                'foo'

        color : str, default 'blue'
            Color name or rgb code.
        **kwargs
            These parameters will be passed to the matplotlib plotting
            function.

        See Also
        --------
        related : Something related.

        Examples
        --------
        >>> result = 1 + 1
        """
        pass

    def swap(self, arr, i, j, *args, **kwargs):
        """
        Swap two indicies on an array.

        The extended summary can be multiple paragraphs, but just one
        is enough to pass the validation.

        Parameters
        ----------
        arr : list
            The list having indexes swapped.
        i, j : int
            The indexes being swapped.
        *args, **kwargs
            Extraneous parameters are being permitted.

        See Also
        --------
        related : Something related.

        Examples
        --------
        >>> result = 1 + 1
        """
        pass

    def sample(self):
        """
        Generate and return a random number.

        The value is sampled from a continuous uniform distribution between
        0 and 1.

        Returns
        -------
        float
            Random number generated.

            - Make sure you set a seed for reproducibility

        See Also
        --------
        related : Something related.

        Examples
        --------
        >>> result = 1 + 1
        """
        pass

    def random_letters(self):
        """
        Generate and return a sequence of random letters.

        The length of the returned string is also random, and is also
        returned.

        Returns
        -------
        length : int
            Length of the returned string.
        letters : str
            String of random letters.

            .. versionadded:: 0.1

        See Also
        --------
        related : Something related.

        Examples
        --------
        >>> result = 1 + 1
        """
        pass

    def sample_values(self):
        """
        Generate an infinite sequence of random numbers.

        The values are sampled from a continuous uniform distribution between
        0 and 1.

        Yields
        ------
        float
            Random number generated.

        See Also
        --------
        related : Something related.

        Examples
        --------
        >>> result = 1 + 1
        """
        pass

    def head(self):
        """
        Return the first 5 elements of the Series.

        This function is mainly useful to preview the values of the
        Series without displaying the whole of it.

        Returns
        -------
        int
            Subset of the original series with the 5 first values.

        See Also
        --------
        Series.tail : Return the last 5 elements of the Series.
        Series.iloc : Return a slice of the elements in the Series,
            which can also be used to return the first or last n.

        Examples
        --------
        >>> 1 + 1
        2
        """
        return 1

    def head1(self, n=5):
        """
        Return the first elements of the Series.

        This function is mainly useful to preview the values of the
        Series without displaying the whole of it.

        Parameters
        ----------
        n : int
            Number of values to return.

        Returns
        -------
        int
            Subset of the original series with the n first values.

        See Also
        --------
        tail : Return the last n elements of the Series.

        Examples
        --------
        >>> s = 10
        >>> s
        10

        With the `n` parameter, we can change the number of returned rows:

        >>> s + 1
        11
        """
        return 1

    def summary_starts_with_number(self, n=5):
        """
        2nd rule of summaries should allow this.

        3 Starting the summary with a number instead of a capital letter.
        Also in parameters, returns, see also...

        Parameters
        ----------
        n : int
            4 Number of values to return.

        Returns
        -------
        int
            5 Subset of the original series with the n first values.

        See Also
        --------
        tail : 6 Return the last n elements of the Series.

        Examples
        --------
        >>> s = 10
        >>> s
        10

        7 With the `n` parameter, we can change the number of returned rows:

        >>> s + 1
        11
        """
        return 1

    def contains(self, pat, case=True, na=float('NaN')):
        """
        Return whether each value contains `pat`.

        In this case, we are illustrating how to use sections, even
        if the example is simple enough and does not require them.

        Parameters
        ----------
        pat : str
            Pattern to check for within each element.
        case : bool, default True
            Whether check should be done with case sensitivity.
        na : object, default np.nan
            Fill value for missing data.

        See Also
        --------
        related : Something related.

        Examples
        --------
        >>> s = 25
        >>> s
        25

        **Case sensitivity**

        With `case_sensitive` set to `False` we can match `a` with both
        `a` and `A`:

        >>> s + 1
        26

        **Missing values**

        We can fill missing values in the output using the `na` parameter:

        >>> s * 2
        50
        """
        pass

    def mode(self, axis, numeric_only):
        """
        Ensure reST directives don't affect checks for leading periods.

        The extended summary can be multiple paragraphs, but just one
        is enough to pass the validation.

        Parameters
        ----------
        axis : str
            Sentence ending in period, followed by single directive.

            .. versionchanged:: 0.1.2

        numeric_only : bool
            Sentence ending in period, followed by multiple directives.

            .. versionadded:: 0.1.2
            .. deprecated:: 0.00.0
                A multiline description,
                which spans another line.

        See Also
        --------
        related : Something related.

        Examples
        --------
        >>> result = 1 + 1
        """
        pass

    def good_imports(self):
        """
        Ensure import other than numpy and pandas are fine.

        The extended summary can be multiple paragraphs, but just one
        is enough to pass the validation.

        See Also
        --------
        related : Something related.

        Examples
        --------
        This example does not import pandas or import numpy.
        >>> import datetime
        >>> datetime.MAXYEAR
        9999
        """
        pass

    def no_returns(self):
        """
        Say hello and have no returns.

        The extended summary can be multiple paragraphs, but just one
        is enough to pass the validation.

        See Also
        --------
        related : Something related.

        Examples
        --------
        >>> result = 1 + 1
        """
        pass

    def empty_returns(self):
        """
        Say hello and always return None.

        Since this function never returns a value, this
        docstring doesn't need a return section.

        See Also
        --------
        related : Something related.

        Examples
        --------
        >>> result = 1 + 1
        """

        def say_hello():
            return "Hello World!"

        say_hello()
        if True:
            return
        else:
            return None

    def multiple_variables_on_one_line(self, matrix, a, b, i, j):
        """
        Swap two values in a matrix.

        The extended summary can be multiple paragraphs, but just one
        is enough to pass the validation.

        Parameters
        ----------
        matrix : list of list
            A double list that represents a matrix.
        a, b : int
            The indicies of the first value.
        i, j : int
            The indicies of the second value.

        See Also
        --------
        related : Something related.

        Examples
        --------
        >>> result = 1 + 1
        """
        pass


class BadGenericDocStrings:
    """Everything here has a bad docstring
    """

    def func(self):

        """Some function.

        With several mistakes in the docstring.

        It has a blank like after the signature `def func():`.

        The text 'Some function' should go in the line after the
        opening quotes of the docstring, not in the same line.

        There is a blank line between the docstring and the first line
        of code `foo = 1`.

        The closing quotes should be in the next line, not in this one."""

        foo = 1
        bar = 2
        return foo + bar

    def astype(self, dtype):
        """
        Casts Series type.

        Verb in third-person of the present simple, should be infinitive.
        """
        pass

    def astype1(self, dtype):
        """
        Method to cast Series type.

        Does not start with verb.
        """
        pass

    def astype2(self, dtype):
        """
        Cast Series type

        Missing dot at the end.
        """
        pass

    def astype3(self, dtype):
        """
        Cast Series type from its current type to the new type defined in
        the parameter dtype.

        Summary is too verbose and doesn't fit in a single line.
        """
        pass

    def two_linebreaks_between_sections(self, foo):
        """
        Test linebreaks message GL03.

        Note 2 blank lines before parameters section.


        Parameters
        ----------
        foo : str
            Description of foo parameter.
        """
        pass

    def linebreak_at_end_of_docstring(self, foo):
        """
        Test linebreaks message GL03.

        Note extra blank line at end of docstring.

        Parameters
        ----------
        foo : str
            Description of foo parameter.

        """
        pass

    def plot(self, kind, **kwargs):
        """
        Generate a plot.

        Render the data in the Series as a matplotlib plot of the
        specified kind.

        Note the blank line between the parameters title and the first
        parameter. Also, note that after the name of the parameter `kind`
        and before the colon, a space is missing.

        Also, note that the parameter descriptions do not start with a
        capital letter, and do not finish with a dot.

        Finally, the `**kwargs` parameter is missing.

        Parameters
        ----------

        kind: str
            kind of matplotlib plot
        """
        pass

    def unknown_section(self):
        """
        This section has an unknown section title.

        Unknown Section
        ---------------
        This should raise an error in the validation.
        """

    def sections_in_wrong_order(self):
        """
        This docstring has the sections in the wrong order.

        Parameters
        ----------
        name : str
            This section is in the right position.

        Examples
        --------
        >>> print('So far Examples is good, as it goes before Parameters')
        So far Examples is good, as it goes before Parameters

        See Also
        --------
        function : This should generate an error, as See Also needs to go
            before Examples.
        """

    def deprecation_in_wrong_order(self):
        """
        This docstring has the deprecation warning in the wrong order.

        This is the extended summary. The correct order should be
        summary, deprecation warning, extended summary.

        .. deprecated:: 1.0
            This should generate an error as it needs to go before
            extended summary.
        """

    def method_wo_docstrings(self):
        pass

    def directives_without_two_colons(self, first, second):
        """
        Ensure reST directives have trailing colons.

        Parameters
        ----------
        first : str
            Sentence ending in period, followed by single directive w/o colons.

            .. versionchanged 0.1.2

        second : bool
            Sentence ending in period, followed by multiple directives w/o
            colons.

            .. versionadded 0.1.2
            .. deprecated 0.00.0

        """
        pass


class BadSummaries:
    def no_summary(self):
        """
        Returns
        -------
        int
            Always one.
        """

    def heading_whitespaces(self):
        """
           Summary with heading whitespaces.

        Returns
        -------
        int
            Always one.
        """

    def wrong_line(self):
        """Quotes are on the wrong line.

        Both opening and closing."""
        pass

    def no_punctuation(self):
        """
        Has the right line but forgets punctuation
        """
        pass

    def no_capitalization(self):
        """
        provides a lowercase summary.
        """
        pass

    def no_infinitive(self):
        """
        Started with a verb that is not infinitive.
        """

    def multi_line(self):
        """
        Extends beyond one line
        which is not correct.
        """

    def two_paragraph_multi_line(self):
        """
        Extends beyond one line
        which is not correct.

        Extends beyond one line, which in itself is correct but the
        previous short summary should still be an issue.
        """


class BadParameters:
    """
    Everything here has a problem with its Parameters section.
    """
    def no_type(self, value):
        """
        Lacks the type.

        Parameters
        ----------
        value
            A parameter without type.
        """

    def type_with_period(self, value):
        """
        Has period after type.

        Parameters
        ----------
        value : str.
            A parameter type should not finish with period.
        """

    def no_description(self, value):
        """
        Lacks the description.

        Parameters
        ----------
        value : str
        """

    def missing_params(self, kind, **kwargs):
        """
        Lacks kwargs in Parameters.

        Parameters
        ----------
        kind : str
            Foo bar baz.
        """

    def bad_colon_spacing(self, kind):
        """
        Has bad spacing in the type line.

        Parameters
        ----------
        kind: str
            Needs a space after kind.
        """

    def no_description_period(self, kind):
        """
        Forgets to add a period to the description.

        Parameters
        ----------
        kind : str
           Doesn't end with a dot
        """

    def no_description_period_with_directive(self, kind):
        """
        Forgets to add a period, and also includes a directive.

        Parameters
        ----------
        kind : str
           Doesn't end with a dot

           .. versionadded:: 0.00.0
        """

    def no_description_period_with_directives(self, kind):
        """
        Forgets to add a period, and also includes multiple directives.

        Parameters
        ----------
        kind : str
           Doesn't end with a dot

           .. versionchanged:: 0.00.0
           .. deprecated:: 0.00.0
        """

    def parameter_capitalization(self, kind):
        """
        Forgets to capitalize the description.

        Parameters
        ----------
        kind : str
           this is not capitalized.
        """

    def blank_lines(self, kind):
        """
        Adds a blank line after the section header.

        Parameters
        ----------

        kind : str
            Foo bar baz.
        """
        pass

    def integer_parameter(self, kind):
        """
        Uses integer instead of int.

        Parameters
        ----------
        kind : integer
            Foo bar baz.
        """
        pass

    def string_parameter(self, kind):
        """
        Uses string instead of str.

        Parameters
        ----------
        kind : string
            Foo bar baz.
        """
        pass

    def boolean_parameter(self, kind):
        """
        Uses boolean instead of bool.

        Parameters
        ----------
        kind : boolean
            Foo bar baz.
        """
        pass

    def list_incorrect_parameter_type(self, kind):
        """
        Uses list of boolean instead of list of bool.

        Parameters
        ----------
        kind : list of boolean, integer, float or string
            Foo bar baz.
        """
        pass

    def bad_parameter_spacing(self, a, b):
        """
        The parameters on the same line have an extra space between them.

        Parameters
        ----------
        a,  b : int
            Foo bar baz.
        """
        pass


class BadReturns:
    def return_not_documented(self):
        """
        Lacks section for Returns
        """
        return "Hello world!"

    def yield_not_documented(self):
        """
        Lacks section for Yields
        """
        yield "Hello world!"

    def no_type(self):
        """
        Returns documented but without type.

        Returns
        -------
        Some value.
        """
        return "Hello world!"

    def no_description(self):
        """
        Provides type but no descrption.

        Returns
        -------
        str
        """
        return "Hello world!"

    def no_punctuation(self):
        """
        Provides type and description but no period.

        Returns
        -------
        str
           A nice greeting
        """
        return "Hello world!"

    def named_single_return(self):
        """
        Provides name but returns only one value.

        Returns
        -------
        s : str
           A nice greeting.
        """
        return "Hello world!"

    def no_capitalization(self):
        """
        Forgets capitalization in return values description.

        Returns
        -------
        foo : str
           The first returned string.
        bar : str
           the second returned string.
        """
        return "Hello", "World!"

    def no_period_multi(self):
        """
        Forgets period in return values description.

        Returns
        -------
        foo : str
           The first returned string
        bar : str
           The second returned string.
        """
        return "Hello", "World!"


class BadSeeAlso:
    def no_desc(self):
        """
        Return the first 5 elements of the Series.

        See Also
        --------
        Series.tail
        """
        pass

    def desc_no_period(self):
        """
        Return the first 5 elements of the Series.

        See Also
        --------
        Series.tail : Return the last 5 elements of the Series.
        Series.iloc : Return a slice of the elements in the Series,
            which can also be used to return the first or last n
        """
        pass

    def desc_first_letter_lowercase(self):
        """
        Return the first 5 elements of the Series.

        See Also
        --------
        Series.tail : return the last 5 elements of the Series.
        Series.iloc : Return a slice of the elements in the Series,
            which can also be used to return the first or last n.
        """
        pass

    def prefix_pandas(self):
        """
        Have `pandas` prefix in See Also section.

        See Also
        --------
        pandas.Series.rename : Alter Series index labels or name.
        DataFrame.head : The first `n` rows of the caller object.
        """
        pass


class BadExamples:
    def missing_whitespace_around_arithmetic_operator(self):
        """
        Examples
        --------
        >>> 2+5
        7
        """
        pass

    def indentation_is_not_a_multiple_of_four(self):
        """
        Examples
        --------
        >>> if 2 + 5:
        ...   pass
        """
        pass

    def missing_whitespace_after_comma(self):
        """
        Examples
        --------
        >>> import datetime
        >>> value = datetime.date(2019,1,1)
        """
        pass


class TestValidator:
    def _import_path(self, klass=None, func=None):
        """
        Build the required import path for tests in this module.

        Parameters
        ----------
        klass : str
            Class name of object in module.
        func : str
            Function name of object in module.

        Returns
        -------
        str
            Import path of specified object in this module
        """
        base_path = "numpydoc.tests.test_validate"

        if klass:
            base_path = ".".join([base_path, klass])

        if func:
            base_path = ".".join([base_path, func])

        return base_path

    def test_one_liner(self, capsys):
        result = validate_one(self._import_path(klass="GoodDocStrings", func='one_liner'))
        errors = " ".join(err[1] for err in result["errors"])
        assert 'should start in the line immediately after the opening quotes' not in errors
        assert 'should be placed in the line after the last text' not in errors

    def test_good_class(self, capsys):
        errors = validate_one(self._import_path(klass="GoodDocStrings"))["errors"]
        assert isinstance(errors, list)
        assert not errors

    @pytest.mark.parametrize(
        "func",
        [
            "plot",
            "swap",
            "sample",
            "random_letters",
            "sample_values",
            "head",
            "head1",
            "summary_starts_with_number",
            "contains",
            "mode",
            "good_imports",
            "no_returns",
            "empty_returns",
            "multiple_variables_on_one_line",
        ],
    )
    def test_good_functions(self, capsys, func):
        errors = validate_one(self._import_path(klass="GoodDocStrings", func=func))[
            "errors"
        ]
        assert isinstance(errors, list)
        assert not errors

    def test_bad_class(self, capsys):
        errors = validate_one(self._import_path(klass="BadGenericDocStrings"))["errors"]
        assert isinstance(errors, list)
        assert errors

    @pytest.mark.parametrize(
        "func",
        [
            "func",
            "astype",
            "astype1",
            "astype2",
            "astype3",
            "plot",
            "directives_without_two_colons",
        ],
    )
    def test_bad_generic_functions(self, capsys, func):
        errors = validate_one(
            self._import_path(klass="BadGenericDocStrings", func=func)  # noqa:F821
        )["errors"]
        assert isinstance(errors, list)
        assert errors

    @pytest.mark.parametrize(
        "klass,func,msgs",
        [
            # See Also tests
            (
                "BadGenericDocStrings",
                "unknown_section",
                ('Found unknown section "Unknown Section".',),
            ),
            (
                "BadGenericDocStrings",
                "sections_in_wrong_order",
                (
                    "Sections are in the wrong order. Correct order is: Parameters, "
                    "See Also, Examples",
                ),
            ),
            (
                "BadGenericDocStrings",
                "deprecation_in_wrong_order",
                ("Deprecation warning should precede extended summary",),
            ),
            (
                "BadGenericDocStrings",
                "directives_without_two_colons",
                (
                    "reST directives ['versionchanged', 'versionadded', "
                    "'deprecated'] must be followed by two colons",
                ),
            ),
            (
                "BadSeeAlso",
                "no_desc",
                ('Missing description for See Also "Series.tail" reference',),
            ),
            (
                "BadSeeAlso",
                "desc_no_period",
                ('Missing period at end of description for See Also "Series.iloc"',),
            ),
            (
                "BadSeeAlso",
                "desc_first_letter_lowercase",
                ('should be capitalized for See Also "Series.tail"',),
            ),
            # Summary tests
            (
                "BadSummaries",
                "no_summary",
                ("No summary found",),
            ),
            (
                "BadSummaries",
                "heading_whitespaces",
                ("Summary contains heading whitespaces",),
            ),
            (
                "BadSummaries",
                "wrong_line",
                ("should start in the line immediately after the opening quotes",
                 "should be placed in the line after the last text"),
            ),
            ("BadSummaries", "no_punctuation", ("Summary does not end with a period",)),
            (
                "BadSummaries",
                "no_capitalization",
                ("Summary does not start with a capital letter",),
            ),
            (
                "BadSummaries",
                "no_capitalization",
                ("Summary must start with infinitive verb",),
            ),
            ("BadSummaries", "multi_line", ("Summary should fit in a single line",)),
            (
                "BadSummaries",
                "two_paragraph_multi_line",
                ("Summary should fit in a single line",),
            ),
            # Parameters tests
            (
                "BadParameters",
                "no_type",
                ('Parameter "value" has no type',),
            ),
            (
                "BadParameters",
                "type_with_period",
                ('Parameter "value" type should not finish with "."',),
            ),
            (
                "BadParameters",
                "no_description",
                ('Parameter "value" has no description',),
            ),
            (
                "BadParameters",
                "missing_params",
                ("Parameters {'**kwargs'} not documented",),
            ),
            (
                "BadParameters",
                "bad_colon_spacing",
                (
                    'Parameter "kind" requires a space before the colon '
                    "separating the parameter name and type",
                ),
            ),
            (
                "BadParameters",
                "no_description_period",
                ('Parameter "kind" description should finish with "."',),
            ),
            (
                "BadParameters",
                "no_description_period_with_directive",
                ('Parameter "kind" description should finish with "."',),
            ),
            (
                "BadParameters",
                "parameter_capitalization",
                ('Parameter "kind" description should start with a capital letter',),
            ),
            (
                "BadParameters",
                "integer_parameter",
                ('Parameter "kind" type should use "int" instead of "integer"',),
            ),
            (
                "BadParameters",
                "string_parameter",
                ('Parameter "kind" type should use "str" instead of "string"',),
            ),
            (
                "BadParameters",
                "boolean_parameter",
                ('Parameter "kind" type should use "bool" instead of "boolean"',),
            ),
            (
                "BadParameters",
                "list_incorrect_parameter_type",
                ('Parameter "kind" type should use "bool" instead of "boolean"',),
            ),
            (
                "BadParameters",
                "list_incorrect_parameter_type",
                ('Parameter "kind" type should use "int" instead of "integer"',),
            ),
            (
                "BadParameters",
                "list_incorrect_parameter_type",
                ('Parameter "kind" type should use "str" instead of "string"',),
            ),
            (
                "BadParameters",
                "bad_parameter_spacing",
                ("Parameters {'b'} not documented", "Unknown parameters {' b'}"),
            ),
            pytest.param(
                "BadParameters",
                "blank_lines",
                ("No error yet?",),
                marks=pytest.mark.xfail,
            ),
            # Returns tests
            ("BadReturns", "return_not_documented", ("No Returns section found",)),
            ("BadReturns", "yield_not_documented", ("No Yields section found",)),
            pytest.param("BadReturns", "no_type", ("foo",), marks=pytest.mark.xfail),
            ("BadReturns", "no_description", ("Return value has no description",)),
            (
                "BadReturns",
                "no_punctuation",
                ('Return value description should finish with "."',),
            ),
            (
                "BadReturns",
                "named_single_return",
                (
                    "The first line of the Returns section should contain only the "
                    "type, unless multiple values are being returned",
                ),
            ),
            (
                "BadReturns",
                "no_capitalization",
                ("Return value description should start with a capital letter",),
            ),
            (
                "BadReturns",
                "no_period_multi",
                ('Return value description should finish with "."',),
            ),
            (
                "BadGenericDocStrings",
                "method_wo_docstrings",
                ("The object does not have a docstring",),
            ),
            (
                "BadGenericDocStrings",
                "two_linebreaks_between_sections",
                (
                    "Double line break found; please use only one blank line to "
                    "separate sections or paragraphs, and do not leave blank lines "
                    "at the end of docstrings",
                ),
            ),
            (
                "BadGenericDocStrings",
                "linebreak_at_end_of_docstring",
                (
                    "Double line break found; please use only one blank line to "
                    "separate sections or paragraphs, and do not leave blank lines "
                    "at the end of docstrings",
                ),
            ),
        ],
    )
    def test_bad_docstrings(self, capsys, klass, func, msgs):
        with pytest.warns(None) as w:
            result = validate_one(self._import_path(klass=klass, func=func))
        if len(w):
            assert all('Unknown section' in str(ww.message) for ww in w)
        for msg in msgs:
            assert msg in " ".join(err[1] for err in result["errors"])


class TestDocstringClass:
    @pytest.mark.parametrize("invalid_name", ["unknown_mod", "unknown_mod.MyClass"])
    def test_raises_for_invalid_module_name(self, invalid_name):
        msg = 'No module can be imported from "{}"'.format(invalid_name)
        with pytest.raises(ImportError, match=msg):
            numpydoc.validate.Docstring(invalid_name)

    @pytest.mark.parametrize(
        "invalid_name", ["datetime.BadClassName", "datetime.bad_method_name"]
    )
    def test_raises_for_invalid_attribute_name(self, invalid_name):
        name_components = invalid_name.split(".")
        obj_name, invalid_attr_name = name_components[-2], name_components[-1]
        msg = "'{}' has no attribute '{}'".format(obj_name, invalid_attr_name)
        with pytest.raises(AttributeError, match=msg):
            numpydoc.validate.Docstring(invalid_name)
