#
# Copyright (c) 2010 Doug Hellmann.  All rights reserved.
#
"""Tests for SpellingBuilder"""

import contextlib
import io
import os
import sys
import textwrap

import pytest
from sphinx.application import Sphinx

from tests import helpers  # isort:skip


def _make_sphinx_project(tmpdir):
    srcdir = tmpdir.mkdir("src")
    outdir = tmpdir.mkdir("out")
    add_file(
        srcdir,
        "conf.py",
        """
    extensions = [ 'sphinxcontrib.spelling' ]
    """,
    )
    return (srcdir, outdir)


@pytest.fixture
def sphinx_project(tmpdir):
    yield _make_sphinx_project(tmpdir)


@contextlib.contextmanager
def working_dir(targetdir):
    "Temporarily change the working directory of the process."
    before = os.getcwd()
    os.chdir(targetdir)
    try:
        yield os.getcwd()
    finally:
        os.chdir(before)


@contextlib.contextmanager
def import_path(new_path):
    "Temporarily change sys.path for imports."
    before = sys.path
    try:
        sys.path = new_path
        yield
    finally:
        sys.path = before


def add_file(thedir, filename, content):
    with open(thedir.join(filename), "w") as f:
        f.write(textwrap.dedent(content))


def get_sphinx_app(srcdir, outdir, docname, builder="spelling"):
    stdout = io.StringIO()
    stderr = io.StringIO()
    app = Sphinx(
        srcdir,
        srcdir,
        outdir,
        outdir,
        builder,
        status=stdout,
        warning=stderr,
        freshenv=True,
    )
    return (stdout, stderr, app)


def get_sphinx_output(srcdir, outdir, docname, builder="spelling"):
    stdout, stderr, app = get_sphinx_app(srcdir, outdir, docname, builder)
    app.build()
    path = os.path.join(outdir, f"{docname}.spelling")
    try:
        with open(path, "r") as f:
            output_text = f.read()
    except FileNotFoundError:
        output_text = None
    return (stdout, stderr, output_text)


def test_setup(sphinx_project):
    srcdir, outdir = sphinx_project
    stdout = io.StringIO()
    stderr = io.StringIO()
    # If the spelling builder is not properly initialized,
    # trying to use it with the Sphinx app class will
    # generate an exception.
    Sphinx(
        str(srcdir),
        str(srcdir),
        str(outdir),
        str(outdir),
        "spelling",
        status=stdout,
        warning=stderr,
        freshenv=True,
    )


def test_title(sphinx_project):
    srcdir, outdir = sphinx_project
    add_file(
        srcdir,
        "contents.rst",
        """
    Welcome to Speeling Checker documentation!
    ==========================================
    """,
    )
    stdout, stderr, output_text = get_sphinx_output(srcdir, outdir, "contents")
    assert "(Speeling)" in output_text


def test_body(sphinx_project):
    srcdir, outdir = sphinx_project
    add_file(
        srcdir,
        "contents.rst",
        """
    Welcome to Spelling Checker documentation!
    ==========================================

    There are several mispelled words in this txt.
    """,
    )
    stdout, stderr, output_text = get_sphinx_output(srcdir, outdir, "contents")
    assert "(mispelled)" in output_text
    assert "(txt)" in output_text


def test_ignore_literals(sphinx_project):
    srcdir, outdir = sphinx_project
    add_file(
        srcdir,
        "contents.rst",
        """
    Welcome to Spelling Checker documentation!
    ==========================================

    There are several misspelled words in this text.

    ::

        Literal blocks are ignoreed.

    Inline ``litterals`` are ignored, too.

    """,
    )
    stdout, stderr, output_text = get_sphinx_output(srcdir, outdir, "contents")
    # The 'contents.spelling' output file should not have been
    # created, because the errors are ignored.
    assert output_text is None


