import argparse
import importlib.metadata
from textwrap import dedent
from unittest.mock import patch

from markdown_it import MarkdownIt
import pytest

import mdformat
from mdformat._cli import run
from mdformat.plugins import (
    _PARSER_EXTENSION_DISTS,
    CODEFORMATTERS,
    PARSER_EXTENSIONS,
    _load_entrypoints,
)
from mdformat.renderer import MDRenderer
from tests.utils import (
    ASTChangingPlugin,
    JSONFormatterPlugin,
    PrefixPostprocessPlugin,
    SuffixPostprocessPlugin,
    TablePlugin,
    TextEditorPlugin,
)


def test_code_formatter(monkeypatch):
    def fmt_func(code, info):
        return "dummy\n"

    monkeypatch.setitem(CODEFORMATTERS, "lang", fmt_func)
    text = mdformat.text(
        dedent(
            """\
    ```lang
    a
    ```
    """
        ),
        codeformatters={"lang"},
    )
    assert text == dedent(
        """\
    ```lang
    dummy
    ```
    """
    )


def test_code_formatter__empty_str(monkeypatch):
    def fmt_func(code, info):
        return ""

    monkeypatch.setitem(CODEFORMATTERS, "lang", fmt_func)
    text = mdformat.text(
        dedent(
            """\
    ~~~lang
    aag
    gw
    ~~~
    """
        ),
        codeformatters={"lang"},
    )
    assert text == dedent(
        """\
    ```lang
    ```
    """
    )


def test_code_formatter__no_end_newline(monkeypatch):
    def fmt_func(code, info):
        return "dummy\ndum"

    monkeypatch.setitem(CODEFORMATTERS, "lang", fmt_func)
    text = mdformat.text(
        dedent(
            """\
    ```lang
    ```
    """
        ),
        codeformatters={"lang"},
    )
    assert text == dedent(
        """\
    ```lang
    dummy
    dum
    ```
    """
    )


def test_code_formatter__interface(monkeypatch):
    def fmt_func(code, info):
        return info + code * 2

    monkeypatch.setitem(CODEFORMATTERS, "lang", fmt_func)
    text = mdformat.text(
        dedent(
            """\
    ```    lang  long
    multi
    mul
    ```
    """
        ),
        codeformatters={"lang"},
    )
    assert text == dedent(
        """\
    ```lang  long
    lang  longmulti
    mul
    multi
    mul
    ```
    """
    )


def test_single_token_extension(monkeypatch):
    """Test the front matter plugin, as a single token extension example."""
    plugin_name = "text_editor"
    monkeypatch.setitem(PARSER_EXTENSIONS, plugin_name, TextEditorPlugin)
    text = mdformat.text(
        dedent(
            """\
    # Example Heading

    Example paragraph.
    """
        ),
        extensions=[plugin_name],
    )
    assert text == dedent(
        """\
    # All text is like this now!

    All text is like this now!
    """
    )


def test_table(monkeypatch):
    """Test the table plugin, as a multi-token extension example."""
    monkeypatch.setitem(PARSER_EXTENSIONS, "table", TablePlugin)
    text = mdformat.text(
        dedent(
            """\
    |a|b|
    |-|-|
    |c|d|

    other text
    """
        ),
        extensions=["table", "table"],
    )
    assert text == dedent(
        """\
    dummy 21

    other text
    """
    )


class ExamplePluginWithGroupedCli:
    """A plugin that adds CLI options."""

    @staticmethod
    def update_mdit(mdit: MarkdownIt):
        mdit.enable("table")

    @staticmethod
    def add_cli_argument_group(group: argparse._ArgumentGroup) -> None:
        group.add_argument("--o1", type=str)
        group.add_argument("--o2", type=str, default="a")
        group.add_argument("--o3", dest="arg_name", type=int)
        group.add_argument("--override-toml")
        group.add_argument("--dont-override-toml", action="store_const", const=True)


