# -*- coding: utf-8 -*-
"""
    test_autodoc
    ~~~~~~~~~~~~

    Test the autodoc extension.  This tests mainly the Documenters; the auto
    directives are tested in a test source file translated by test_build.

    :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

# "raises" imported for usage by autodoc
import six
import sys
from sphinx.testing.util import SphinxTestApp, Struct
import pytest

from six import StringIO
from docutils.statemachine import ViewList

from sphinx.ext.autodoc import AutoDirective, add_documenter, \
    ModuleLevelDocumenter, FunctionDocumenter, cut_lines, between, ALL

app = None


@pytest.fixture(scope='module', autouse=True)
def setup_module(rootdir, sphinx_test_tempdir):
    global app
    srcdir = sphinx_test_tempdir / 'autodoc-root'
    if not srcdir.exists():
        (rootdir/'test-root').copytree(srcdir)
    app = SphinxTestApp(srcdir=srcdir)
    app.builder.env.app = app
    app.builder.env.temp_data['docname'] = 'dummy'
    app.connect('autodoc-process-docstring', process_docstring)
    app.connect('autodoc-process-signature', process_signature)
    app.connect('autodoc-skip-member', skip_member)
    yield
    app.cleanup()


directive = options = None


@pytest.fixture
def setup_test():
    global options, directive
    global processed_docstrings, processed_signatures, _warnings

    options = Struct(
        inherited_members = False,
        undoc_members = False,
        private_members = False,
        special_members = False,
        imported_members = False,
        show_inheritance = False,
        noindex = False,
        annotation = None,
        synopsis = '',
        platform = '',
        deprecated = False,
        members = [],
        member_order = 'alphabetic',
        exclude_members = set(),
    )

    directive = Struct(
        env = app.builder.env,
        genopt = options,
        result = ViewList(),
        warn = warnfunc,
        filename_set = set(),
    )

    processed_docstrings = []
    processed_signatures = []
    _warnings = []


_warnings = []
processed_docstrings = []
processed_signatures = []


def warnfunc(msg):
    _warnings.append(msg)


def process_docstring(app, what, name, obj, options, lines):
    processed_docstrings.append((what, name))
    if name == 'bar':
        lines.extend(['42', ''])


def process_signature(app, what, name, obj, options, args, retann):
    processed_signatures.append((what, name))
    if name == 'bar':
        return '42', None


def skip_member(app, what, name, obj, skip, options):
    if name in ('__special1__', '__special2__'):
        return skip
    if name.startswith('_'):
        return True
    if name == 'skipmeth':
        return True


@pytest.mark.usefixtures('setup_test')
def test_generate():
    def assert_warns(warn_str, objtype, name, **kw):
        inst = AutoDirective._registry[objtype](directive, name)
        inst.generate(**kw)
        assert len(directive.result) == 0, directive.result
        assert len(_warnings) == 1, _warnings
        assert warn_str in _warnings[0], _warnings
        del _warnings[:]

    def assert_works(objtype, name, **kw):
        inst = AutoDirective._registry[objtype](directive, name)
        inst.generate(**kw)
        assert directive.result
        # print '\n'.join(directive.result)
        assert len(_warnings) == 0, _warnings
        del directive.result[:]

    def assert_processes(items, objtype, name, **kw):
        del processed_docstrings[:]
        del processed_signatures[:]
        assert_works(objtype, name, **kw)
        assert set(processed_docstrings) | set(processed_signatures) == set(items)

    def assert_result_contains(item, objtype, name, **kw):
        inst = AutoDirective._registry[objtype](directive, name)
        inst.generate(**kw)
        # print '\n'.join(directive.result)
        assert len(_warnings) == 0, _warnings
        assert item in directive.result
        del directive.result[:]

    def assert_order(items, objtype, name, member_order, **kw):
        inst = AutoDirective._registry[objtype](directive, name)
        inst.options.member_order = member_order
        inst.generate(**kw)
        assert len(_warnings) == 0, _warnings
        items = list(reversed(items))
        lineiter = iter(directive.result)
        # for line in directive.result:
        #     if line.strip():
        #         print repr(line)
        while items:
            item = items.pop()
            for line in lineiter:
                if line == item:
                    break
            else:  # ran out of items!
                assert False, ('item %r not found in result or not in the '
                               ' correct order' % item)
        del directive.result[:]

    options.members = []

    # no module found?
    assert_warns("import for autodocumenting 'foobar'",
                 'function', 'foobar', more_content=None)
    # importing
    assert_warns("failed to import module 'test_foobar'",
                 'module', 'test_foobar', more_content=None)
    # attributes missing
    assert_warns("failed to import function 'foobar' from module 'util'",
                 'function', 'util.foobar', more_content=None)
    # method missing
    assert_warns("failed to import method 'Class.foobar' from module 'test_autodoc_py35';",
                 'method', 'test_autodoc_py35.Class.foobar', more_content=None)

    # test auto and given content mixing
    directive.env.ref_context['py:module'] = 'test_autodoc_py35'
    assert_result_contains('   Function.', 'method', 'Class.meth')
    add_content = ViewList()
    add_content.append('Content.', '', 0)
    assert_result_contains('   Function.', 'method',
                           'Class.meth', more_content=add_content)
    assert_result_contains('   Content.', 'method',
                           'Class.meth', more_content=add_content)

    # test check_module
    inst = FunctionDocumenter(directive, 'add_documenter')
    inst.generate(check_module=True)
    assert len(directive.result) == 0

    # assert that exceptions can be documented
    assert_works('exception', 'test_autodoc_py35.CustomEx', all_members=True)
    assert_works('exception', 'test_autodoc_py35.CustomEx')

    # test diverse inclusion settings for members
    should = [('class', 'test_autodoc_py35.Class')]
    assert_processes(should, 'class', 'Class')
    should.extend([('method', 'test_autodoc_py35.Class.meth')])
    options.members = ['meth']
    options.exclude_members = set(['excludemeth'])
    assert_processes(should, 'class', 'Class')
    should.extend([('attribute', 'test_autodoc_py35.Class.prop'),
                   ('attribute', 'test_autodoc_py35.Class.descr'),
                   ('attribute', 'test_autodoc_py35.Class.attr'),
                   ('attribute', 'test_autodoc_py35.Class.docattr'),
                   ('attribute', 'test_autodoc_py35.Class.udocattr'),
                   ('attribute', 'test_autodoc_py35.Class.mdocattr'),
                   ('attribute', 'test_autodoc_py35.Class.inst_attr_comment'),
                   ('attribute', 'test_autodoc_py35.Class.inst_attr_inline'),
                   ('attribute', 'test_autodoc_py35.Class.inst_attr_string'),
                   ('method', 'test_autodoc_py35.Class.moore'),
                   ])
    if six.PY3 and sys.version_info[:2] >= (3, 5):
        should.extend([
            ('method', 'test_autodoc_py35.Class.do_coroutine'),
        ])
    options.members = ALL
    assert_processes(should, 'class', 'Class')
    options.undoc_members = True
    should.extend((('attribute', 'test_autodoc_py35.Class.skipattr'),
                   ('method', 'test_autodoc_py35.Class.undocmeth'),
                   ('method', 'test_autodoc_py35.Class.roger')))
    assert_processes(should, 'class', 'Class')
    options.inherited_members = True
    should.append(('method', 'test_autodoc_py35.Class.inheritedmeth'))
    assert_processes(should, 'class', 'Class')

    # test special members
    options.special_members = ['__special1__']
    should.append(('method', 'test_autodoc_py35.Class.__special1__'))
    assert_processes(should, 'class', 'Class')
    options.special_members = ALL
    should.append(('method', 'test_autodoc_py35.Class.__special2__'))
    assert_processes(should, 'class', 'Class')
    options.special_members = False


# --- generate fodder ------------
__all__ = ['Class']

#: documentation for the integer
integer = 1


class CustomEx(Exception):
    """My custom exception."""

    def f(self):
        """Exception method."""


class CustomDataDescriptor(object):
    """Descriptor class docstring."""

    def __init__(self, doc):
        self.__doc__ = doc

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return 42

    def meth(self):
        """Function."""
        return "The Answer"


def _funky_classmethod(name, b, c, d, docstring=None):
    """Generates a classmethod for a class from a template by filling out
    some arguments."""
    def template(cls, a, b, c, d=4, e=5, f=6):
        return a, b, c, d, e, f
    from functools import partial
    function = partial(template, b=b, c=c, d=d)
    function.__name__ = name
    function.__doc__ = docstring
    return classmethod(function)


class Base(object):
    def inheritedmeth(self):
        """Inherited function."""

if six.PY3 and sys.version_info[:2] >= (3, 5):

    async def _other_coro_func():
        return "run"


class Class(Base):
    """Class to document."""

    descr = CustomDataDescriptor("Descriptor instance docstring.")

    def meth(self):
        """Function."""

    def undocmeth(self):
        pass

    def skipmeth(self):
        """Method that should be skipped."""

    def excludemeth(self):
        """Method that should be excluded."""

    # should not be documented
    skipattr = 'foo'

    #: should be documented -- süß
    attr = 'bar'

    @property
    def prop(self):
        """Property."""

    docattr = 'baz'
    """should likewise be documented -- süß"""

    udocattr = 'quux'
    u"""should be documented as well - süß"""

    # initialized to any class imported from another module
    mdocattr = StringIO()
    """should be documented as well - süß"""

    roger = _funky_classmethod("roger", 2, 3, 4)

    moore = _funky_classmethod("moore", 9, 8, 7,
                               docstring="moore(a, e, f) -> happiness")

    def __init__(self, arg):
        self.inst_attr_inline = None  #: an inline documented instance attr
        #: a documented instance attribute
        self.inst_attr_comment = None
        self.inst_attr_string = None
        """a documented instance attribute"""

    def __special1__(self):
        """documented special method"""

    def __special2__(self):
        # undocumented special method
        pass

    if six.PY3 and sys.version_info[:2] >= (3, 5):

        async def do_coroutine(self):
            """A documented coroutine function"""

            attr_coro_result = await _other_coro_func()