def test_several_word_lists(sphinx_project):
    srcdir, outdir = sphinx_project
    add_file(
        srcdir,
        "conf.py",
        """
    extensions = ['sphinxcontrib.spelling']
    spelling_word_list_filename=['test_wordlist.txt','test_wordlist2.txt']
    """,
    )

    add_file(
        srcdir,
        "contents.rst",
        """
    Welcome to Spelling Checker documentation!
    ==========================================

    There are several mispelled words in tihs txt.
    """,
    )

    add_file(
        srcdir,
        "test_wordlist.txt",
        """
    txt
    """,
    )

    add_file(
        srcdir,
        "test_wordlist2.txt",
        """
    mispelled
    """,
    )
    stdout, stderr, output_text = get_sphinx_output(srcdir, outdir, "contents")
    # Both of these should be fine now
    assert "(mispelled)" not in output_text
    assert "(txt)" not in output_text
    # But not this one
    assert "(tihs)" in output_text


def _wordlist_sphinx_project(tmpdir, conf_contents):
    srcdir, outdir = _make_sphinx_project(tmpdir)
    add_file(srcdir, "conf.py", conf_contents)
    add_file(
        srcdir,
        "test_wordlist.txt",
        """
    txt
    """,
    )
    add_file(
        srcdir,
        "test_wordlist2.txt",
        """
    mispelled
    """,
    )
    stdout, stderr, app = get_sphinx_app(srcdir, outdir, "contents")
    return (srcdir, outdir, stdout, stderr, app)


def test_word_list_default(tmpdir):
    srcdir, outdir, stdout, stderr, app = _wordlist_sphinx_project(
        tmpdir,
        """
        extensions = ['sphinxcontrib.spelling']
        """,
    )
    results = app.builder.get_configured_wordlist_filenames()
    assert len(results) == 1
    assert os.path.basename(results[0]) == "spelling_wordlist.txt"


def test_one_word_list_str(tmpdir):
    srcdir, outdir, stdout, stderr, app = _wordlist_sphinx_project(
        tmpdir,
        """
        extensions = ['sphinxcontrib.spelling']
        spelling_word_list_filename='test_wordlist.txt'
        """,
    )
    results = app.builder.get_configured_wordlist_filenames()
    assert len(results) == 1
    assert os.path.basename(results[0]) == "test_wordlist.txt"


def test_multiple_word_list_str(tmpdir):
    # We don't expect anyone to set up their conf.py this way but it
    # simulates passing the configuration option from the command line
    # using -D.
    srcdir, outdir, stdout, stderr, app = _wordlist_sphinx_project(
        tmpdir,
        """
        extensions = ['sphinxcontrib.spelling']
        spelling_word_list_filename='test_wordlist.txt,test_wordlist2.txt'
        """,
    )
    results = app.builder.get_configured_wordlist_filenames()
    assert len(results) == 2
    assert os.path.basename(results[0]) == "test_wordlist.txt"
    assert os.path.basename(results[1]) == "test_wordlist2.txt"


def test_multiple_word_list_list(tmpdir):
    srcdir, outdir, stdout, stderr, app = _wordlist_sphinx_project(
        tmpdir,
        """
        extensions = ['sphinxcontrib.spelling']
        spelling_word_list_filename=['test_wordlist.txt', 'test_wordlist2.txt']
        """,
    )
    results = app.builder.get_configured_wordlist_filenames()
    assert len(results) == 2
    assert os.path.basename(results[0]) == "test_wordlist.txt"
    assert os.path.basename(results[1]) == "test_wordlist2.txt"


def test_ignore_file(sphinx_project):
    srcdir, outdir = sphinx_project
    add_file(
        srcdir,
        "conf.py",
        """
    extensions = ['sphinxcontrib.spelling']
    spelling_exclude_patterns=['con*']
    """,
    )

    add_file(
        srcdir,
        "contents.rst",
        """
    Welcome to Speeling Checker documentation!
    ==========================================
    """,
    )

    stdout, stderr, output_text = get_sphinx_output(srcdir, outdir, "contents")
    # The 'contents.spelling' output file should not have been
    # created, because the file is ignored.
    assert output_text is None


