from typing import List
from textwrap import dedent

from pydoctor.epydoc.markup import DocstringLinker, ParseError, ParsedDocstring, get_parser_by_name
from pydoctor.epydoc.markup.restructuredtext import parse_docstring
from pydoctor.test import NotFoundLinker
from pydoctor.node2stan import node2stan
from pydoctor.stanutils import flatten, flatten_text

from docutils import nodes, __version_info__ as docutils_version_info
from bs4 import BeautifulSoup
import pytest

def prettify(html: str) -> str:
    return BeautifulSoup(html, features="html.parser").prettify()  # type: ignore[no-any-return]

def parse_rst(s: str) -> ParsedDocstring:
    errors: List[ParseError] = []
    parsed = parse_docstring(s, errors)
    assert not errors
    return parsed

def rst2html(docstring: str, linker: DocstringLinker = NotFoundLinker()) -> str:
    """
    Render a docstring to HTML.
    """
    return flatten(parse_rst(docstring).to_stan(linker))
    
def node2html(node: nodes.Node, oneline: bool = True) -> str:
    if oneline:
        return ''.join(prettify(flatten(node2stan(node, NotFoundLinker()))).splitlines())
    else:
        return flatten(node2stan(node, NotFoundLinker()))

def rst2node(s: str) -> nodes.document:
    return parse_rst(s).to_node()

def test_rst_partial() -> None:
    """
    The L{node2html()} function can convert fragment of a L{docutils} document, 
    it's not restricted to actual L{docutils.nodes.document} object. 
    
    Really, any nodes can be passed to that function, the only requirement is 
    that the node's C{document} attribute is set to a valid L{docutils.nodes.document} object.
    """
    doc = dedent('''
        This is a paragraph.  Paragraphs can
        span multiple lines, and can contain
        `inline markup`.

        This is another paragraph.  Paragraphs
        are separated by blank lines.
        ''')
    expected = dedent('''
        <p>This is another paragraph.  Paragraphs
        are separated by blank lines.</p>
        ''').lstrip()
    
    node = rst2node(doc)

    for child in node[:]:
          assert isinstance(child, nodes.paragraph)
    
    assert node2html(node[-1], oneline=False) == expected
    assert node[-1].parent == node

def test_rst_body_empty() -> None:
    src = """
    :return: a number
    :rtype: int
    """
    errors: List[ParseError] = []
    pdoc = parse_docstring(src, errors)
    assert not errors
    assert not pdoc.has_body
    assert len(pdoc.fields) == 2

def test_rst_body_nonempty() -> None:
    src = """
    Only body text, no fields.
    """
    errors: List[ParseError] = []
    pdoc = parse_docstring(src, errors)
    assert not errors
    assert pdoc.has_body
    assert len(pdoc.fields) == 0

def test_rst_anon_link_target_missing() -> None:
    src = """
    This link's target is `not defined anywhere`__.
    """
    errors: List[ParseError] = []
    parse_docstring(src, errors)
    assert len(errors) == 1
    assert errors[0].descr().startswith("Anonymous hyperlink mismatch:")
    assert errors[0].is_fatal()

def test_rst_anon_link_email() -> None:
    src = "`<postmaster@example.net>`__"
    html = rst2html(src)
    assert html.startswith('<a ')
    assert ' href="mailto:postmaster@example.net"' in html
    assert html.endswith('>mailto:postmaster@example.net</a>')

def test_rst_xref_with_target() -> None:
    src = "`mapping <typing.MutableMapping>`"
    html = rst2html(src)
    assert html == '<code><a>mapping</a></code>'

def test_rst_xref_implicit_target() -> None:
    src = "`func()`"
    html = rst2html(src)
    assert html == '<code><a>func()</a></code>'

def test_rst_directive_adnomitions() -> None:
    expected_html_multiline="""
        <div class="rst-admonition rst-{}">
        <p class="rst-admonition-title rst-first">{}</p>
        <p>this is the first line</p>
        <p class="rst-last">and this is the second line</p>
        </div>
"""

    expected_html_single_line = """
        <div class="rst-admonition rst-{}">
        <p class="rst-admonition-title rst-first">{}</p>
        <p class="rst-last">this is a single line</p>
        </div>
"""

    admonition_map = {
            'Attention': 'attention',
            'Caution': 'caution',
            'Danger': 'danger',
            'Error': 'error',
            'Hint': 'hint',
            'Important': 'important',
            'Note': 'note',
            'Tip': 'tip',
            'Warning': 'warning',
        }

    for title, admonition_name in admonition_map.items():
        # Multiline
        docstring = (".. {}::\n"
                    "\n"
                    "   this is the first line\n"
                    "   \n"
                    "   and this is the second line\n"
                    ).format(admonition_name)

        expect = expected_html_multiline.format(
            admonition_name, title
        )

        actual = rst2html(docstring)

        assert prettify(expect)==prettify(actual)

        # Single line
        docstring = (".. {}:: this is a single line\n"
                    ).format(admonition_name)

        expect = expected_html_single_line.format(
            admonition_name, title
        )

        actual = rst2html(docstring)

        assert prettify(expect)==prettify(actual)