def test_cli_options_group(monkeypatch, tmp_path):
    """Test that CLI arguments added by plugins are correctly added to the
    options dict.

    Use add_cli_argument_group plugin API.
    """
    monkeypatch.setitem(PARSER_EXTENSIONS, "table", ExamplePluginWithGroupedCli)
    file_path = tmp_path / "test_markdown.md"
    conf_path = tmp_path / ".mdformat.toml"
    file_path.touch()
    conf_path.write_text(
        """\
[plugin.table]
override_toml = 'failed'
toml_only = true
dont_override_toml = 'dont override this with None if CLI opt not given'
"""
    )

    with patch.object(MDRenderer, "render", return_value="") as mock_render:
        assert (
            run(
                (
                    str(file_path),
                    "--o1",
                    "other",
                    "--o3",
                    "4",
                    "--override-toml",
                    "success",
                )
            )
            == 0
        )

    (call_,) = mock_render.call_args_list
    posargs = call_[0]
    # Options is the second positional arg of MDRender.render
    opts = posargs[1]
    table_opts = opts["mdformat"]["plugin"]["table"]
    assert table_opts["o1"] == "other"
    assert table_opts["o2"] == "a"
    assert table_opts["arg_name"] == 4
    assert table_opts["override_toml"] == "success"
    assert table_opts["toml_only"] is True
    assert (
        table_opts["dont_override_toml"]
        == "dont override this with None if CLI opt not given"
    )


def test_plugin_argument_warnings(monkeypatch, tmp_path):
    """Test for warnings of plugin arguments that conflict with TOML."""

    class ExamplePluginWithStoreTrue:
        @staticmethod
        def update_mdit(mdit: MarkdownIt):
            pass

        @staticmethod
        def add_cli_argument_group(group: argparse._ArgumentGroup) -> None:
            group.add_argument("--store-true", action="store_true")
            group.add_argument("--store-false", action="store_false")
            group.add_argument("--store-zero", default=0)
            group.add_argument("--store-const", action="store_const", const=True)

    monkeypatch.setitem(PARSER_EXTENSIONS, "table", ExamplePluginWithStoreTrue)
    file_path = tmp_path / "test_markdown.md"
    file_path.touch()

    with patch.object(MDRenderer, "render", return_value=""):
        with pytest.warns(DeprecationWarning) as warnings:
            assert run([str(file_path)]) == 0

    assert "--store-true" in str(warnings.pop().message)
    assert "--store-false" in str(warnings.pop().message)
    assert "--store-zero" in str(warnings.pop().message)
    assert len(warnings) == 0


def test_cli_options_group__no_toml(monkeypatch, tmp_path):
    """Test add_cli_argument_group plugin API with configuration only from
    CLI."""
    monkeypatch.setitem(PARSER_EXTENSIONS, "table", ExamplePluginWithGroupedCli)
    file_path = tmp_path / "test_markdown.md"
    file_path.touch()

    with patch.object(MDRenderer, "render", return_value="") as mock_render:
        assert run((str(file_path), "--o1", "other")) == 0

    (call_,) = mock_render.call_args_list
    posargs = call_[0]
    # Options is the second positional arg of MDRender.render
    opts = posargs[1]
    assert opts["mdformat"]["plugin"]["table"]["o1"] == "other"


def test_ast_changing_plugin(monkeypatch, tmp_path):
    plugin = ASTChangingPlugin()
    monkeypatch.setitem(PARSER_EXTENSIONS, "ast_changer", plugin)
    file_path = tmp_path / "test_markdown.md"

    # Test that the AST changing formatting is applied successfully
    # under normal operation.
    file_path.write_text("Some markdown here\n")
    assert run((str(file_path),)) == 0
    assert file_path.read_text() == plugin.TEXT_REPLACEMENT + "\n"

    # Set the plugin's `CHANGES_AST` flag to False and test that the
    # equality check triggers, notices the AST breaking changes and a
    # non-zero error code is returned.
    plugin.CHANGES_AST = False
    file_path.write_text("Some markdown here\n")
    assert run((str(file_path),)) == 1
    assert file_path.read_text() == "Some markdown here\n"


def test_code_format_warnings__cli(monkeypatch, tmp_path, capsys):
    monkeypatch.setitem(CODEFORMATTERS, "json", JSONFormatterPlugin.format_json)
    file_path = tmp_path / "test_markdown.md"
    file_path.write_text("```json\nthis is invalid json\n```\n")
    assert run([str(file_path)]) == 0
    captured = capsys.readouterr()
    assert (
        captured.err
        == f"Warning: Failed formatting content of a json code block (line 1 before formatting). Filename: {file_path}\n"  # noqa: E501
    )


def test_code_format_warnings__api(monkeypatch, caplog):
    monkeypatch.setitem(CODEFORMATTERS, "json", JSONFormatterPlugin.format_json)
    assert (
        mdformat.text("```json\nthis is invalid json\n```\n", codeformatters=("json",))
        == "```json\nthis is invalid json\n```\n"
    )
    assert (
        caplog.messages[0]
        == "Failed formatting content of a json code block (line 1 before formatting)"
    )