@helpers.require_git_repo
def test_docstrings(sphinx_project):
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "conf.py",
        """
    extensions = ['sphinxcontrib.spelling', 'sphinx.ext.autodoc']
    """,
    )

    add_file(
        srcdir / "..",
        "the_source.py",
        '''
    #!/usr/bin/env python3

    def public_function(arg_name):
        """Does something useful.

        :param arg_name: Pass a vaule
        """
        return 1
    ''',
    )

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    .. automodule:: the_source
       :members:

    """,
    )

    with working_dir(srcdir / ".."):
        with import_path(["."] + sys.path):
            stdout, stderr, output_text = get_sphinx_output(
                srcdir,
                outdir,
                "contents",
            )

    expected = "src/contents.rst:3: (vaule)  Pass a vaule\n"
    assert expected in output_text


def test_get_suggestions_to_show_all(sphinx_project):
    srcdir, outdir = sphinx_project
    add_file(
        srcdir,
        "conf.py",
        """
    extensions = ['sphinxcontrib.spelling']
    spelling_show_suggestions = True
    spelling_suggestion_limit = 0
    """,
    )
    stdout, stderr, app = get_sphinx_app(srcdir, outdir, "contents")
    results = app.builder.get_suggestions_to_show(["a", "b", "c"])
    assert len(results) == 3


def test_get_suggestions_to_show_limit(sphinx_project):
    srcdir, outdir = sphinx_project
    add_file(
        srcdir,
        "conf.py",
        """
    extensions = ['sphinxcontrib.spelling']
    spelling_show_suggestions = True
    spelling_suggestion_limit = 1
    """,
    )
    stdout, stderr, app = get_sphinx_app(srcdir, outdir, "contents")
    results = app.builder.get_suggestions_to_show(["a", "b", "c"])
    assert len(results) == 1


def test_get_suggestions_to_show_disabled(sphinx_project):
    srcdir, outdir = sphinx_project
    add_file(
        srcdir,
        "conf.py",
        """
    extensions = ['sphinxcontrib.spelling']
    spelling_show_suggestions = False
    spelling_suggestion_limit = 0
    """,
    )
    stdout, stderr, app = get_sphinx_app(srcdir, outdir, "contents")
    results = app.builder.get_suggestions_to_show(["a", "b", "c"])
    assert len(results) == 0


def test_captions(sphinx_project):
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    .. figure:: blah.gif

       Teh caption

    """,
    )

    stdout, stderr, output_text = get_sphinx_output(
        srcdir,
        outdir,
        "contents",
    )
    assert "(Teh)" in output_text


def test_legacy_directive(sphinx_project):
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    .. spelling::

       teh

    teh is OK

    """,
    )

    stdout, stderr, output_text = get_sphinx_output(
        srcdir,
        outdir,
        "contents",
    )
    assert output_text is None


def test_domain_directive(sphinx_project):
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    .. spelling:word-list::

       teh

    teh is OK

    """,
    )

    stdout, stderr, output_text = get_sphinx_output(
        srcdir,
        outdir,
        "contents",
    )
    assert output_text is None


def test_domain_role(sphinx_project):
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    :spelling:word:`teh` is OK

    """,
    )

    stdout, stderr, output_text = get_sphinx_output(
        srcdir,
        outdir,
        "contents",
    )
    assert output_text is None


def test_domain_role_multiple_words(sphinx_project):
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    :spelling:word:`teh is KO`

    """,
    )

    stdout, stderr, output_text = get_sphinx_output(
        srcdir,
        outdir,
        "contents",
    )
    assert output_text is None