def test_rst_directive_versionadded() -> None:
    """
    It renders the C{versionadded} RST directive using a custom markup with
    dedicated CSS classes.
    """
    html = rst2html(".. versionadded:: 0.6")
    expected_html="""<div class="rst-versionadded">
<span class="rst-added rst-versionmodified">New in version 0.6.</span></div>
"""
    assert html==expected_html, html

def test_rst_directive_versionchanged() -> None:
    """
    It renders the C{versionchanged} RST directive with custom markup and supports
    an extra text besides the version information.
    """
    html = rst2html(""".. versionchanged:: 0.7
    Add extras""")
    expected_html="""<div class="rst-versionchanged">
<span class="rst-changed rst-versionmodified">Changed in version 0.7: </span><span>Add extras</span></div>
"""
    assert html==expected_html, html

def test_rst_directive_deprecated() -> None:
    """
    It renders the C{deprecated} RST directive with custom markup and supports
    an extra text besides the version information.
    """
    html = rst2html(""".. deprecated:: 0.2
    For security reasons""")
    expected_html="""<div class="rst-deprecated">
<span class="rst-deprecated rst-versionmodified">Deprecated since version 0.2: </span><span>For security reasons</span></div>
"""
    assert html==expected_html, html
    
def test_rst_directive_seealso() -> None:

    html = rst2html(".. seealso:: Hey")
    expected_html = """
        <div class="rst-admonition rst-seealso">
        <p class="rst-admonition-title rst-first">See Also</p>
        <p class="rst-last">Hey</p>
        </div>"""
    assert prettify(html).strip() == prettify(expected_html).strip(), html

@pytest.mark.parametrize(
    'markup', ('epytext', 'plaintext', 'restructuredtext', 'numpy', 'google')
    )
def test_summary(markup:str) -> None:
    """
    Summaries are generated from the inline text inside the first paragraph. 
    The text is trimmed as soon as we reach a break point (or another docutils element) after 200 characters.
    """
    cases = [
        ("Single line", "Single line"), 
        ("Single line.", "Single line."), 
        ("Single line with period.", "Single line with period."),
        ("""
        Single line with period.
        
        @type: Also with a tag.
        """, "Single line with period."), 
        ("Other lines with period.\nThis is attached", "Other lines with period. This is attached"),
        ("Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. ", 
         "Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line. Single line..."),
        ("Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line. Single line Single line Single line ", 
         "Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line..."),
        ("Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line.",
         "Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line Single line."),
        ("""
        Return a fully qualified name for the possibly-dotted name.

        To explain what this means, consider the following modules... blabla""",
        "Return a fully qualified name for the possibly-dotted name.")
    ]
    for src, summary_text in cases:
        errors: List[ParseError] = []
        pdoc = get_parser_by_name(markup)(dedent(src), errors)
        assert not errors
        assert flatten_text(pdoc.get_summary().to_stan(NotFoundLinker())) == summary_text


# From docutils 0.18 the toc entries uses different ids.
@pytest.mark.skipif(docutils_version_info < (0,18), reason="HTML ids in toc tree changed in docutils 0.18.0.")
def test_get_toc() -> None:

    docstring = """
Titles
======

Level 2
-------

Level 3
~~~~~~~

Level 4
^^^^^^^

Level 5
!!!!!!!

Level 2.2
---------

Level 22
--------

Lists
=====

Other
=====
"""

    errors: List[ParseError] = []
    parsed = parse_docstring(docstring, errors)
    assert not errors, [str(e.descr) for e in errors]
    
    toc = parsed.get_toc(4)
    assert toc is not None
    html = flatten(toc.to_stan(NotFoundLinker()))
    
    expected_html="""
<li>
 <p class="rst-first">
  <a class="rst-internal rst-reference" href="#rst-titles" id="rst-toc-entry-1">
   Titles
  </a>
 </p>
 <ul class="rst-simple">
  <li>
   <a class="rst-internal rst-reference" href="#rst-level-2" id="rst-toc-entry-2">
    Level 2
   </a>
   <ul>
    <li>
     <a class="rst-internal rst-reference" href="#rst-level-3" id="rst-toc-entry-3">
      Level 3
     </a>
     <ul>
      <li>
       <a class="rst-internal rst-reference" href="#rst-level-4" id="rst-toc-entry-4">
        Level 4
       </a>
      </li>
     </ul>
    </li>
   </ul>
  </li>
  <li>
   <a class="rst-internal rst-reference" href="#rst-level-2-2" id="rst-toc-entry-5">
    Level 2.2
   </a>
  </li>
  <li>
   <a class="rst-internal rst-reference" href="#rst-level-22" id="rst-toc-entry-6">
    Level 22
   </a>
  </li>
 </ul>
</li>
<li>
 <a class="rst-internal rst-reference" href="#rst-lists" id="rst-toc-entry-7">
  Lists
 </a>
</li>
<li>
 <a class="rst-internal rst-reference" href="#rst-other" id="rst-toc-entry-8">
  Other
 </a>
</li>
"""
    assert prettify(html) == prettify(expected_html)