def test_plugin_conflict(monkeypatch, tmp_path, capsys):
    """Test a warning when plugins try to render same syntax."""
    plugin_name_1 = "plug1"
    plugin_name_2 = "plug2"
    monkeypatch.setitem(PARSER_EXTENSIONS, plugin_name_1, TextEditorPlugin)
    monkeypatch.setitem(PARSER_EXTENSIONS, plugin_name_2, ASTChangingPlugin)

    file_path = tmp_path / "test_markdown.md"
    file_path.write_text("some markdown here")
    assert run([str(file_path)]) == 0
    captured = capsys.readouterr()
    assert (
        captured.err
        == 'Warning: Plugin conflict. More than one plugin defined a renderer for "text" syntax.\n'  # noqa: E501
    )


def test_plugin_versions_in_cli_help(monkeypatch, capsys):
    monkeypatch.setitem(
        _PARSER_EXTENSION_DISTS, "table-dist", ("v3.2.1", ["table-ext"])
    )
    with pytest.raises(SystemExit) as exc_info:
        run(["--help"])
    assert exc_info.value.code == 0
    captured = capsys.readouterr()
    assert "installed extensions:" in captured.out
    assert "table-dist: table-ext" in captured.out


def test_postprocess_plugins(monkeypatch):
    """Test that postprocessors work collaboratively."""
    suffix_plugin_name = "suffixer"
    prefix_plugin_name = "prefixer"
    monkeypatch.setitem(PARSER_EXTENSIONS, suffix_plugin_name, SuffixPostprocessPlugin)
    monkeypatch.setitem(PARSER_EXTENSIONS, prefix_plugin_name, PrefixPostprocessPlugin)
    text = mdformat.text(
        dedent(
            """\
            # Example Heading.

            Example paragraph.
            """
        ),
        extensions=[suffix_plugin_name, prefix_plugin_name],
    )
    assert text == dedent(
        """\
        # Prefixed!Example Heading.Suffixed!

        Prefixed!Example paragraph.Suffixed!
        """
    )


def test_load_entrypoints(tmp_path, monkeypatch):
    """Test the function that loads plugins to constants."""
    # Create a minimal .dist-info to create EntryPoints out of
    dist_info_path = tmp_path / "mdformat_gfm-0.3.6.dist-info"
    dist_info_path.mkdir()
    entry_points_path = dist_info_path / "entry_points.txt"
    metadata_path = dist_info_path / "METADATA"
    # The modules here will get loaded so use ones we know will always exist
    # (even though they aren't actual extensions).
    entry_points_path.write_text(
        """\
[mdformat.parser_extension]
ext1=mdformat.plugins
ext2=mdformat.plugins
"""
    )
    metadata_path.write_text(
        """\
Metadata-Version: 2.1
Name: mdformat-gfm
Version: 0.3.6
"""
    )
    distro = importlib.metadata.PathDistribution(dist_info_path)
    entrypoints = distro.entry_points

    loaded_eps, dist_infos = _load_entrypoints(entrypoints)
    assert loaded_eps == {"ext1": mdformat.plugins, "ext2": mdformat.plugins}
    assert dist_infos == {"mdformat-gfm": ("0.3.6", ["ext1", "ext2"])}


def test_no_codeformatters__toml(tmp_path, monkeypatch):
    monkeypatch.setitem(CODEFORMATTERS, "json", JSONFormatterPlugin.format_json)
    unformatted = """\
```json
{"a": "b"}
```
"""
    formatted = """\
```json
{
  "a": "b"
}
```
"""
    file1_path = tmp_path / "file1.md"

    # Without TOML
    file1_path.write_text(unformatted)
    assert run((str(tmp_path),)) == 0
    assert file1_path.read_text() == formatted

    # With TOML
    file1_path.write_text(unformatted)
    config_path = tmp_path / ".mdformat.toml"
    config_path.write_text("codeformatters = []")
    assert run((str(tmp_path),), cache_toml=False) == 0
    assert file1_path.read_text() == unformatted


def test_no_extensions__toml(tmp_path, monkeypatch):
    plugin = ASTChangingPlugin()
    monkeypatch.setitem(PARSER_EXTENSIONS, "ast_changer", plugin)
    unformatted = "text\n"
    formatted = plugin.TEXT_REPLACEMENT + "\n"
    file1_path = tmp_path / "file1.md"

    # Without TOML
    file1_path.write_text(unformatted)
    assert run((str(tmp_path),)) == 0
    assert file1_path.read_text() == formatted

    # With TOML
    file1_path.write_text(unformatted)
    config_path = tmp_path / ".mdformat.toml"
    config_path.write_text("extensions = []")
    assert run((str(tmp_path),), cache_toml=False) == 0
    assert file1_path.read_text() == unformatted