def test_domain_role_output(sphinx_project):
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    :spelling:word:`teh` is OK

    """,
    )

    stdout, stderr, output_text = get_sphinx_output(
        srcdir,
        outdir,
        "contents",
        "text",
    )

    path = os.path.join(outdir, "contents.txt")
    try:
        with open(path, "r") as f:
            output_text = f.read()
    except FileNotFoundError:
        output_text = None

    assert output_text == "The Module\n**********\n\nteh is OK\n"


def test_domain_ignore(sphinx_project):
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    :spelling:ignore:`baddddd` is OK

    """,
    )

    stdout, stderr, output_text = get_sphinx_output(
        srcdir,
        outdir,
        "contents",
    )
    assert output_text is None


def test_domain_ignore_multiple_words(sphinx_project):
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    :spelling:ignore:`baddddd` is OK here.

    But, baddddd is not OK here.
    Nor, here baddddd.

    """,
    )

    stdout, stderr, output_text = get_sphinx_output(
        srcdir,
        outdir,
        "contents",
    )
    assert "(baddddd)" in output_text
    assert output_text.count("\n") == 2  # Only expect 2 errors, not 3.


def test_domain_ignore_output(sphinx_project):
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    :spelling:ignore:`teh` is OK

    """,
    )

    stdout, stderr, output_text = get_sphinx_output(
        srcdir,
        outdir,
        "contents",
        "text",
    )

    path = os.path.join(outdir, "contents.txt")
    try:
        with open(path, "r") as f:
            output_text = f.read()
    except FileNotFoundError:
        output_text = None

    assert output_text == "The Module\n**********\n\nteh is OK\n"


def test_only_directive(sphinx_project):
    # How to skip checking nested blocks of content
    # https://github.com/sphinx-contrib/spelling/issues/204
    srcdir, outdir = sphinx_project

    add_file(
        srcdir,
        "contents.rst",
        """
    The Module
    ==========

    .. only:: html

       teh is ok

    whaat is not ok
    """,
    )

    stdout, stderr, output_text = get_sphinx_output(
        srcdir,
        outdir,
        "contents",
    )
    assert "(whaat)" in output_text
    assert "(teh)" not in output_text


def test_nodes_with_none_source(sphinx_project):
    # Reproduces https://github.com/sphinx-contrib/spelling/issues/234
    # Tests handling of nodes where get_source_line returns None for source
    srcdir, outdir = sphinx_project

    # Create an extension that patches docutils.utils.get_source_line to return None
    # This directly simulates the bug condition
    add_file(
        srcdir,
        "mock_none_source.py",
        """
import docutils.utils


# Store the original function
_original_get_source_line = docutils.utils.get_source_line


def patched_get_source_line(node):
    '''Return None for source to simulate issue #234'''
    # Call original to get line number
    source, lineno = _original_get_source_line(node)
    # But return None for source to trigger the bug
    return (None, lineno)


def setup(app):
    # Patch the function when building
    def on_build_finished(app, exception):
        # Restore original after build
        docutils.utils.get_source_line = _original_get_source_line

    # Patch before doctree-read
    def on_doctree_read(app, doctree):
        docutils.utils.get_source_line = patched_get_source_line

    app.connect('doctree-read', on_doctree_read, priority=1)
    app.connect('build-finished', on_build_finished)

    return {'version': '0.1'}
    """,
    )

    add_file(
        srcdir,
        "conf.py",
        f"""
import sys
sys.path.insert(0, r'{srcdir}')
extensions = ['sphinxcontrib.spelling', 'mock_none_source']
    """,
    )

    add_file(
        srcdir,
        "contents.rst",
        """
    Test Document
    =============

    This text has a mispeling that should be caught.
    """,
    )

    # Without the fix, this will crash with:
    # TypeError: expected str, bytes or os.PathLike object, not NoneType
    # With the fix, it should handle None gracefully and produce output
    stdout, stderr, output_text = get_sphinx_output(srcdir, outdir, "contents")

    # The spelling check should still work and find the misspelling
    assert output_text is not None
    assert "(mispeling)" in output_text
