"""Tests for Sphinx extensions."""

from collections.abc import Callable
from importlib.metadata import version
from pathlib import Path
from textwrap import dedent

import pytest
from docutils import core, nodes
from docutils.parsers.rst import directives
from sphinx.errors import SphinxError
from sphinx.testing.util import SphinxTestApp

import sphinx_substitution_extensions


def test_setup(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Test that the setup function returns the expected metadata."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()

    app = make_app(
        srcdir=source_directory,
    )
    setup_result = sphinx_substitution_extensions.setup(app=app)
    pkg_version = version(distribution_name="sphinx-substitution-extensions")
    assert setup_result == {
        "parallel_read_safe": True,
        "version": pkg_version,
    }


def test_no_substitution_code_block(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """The ``code-block`` directive does not replace placeholders."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. code-block:: shell

           $ PRE-|a|-POST
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        freshenv=True,
    )

    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()

    assert content_html == expected_content_html


def test_substitution_code_block(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``code-block`` directive replaces the placeholders defined in
    ``conf.py`` as specified.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. code-block:: shell
           :substitutions:

           $ PRE-|a|-POST
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        .. code-block:: shell

            $ PRE-example_substitution-POST
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_code_block_case_preserving(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``code-block`` directive respects the original case of
    replacements.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    source_file_content = dedent(
        text="""\
        .. |aBcD_eFgH| replace:: example_substitution

        .. code-block:: shell
           :substitutions:

           $ PRE-|aBcD_eFgH|-POST
        """,
    )
    source_file.write_text(data=source_file_content)

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        .. code-block:: shell

            $ PRE-example_substitution-POST
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitutions_enabled(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    When ``substitutions_default_enabled`` is set to True in conf.py,
    code
    blocks should apply substitutions by default without needing the
    ``:substitutions:`` flag.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. code-block:: shell

           $ PRE-|a|-POST
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": ["sphinx_substitution_extensions"],
            "substitutions_default_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        .. code-block:: shell

            $ PRE-example_substitution-POST
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitutions_disabled_with_flag(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    When ``substitutions_default_enabled`` is True but a code block has
    the
    ``:nosubstitutions:`` flag, substitutions should not be applied.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. code-block:: shell
           :nosubstitutions:

           $ PRE-|a|-POST
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": ["sphinx_substitution_extensions"],
            "substitutions_default_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        .. code-block:: shell

            $ PRE-|a|-POST
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        freshenv=True,
    )

    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()

    assert content_html == expected_content_html


def test_substitution_inline(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``substitution-code`` role replaces the placeholders defined in
    ``conf.py`` as specified.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        Example :substitution-code:`PRE-|a|-POST`
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        Example :code:`PRE-example_substitution-POST`
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_inline_case_preserving(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``substitution-code`` role respects the original case of
    replacements.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    source_file_content = dedent(
        text="""\
        .. |aBcD_eFgH| replace:: example_substitution

        Example :substitution-code:`PRE-|aBcD_eFgH|-POST`
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        Example :code:`PRE-example_substitution-POST`
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_download(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``substitution-download`` role replaces the placeholders defined
    in
    ``conf.py`` as specified in both the download text and the download
    target.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    # Importantly we have a non-space whitespace character in the target name.
    downloadable_file = (
        source_directory / "tgt_pre-example_substitution-tgt_post .py"
    )
    downloadable_file.write_text(data="Sample")
    source_file_content = dedent(
        # Importantly we have a substitution in the download text and the
        # target.
        text="""\
    .. |a| replace:: example_substitution

    :substitution-download:`txt_pre-|a|-txt_post <tgt_pre-|a|-tgt_post\t.py>`
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        :download:`txt_pre-example_substitution-txt_post <tgt_pre-example_substitution-tgt_post\t.py>`
        """,  # noqa: E501
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_no_substitution_hyperlink_target(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Leave placeholders in hyperlink targets unchanged by default."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    source_file.write_text(
        data=dedent(
            text="""\
            .. |ver| replace:: 0.8.5

            Download the tarball_

            .. _tarball: https://example.com/releases/v|ver|.tar.gz
            """,
        ),
    )
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_hyperlink_target(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Replace placeholders in external hyperlink targets."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    source_file.write_text(
        data=dedent(
            text="""\
            Download the tarball_

            .. _tarball: https://example.com/releases/v|ver|.tar.gz

            See the `internal section`_.

            Internal section
            ----------------
            """,
        ),
    )
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": ["sphinx_substitution_extensions"],
            "rst_prolog": ".. |ver| replace:: 0.8.5",
            "substitutions_hyperlink_targets_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        Download the tarball_

        .. _tarball: https://example.com/releases/v0.8.5.tar.gz

        See the `internal section`_.

        Internal section
        ----------------
        """,
    )
    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_myst_substitution_hyperlink_target(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Replace global MyST substitutions in external hyperlink targets."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.md"
    (source_directory / "conf.py").touch()
    source_file.write_text(
        data=dedent(
            text="""\
            ```{eval-rst}
            Download the tarball_

            .. _tarball: https://example.com/releases/v|ver|.tar.gz
            ```
            """,
        ),
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": [
                "myst_parser",
                "sphinx_substitution_extensions",
            ],
            "myst_enable_extensions": ["substitution"],
            "myst_substitutions": {"ver": "0.8.5"},
            "substitutions_hyperlink_targets_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    source_file.write_text(
        data=dedent(
            text="""\
            ```{eval-rst}
            Download the tarball_

            .. _tarball: https://example.com/releases/v0.8.5.tar.gz
            ```
            """,
        ),
    )
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        freshenv=True,
        confoverrides={"extensions": ["myst_parser"]},
    )
    app_expected.build()
    assert app_expected.statuscode == 0
    expected_content_html = (app_expected.outdir / "index.html").read_text()

    assert content_html == expected_content_html


def test_no_substitution_literal_include(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """The ``literalinclude`` directive does not replace placeholders."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    include_file = source_directory / "example.txt"
    include_file.write_text(data="Content with |a| placeholder")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. literalinclude:: example.txt
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        freshenv=True,
    )

    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()

    assert content_html == expected_content_html


def test_substitution_literal_include(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``literalinclude`` directive replaces the placeholders defined
    in
    ``conf.py`` as specified when the `:content-substitutions:` flag is
    set.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    include_file = source_directory / "example.txt"
    include_file.write_text(data="Content with |a| placeholder")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. literalinclude:: example.txt
           :content-substitutions:
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    include_file.write_text(
        data="Content with example_substitution placeholder"
    )

    equivalent_source = dedent(
        text="""\
        .. literalinclude:: example.txt
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_literal_include_empty_file(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``literalinclude`` directive handles empty files without
    crashing.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    # Create an empty file
    include_file = source_directory / "empty.txt"
    include_file.write_text(data="")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. literalinclude:: empty.txt
           :content-substitutions:
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        .. literalinclude:: empty.txt
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_literal_include_multiple(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """The ``literalinclude`` directive replaces multiple placeholders."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    include_file = source_directory / "example.txt"
    include_file.write_text(data="PRE-|a|-MID-|b|-POST")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: first_substitution
        .. |b| replace:: second_substitution

        .. literalinclude:: example.txt
           :content-substitutions:
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    include_file.write_text(
        data="PRE-first_substitution-MID-second_substitution-POST",
    )

    equivalent_source = dedent(
        text="""\
        .. literalinclude:: example.txt
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_literal_include_with_caption(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """The ``literalinclude`` directive works with captions."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    include_file = source_directory / "example.txt"
    include_file.write_text(data="Content with |a| placeholder")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. literalinclude:: example.txt
           :caption: Example caption
           :content-substitutions:
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    include_file.write_text(
        data="Content with example_substitution placeholder"
    )

    equivalent_source = dedent(
        text="""\
        .. literalinclude:: example.txt
           :caption: Example caption
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_literal_include_in_rest_example(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """The ``literalinclude`` directive works inside rest-example."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    include_file = source_directory / "example.txt"
    include_file.write_text(data="Content with |a| placeholder")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. rest-example::

           .. literalinclude:: example.txt
              :content-substitutions:
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        warningiserror=True,
        confoverrides={
            "extensions": [
                "sphinx_substitution_extensions",
                "sphinx_toolbox.rest_example",
            ],
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    assert "example_substitution" in content_html


def test_substitution_literal_include_path(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``literalinclude`` directive replaces placeholders in the file
    path
    when the `:path-substitutions:` flag is set.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    # Create a file with substitution in the name
    include_file = source_directory / "example_substitution.txt"
    include_file.write_text(data="File content")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. literalinclude:: |a|.txt
           :path-substitutions:
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    # Compare with directly using the filename
    equivalent_source = dedent(
        text="""\
        .. literalinclude:: example_substitution.txt
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_literal_include_both_path_and_content(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``literalinclude`` directive can use both path and content
    substitutions at the same time.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    # Create a file with substitution in the name and content
    include_file = source_directory / "example_substitution.txt"
    include_file.write_text(data="Content with |b| placeholder")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution
        .. |b| replace:: test_value

        .. literalinclude:: |a|.txt
           :path-substitutions:
           :content-substitutions:
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    # Create equivalent file with substituted content
    include_file.write_text(data="Content with test_value placeholder")

    equivalent_source = dedent(
        text="""\
        .. literalinclude:: example_substitution.txt
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html

    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitutions_literal_include_content(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    When ``substitutions_default_enabled`` is True, ``literalinclude``
    should
    apply content substitutions by default without requiring the
    ``:content-
    substitutions:`` flag.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    include_file = source_directory / "example.txt"
    include_file.write_text(data="Content with |a| placeholder")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. literalinclude:: example.txt
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": ["sphinx_substitution_extensions"],
            "substitutions_default_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    include_file.write_text(
        data="Content with example_substitution placeholder"
    )

    equivalent_source = dedent(
        text="""\
        .. literalinclude:: example.txt
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitutions_literal_include_path(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    When ``substitutions_default_enabled`` is True, ``literalinclude``
    should
    apply path substitutions by default without requiring the ``:path-
    substitutions:`` flag.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    include_file = source_directory / "example_substitution.txt"
    include_file.write_text(data="File content")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. literalinclude:: |a|.txt
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": ["sphinx_substitution_extensions"],
            "substitutions_default_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        .. literalinclude:: example_substitution.txt
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitutions_literal_include_disabled_content(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    When ``substitutions_default_enabled`` is True but
    ``literalinclude`` has
    the ``:nocontent-substitutions:`` flag, content substitutions should
    not be
    applied.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    include_file = source_directory / "example.txt"
    include_file.write_text(data="Content with |a| placeholder")

    source_file_content = dedent(
        text="""\
        .. |a| replace:: example_substitution

        .. literalinclude:: example.txt
           :nocontent-substitutions:
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": ["sphinx_substitution_extensions"],
            "substitutions_default_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        .. literalinclude:: example.txt
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        freshenv=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitutions_literal_include_disabled_path(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """When ``substitutions_default_enabled`` is True but
    ``literalinclude``
    has the ``:nopath-substitutions:`` flag, path substitutions should not
    be
    applied.

    Note: This test uses MyST format with custom delimiters because the `|`
    character cannot be used in Windows file paths.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    index_source_file = source_directory / "index.rst"
    markdown_source_file = source_directory / "markdown_document.md"
    (source_directory / "conf.py").touch()

    # Use custom delimiters [[a]] instead of |a| because | is not allowed
    # in Windows file paths
    include_file = source_directory / "[[a]].txt"
    include_file.write_text(data="File content")

    index_source_file_content = dedent(
        text="""\
        .. toctree::

           markdown_document
        """,
    )
    markdown_source_file_content = dedent(
        text="""\
        # Title

        ```{literalinclude} [[a]].txt
        :nopath-substitutions:
        ```
        """,
    )
    index_source_file.write_text(data=index_source_file_content)
    markdown_source_file.write_text(data=markdown_source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": [
                "myst_parser",
                "sphinx_substitution_extensions",
            ],
            "myst_enable_extensions": ["substitution"],
            "myst_substitutions": {
                "a": "example_substitution",
            },
            "myst_sub_delimiters": ("[", "]"),
            "substitutions_default_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "markdown_document.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        # Title

        ```{literalinclude} [[a]].txt
        ```
        """,
    )

    markdown_source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["myst_parser"]},
        freshenv=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (
        app_expected.outdir / "markdown_document.html"
    ).read_text()
    assert content_html == expected_content_html


class TestMyst:
    """Tests for MyST documents."""

    @staticmethod
    def test_myst_substitutions_ignored_given_rst_definition(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """
        MyST substitutions are ignored in rST documents with a rST
        substitution
        definition.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        source_file = source_directory / "index.rst"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. |a| replace:: rst_prolog_substitution

            .. code-block:: shell
               :substitutions:

               $ PRE-|a|-POST
            """,
        )
        source_file.write_text(data=index_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "myst_substitution",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "index.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            .. code-block:: shell

               $ PRE-rst_prolog_substitution-POST
            """,
        )

        source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "index.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_substitutions_ignored_without_rst_definition(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """
        MyST substitutions are ignored in rST documents without a rST
        substitution definition.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        source_file = source_directory / "index.rst"
        (source_directory / "conf.py").touch()
        source_file_content = dedent(
            text="""\
            .. code-block:: shell
               :substitutions:

               $ PRE-|a|-POST
            """,
        )
        source_file.write_text(data=source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "myst_substitution",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "index.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            .. code-block:: shell

               $ PRE-|a|-POST
            """,
        )

        source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "index.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_substitutions(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST substitutions are respected in MyST documents."""
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-|a|-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "example_substitution",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-example_substitution-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_frontmatter_substitutions(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST front matter substitutions are respected in code
        blocks.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file.write_text(
            data=dedent(
                text="""\
                .. toctree::

                   markdown_document
                """,
            ),
        )
        markdown_source_file.write_text(
            data=dedent(
                text="""\
                ---
                myst:
                  substitutions:
                    local:
                      nested: local_value
                ---

                # Title

                ```{code-block} shell
                :substitutions:

                $ PRE-|global|-|local.nested|-POST
                ```
                """,
            ),
        )

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "global": "global_value",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        markdown_source_file.write_text(
            data=dedent(
                text="""\
                # Title

                ```{code-block} shell

                $ PRE-global_value-local_value-POST
                ```
                """,
            ),
        )
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            freshenv=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0
        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()

        assert content_html == expected_content_html

    @staticmethod
    def test_myst_nested_substitutions(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST nested substitutions are flattened and applied
        correctly.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-|a|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|b.c|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|b.d.e|-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "value_a",
                    "b": {
                        "c": "value_b_c",
                        "d": {
                            "e": "value_b_d_e",
                        },
                    },
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-value_a-POST
            ```

            ```{code-block}

            $ PRE-value_b_c-POST
            ```

            ```{code-block}

            $ PRE-value_b_d_e-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_nested_substitutions_with_lists(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST nested substitutions with lists are flattened and
        applied correctly.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-|items.0|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|items.1|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|nested.0.name|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|nested.1.value|-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "items": ["first", "second"],
                    "nested": [
                        {"name": "a", "value": "1"},
                        {"name": "b", "value": "2"},
                    ],
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-first-POST
            ```

            ```{code-block}

            $ PRE-second-POST
            ```

            ```{code-block}

            $ PRE-a-POST
            ```

            ```{code-block}

            $ PRE-2-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_deep_nesting(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST deeply nested substitutions (5 levels) are flattened
        correctly.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-|level1.level2.level3.level4.level5|-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "level1": {
                        "level2": {
                            "level3": {
                                "level4": {
                                    "level5": "deep_value",
                                },
                            },
                        },
                    },
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-deep_value-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_empty_containers(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST empty containers (lists and dictionaries) do not create
        keys
        and do not break the build.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-|normal|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|mixed.normal|-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "empty_list": [],
                    "empty_dict": {},
                    "mixed": {
                        "empty_nested_list": [],
                        "empty_nested_dict": {},
                        "normal": "value",
                    },
                    "normal": "normal_value",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-normal_value-POST
            ```

            ```{code-block}

            $ PRE-value-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_mixed_types_in_list(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST mixed types in lists are converted to strings
        correctly.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-|mixed.0|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|mixed.1|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|mixed.2|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|mixed.3|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|mixed.4|-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "mixed": [1, "text", 3.14, -42, 0],
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-1-POST
            ```

            ```{code-block}

            $ PRE-text-POST
            ```

            ```{code-block}

            $ PRE-3.14-POST
            ```

            ```{code-block}

            $ PRE--42-POST
            ```

            ```{code-block}

            $ PRE-0-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_nested_lists(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST nested lists are flattened with multi-index notation."""
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-|matrix.0.0|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|matrix.0.1|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|matrix.1.0|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|matrix.1.1|-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "matrix": [
                        [1, 2],
                        [3, 4],
                    ],
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-1-POST
            ```

            ```{code-block}

            $ PRE-2-POST
            ```

            ```{code-block}

            $ PRE-3-POST
            ```

            ```{code-block}

            $ PRE-4-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_invalid_substitution_access(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST invalid substitution access does not break the build."""
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-|items.99|-POST
            ```

            ```{code-block}
            :substitutions:

            $ PRE-|nonexistent.key|-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=False,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "items": ["a", "b"],
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        expected_text_in_html = [
            "$ PRE-|items.99|-POST",
            "$ PRE-|nonexistent.key|-POST",
        ]
        for text in expected_text_in_html:
            assert text in content_html

    @staticmethod
    def test_myst_substitution_key_with_dot_raises_error(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST substitution keys containing dots raise SphinxError.

        Dots are reserved for nested access notation.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            |key.with.dots|
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "key.with.dots": "value",
                },
            },
        )

        with pytest.raises(
            expected_exception=SphinxError,
            match=r"Substitution key 'key\.with\.dots' contains a dot",
        ):
            app.build()

    @staticmethod
    def test_myst_nested_substitution_key_with_dot_raises_error(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """MyST nested substitution keys containing dots raise SphinxError.

        Dots are reserved for nested access notation.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            |parent.key.with.dots|
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "parent": {
                        "key.with.dots": "value",
                    },
                },
            },
        )

        with pytest.raises(
            expected_exception=SphinxError,
            match=r"Substitution key 'key\.with\.dots' contains a dot",
        ):
            app.build()

    @staticmethod
    def test_rst_substitution_key_with_dot_does_not_raise_error(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """Substitution names with dots do not raise SphinxError in reST.

        The dot validation applies only to flattened MyST substitutions, so
        a refactoring that leaked it into the reST path would be caught here.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        source_file = source_directory / "index.rst"
        (source_directory / "conf.py").touch()

        source_file_content = dedent(
            text="""\
            .. |key.with.dots| replace:: example_substitution

            .. code-block:: shell
               :substitutions:

               $ PRE-|key.with.dots|-POST
            """,
        )
        source_file.write_text(data=source_file_content)
        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": ["sphinx_substitution_extensions"],
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "index.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            .. code-block:: shell

                $ PRE-example_substitution-POST
            """,
        )

        source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            freshenv=True,
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "index.html"
        ).read_text()
        assert content_html == expected_content_html


def test_xref_role_class_prefix_removal(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``SubstitutionXRefRole`` should only remove the "substitution-"
    prefix
    from CSS classes, not all occurrences of "substitution-" in the class
    name.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    conf_file = source_directory / "conf.py"

    downloadable_file = source_directory / "example.py"
    downloadable_file.write_text(data="Sample")

    conf_file.write_text(
        data=dedent(
            text="""\
            from sphinx import addnodes
            from sphinx_substitution_extensions import SubstitutionXRefRole

            def setup(app):
                role = SubstitutionXRefRole(
                    nodeclass=addnodes.download_reference,
                )
                app.add_role("substitution-my-substitution-download", role)
            """,
        ),
    )

    source_file_content = dedent(
        text="""\
        :substitution-my-substitution-download:`Download <example.py>`
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    conf_file.write_text(
        data=dedent(
            text="""\
            from sphinx import addnodes
            from sphinx.roles import XRefRole

            def setup(app):
                role = XRefRole(nodeclass=addnodes.download_reference)
                app.add_role("my-substitution-download", role)
            """,
        ),
    )

    equivalent_source = dedent(
        text="""\
        :my-substitution-download:`Download <example.py>`
        """,
    )
    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_no_substitution_include(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Leave an ``include`` path unchanged by default."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()
    (source_directory / "example.txt").write_text(data="Included content")
    (source_directory / "index.rst").write_text(
        data=".. include:: example.txt\n",
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()

    assert app.statuscode == 0
    assert "Included content" in (app.outdir / "index.html").read_text()


def test_include_without_sphinx_environment(tmp_path: Path) -> None:
    """Support documents which have no Sphinx environment."""
    source_file = tmp_path / "index.rst"
    source_file.write_text(data=".. include:: included.rst\n")
    (tmp_path / "included.rst").write_text(data="Included content")
    directives.register_directive(
        name="include",
        directive=sphinx_substitution_extensions.SubstitutionInclude,
    )

    publish_doctree: Callable[..., nodes.document] = core.publish_doctree  # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
    document = publish_doctree(
        source=source_file.read_text(),
        source_path=source_file.as_posix(),
        settings_overrides={"env": None},
    )

    assert "Included content" in document.astext()


def test_substitution_include_path(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Replace placeholders in an ``include`` path when requested."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()
    (source_directory / "example.txt").write_text(data="Included content")
    (source_directory / "index.rst").write_text(
        data=dedent(
            text="""\
            .. |name| replace:: example

            .. include:: |name|.txt
               :path-substitutions:
            """,
        ),
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()

    assert app.statuscode == 0
    assert "Included content" in (app.outdir / "index.html").read_text()


def test_substitution_include_content(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Replace placeholders in included source content when requested."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()
    include_file = source_directory / "upgrade-guide.txt"
    include_file.write_text(
        data=dedent(
            text="""\
            .. code-block:: shell

               upgrade --from |SRC_VERSION| --to |NEW_VERSION|
            """,
        ),
    )
    source_file = source_directory / "index.rst"
    source_file.write_text(
        data=dedent(
            text="""\
            .. |SRC_VERSION| replace:: 4.0
            .. |NEW_VERSION| replace:: 4.1

            .. include:: upgrade-guide.txt
               :content-substitutions:
            """,
        ),
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    include_file.write_text(
        data=dedent(
            text="""\
            .. code-block:: shell

               upgrade --from 4.0 --to 4.1
            """,
        ),
    )
    source_file.write_text(data=".. include:: upgrade-guide.txt\n")

    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_include_content_does_not_leak_to_nested_includes(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Apply content substitutions only to the include that requests
    them.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()
    (source_directory / "nested-default.txt").write_text(
        data=dedent(
            text="""\
            .. code-block:: text

               Default nested content with |name| placeholder
            """,
        ),
    )
    (source_directory / "nested-disabled.txt").write_text(
        data=dedent(
            text="""\
            .. code-block:: text

               Disabled nested content with |name| placeholder
            """,
        ),
    )
    outer_file = source_directory / "outer.txt"
    outer_file.write_text(
        data=dedent(
            text="""\
            .. include:: nested-default.txt

            .. include:: nested-disabled.txt
               :nocontent-substitutions:
            """,
        ),
    )
    source_file = source_directory / "index.rst"
    source_file.write_text(
        data=dedent(
            text="""\
            .. |name| replace:: example

            .. include:: outer.txt
               :content-substitutions:
            """,
        ),
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()

    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    outer_file.write_text(
        data=dedent(
            text="""\
            .. include:: nested-default.txt

            .. include:: nested-disabled.txt
            """,
        ),
    )
    source_file.write_text(data=".. include:: outer.txt\n")

    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_include_path_and_content(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Replace placeholders in an include path and its source content."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()
    include_file = source_directory / "upgrade-guide.txt"
    include_file.write_text(
        data=dedent(
            text="""\
            .. code-block:: text

               Upgrade to |version|.
            """,
        ),
    )
    source_file = source_directory / "index.rst"
    source_file.write_text(
        data=dedent(
            text="""\
            .. |document| replace:: upgrade-guide
            .. |version| replace:: 4.1

            .. include:: |document|.txt
               :path-substitutions:
               :content-substitutions:
            """,
        ),
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    include_file.write_text(
        data=dedent(
            text="""\
            .. code-block:: text

               Upgrade to 4.1.
            """,
        ),
    )
    source_file.write_text(data=".. include:: upgrade-guide.txt\n")

    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_include_literal_content(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Replace placeholders when ``include`` returns a literal node."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()
    include_file = source_directory / "example.txt"
    include_file.write_text(
        data="Content with |name| placeholder",
    )
    source_file = source_directory / "index.rst"
    source_file.write_text(
        data=dedent(
            text="""\
            .. |name| replace:: example

            .. include:: example.txt
               :literal:
               :content-substitutions:
            """,
        ),
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    include_file.write_text(data="Content with example placeholder")
    source_file.write_text(
        data=dedent(
            text="""\
            .. include:: example.txt
               :literal:
            """,
        ),
    )

    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitution_include_path(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Replace ``include`` path placeholders when defaults are enabled."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()
    (source_directory / "example.txt").write_text(data="Included content")
    (source_directory / "index.rst").write_text(
        data=dedent(
            text="""\
            .. |name| replace:: example

            .. include:: |name|.txt
            """,
        ),
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": ["sphinx_substitution_extensions"],
            "substitutions_default_enabled": True,
        },
    )
    app.build()

    assert app.statuscode == 0
    assert "Included content" in (app.outdir / "index.html").read_text()


def test_default_substitution_include_content(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Replace include content when substitutions are enabled by
    default.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()
    include_file = source_directory / "example.txt"
    include_file.write_text(
        data=dedent(
            text="""\
            Included literal content::

               Content with |name| placeholder
            """,
        ),
    )
    source_file = source_directory / "index.rst"
    source_file.write_text(
        data=dedent(
            text="""\
            .. |name| replace:: example

            .. include:: example.txt
            """,
        ),
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": ["sphinx_substitution_extensions"],
            "substitutions_default_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    include_file.write_text(
        data=dedent(
            text="""\
            Included literal content::

               Content with example placeholder
            """,
        ),
    )
    source_file.write_text(data=".. include:: example.txt\n")

    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitution_include_disabled_content(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Respect ``:nocontent-substitutions:`` when defaults are enabled."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()
    include_file = source_directory / "example.txt"
    include_file.write_text(
        data=dedent(
            text="""\
            Included literal content::

               Content with |name| placeholder
            """,
        ),
    )
    source_file = source_directory / "index.rst"
    source_file.write_text(
        data=dedent(
            text="""\
            .. |name| replace:: example

            .. include:: example.txt
               :nocontent-substitutions:
            """,
        ),
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": ["sphinx_substitution_extensions"],
            "substitutions_default_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    source_file.write_text(data=".. include:: example.txt\n")

    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitution_include_disabled(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """Respect ``:nopath-substitutions:`` when defaults are enabled."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    (source_directory / "conf.py").touch()
    (source_directory / "[[name]].txt").write_text(data="Included content")
    (source_directory / "index.rst").write_text(
        data=dedent(
            text="""\
            .. toctree::

               document
            """,
        ),
    )
    (source_directory / "document.md").write_text(
        data=dedent(
            text="""\
            # Document

            ```{include} [[name]].txt
            :nopath-substitutions:
            ```
            """,
        ),
    )

    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": [
                "myst_parser",
                "sphinx_substitution_extensions",
            ],
            "myst_enable_extensions": ["substitution"],
            "myst_sub_delimiters": ("[", "]"),
            "myst_substitutions": {"name": "example"},
            "substitutions_default_enabled": True,
        },
    )
    app.build()

    assert app.statuscode == 0
    assert "Included content" in (app.outdir / "document.html").read_text()


def test_no_substitution_image(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """The ``image`` directive does not replace custom placeholders by
    default.

    Note: reST by default processes |substitutions| in image paths, but
    our extension adds the ability to use custom delimiters like {{var}}.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    image_file = source_directory / "test_image.png"
    png_data = (
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
        b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx"
        b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
    )
    image_file.write_bytes(data=png_data)

    source_file_content = dedent(
        text="""\
        .. |a| replace:: test_image

        .. image:: test_image.png
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        freshenv=True,
    )

    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()

    # The behavior should be the same with or without our extension
    # when not using :path-substitutions:
    assert content_html == expected_content_html


def test_substitution_image_path(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``image`` directive replaces placeholders in the file path when
    the
    ``:path-substitutions:`` flag is set.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    # Create a simple image file with substitution in the name
    image_file = source_directory / "test_image.png"
    png_data = (
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
        b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx"
        b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
    )
    image_file.write_bytes(data=png_data)

    source_file_content = dedent(
        text="""\
        .. |a| replace:: test_image

        .. image:: |a|.png
           :path-substitutions:
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    # Compare with directly using the filename
    equivalent_source = dedent(
        text="""\
        .. image:: test_image.png
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_image_path_multiple(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    The ``image`` directive replaces multiple placeholders in the file
    path.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    # Create an image file with multiple substitutions in the name
    image_file = source_directory / "pre_test_mid_image_post.png"
    png_data = (
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
        b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx"
        b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
    )
    image_file.write_bytes(data=png_data)

    source_file_content = dedent(
        text="""\
        .. |a| replace:: test
        .. |b| replace:: image

        .. image:: pre_|a|_mid_|b|_post.png
           :path-substitutions:
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    # Compare with directly using the filename
    equivalent_source = dedent(
        text="""\
        .. image:: pre_test_mid_image_post.png
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_substitution_image_with_options(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """The ``image`` directive works with standard image options."""
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    image_file = source_directory / "test_image.png"
    png_data = (
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
        b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx"
        b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
    )
    image_file.write_bytes(data=png_data)

    source_file_content = dedent(
        text="""\
        .. |a| replace:: test_image

        .. image:: |a|.png
           :path-substitutions:
           :alt: Test image alt text
           :width: 100px
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["sphinx_substitution_extensions"]},
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        .. image:: test_image.png
           :alt: Test image alt text
           :width: 100px
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitutions_image_path(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """
    When ``substitutions_default_enabled`` is True, ``image`` should
    apply path
    substitutions by default without requiring the ``:path-substitutions:``
    flag.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    source_file = source_directory / "index.rst"
    (source_directory / "conf.py").touch()

    image_file = source_directory / "test_image.png"
    png_data = (
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
        b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx"
        b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
    )
    image_file.write_bytes(data=png_data)

    source_file_content = dedent(
        text="""\
        .. |a| replace:: test_image

        .. image:: |a|.png
        """,
    )
    source_file.write_text(data=source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": ["sphinx_substitution_extensions"],
            "substitutions_default_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "index.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        .. image:: test_image.png
        """,
    )

    source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (app_expected.outdir / "index.html").read_text()
    assert content_html == expected_content_html


def test_default_substitutions_image_disabled_path(
    *,
    tmp_path: Path,
    make_app: Callable[..., SphinxTestApp],
) -> None:
    """When ``substitutions_default_enabled`` is True but ``image`` has the
    ``:nopath-substitutions:`` flag, path substitutions should not be
    applied.

    Note: This test uses MyST format with custom delimiters because the `|`
    character cannot be used in Windows file paths.
    """
    source_directory = tmp_path / "source"
    source_directory.mkdir()
    index_source_file = source_directory / "index.rst"
    markdown_source_file = source_directory / "markdown_document.md"
    (source_directory / "conf.py").touch()

    # Create an image file with the literal [[a]] in the filename
    image_file = source_directory / "[[a]].png"
    png_data = (
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
        b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx"
        b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
    )
    image_file.write_bytes(data=png_data)

    index_source_file_content = dedent(
        text="""\
        .. toctree::

           markdown_document
        """,
    )
    markdown_source_file_content = dedent(
        text="""\
        # Title

        ```{image} [[a]].png
        :nopath-substitutions:
        ```
        """,
    )
    index_source_file.write_text(data=index_source_file_content)
    markdown_source_file.write_text(data=markdown_source_file_content)
    app = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={
            "extensions": [
                "myst_parser",
                "sphinx_substitution_extensions",
            ],
            "myst_enable_extensions": ["substitution"],
            "myst_substitutions": {
                "a": "example_substitution",
            },
            "myst_sub_delimiters": ("[", "]"),
            "substitutions_default_enabled": True,
        },
    )
    app.build()
    assert app.statuscode == 0
    content_html = (app.outdir / "markdown_document.html").read_text()
    app.cleanup()

    equivalent_source = dedent(
        text="""\
        # Title

        ```{image} [[a]].png
        ```
        """,
    )

    markdown_source_file.write_text(data=equivalent_source)
    app_expected = make_app(
        srcdir=source_directory,
        exception_on_warning=True,
        confoverrides={"extensions": ["myst_parser"]},
        freshenv=True,
    )
    app_expected.build()
    assert app_expected.statuscode == 0

    expected_content_html = (
        app_expected.outdir / "markdown_document.html"
    ).read_text()
    assert content_html == expected_content_html


class TestImageMyst:
    """Tests for image directive with MyST documents."""

    @staticmethod
    def test_myst_substitutions_image(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """
        MyST substitutions are respected in image paths in MyST
        documents.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()

        # Create an image file
        image_file = source_directory / "test_image.png"
        png_data = (
            b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
            b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx"
            b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
        )
        image_file.write_bytes(data=png_data)

        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{image} |a|.png
            :path-substitutions:
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "test_image",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{image} test_image.png
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_substitutions_image_default_delimiters(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """
        The default MyST substitution delimiters {{}} are respected for
        images.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()

        image_file = source_directory / "test_image.png"
        png_data = (
            b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
            b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx"
            b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
        )
        image_file.write_bytes(data=png_data)

        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{image} {{a}}.png
            :path-substitutions:
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "test_image",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{image} test_image.png
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_substitutions_not_enabled(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """
        MyST substitutions are not respected in MyST documents when
        ``myst_enable_extensions`` does not contain ``substitutions``.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-|a|-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_substitutions": {
                    "a": "example_substitution",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-|a|-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_myst_substitutions_custom_markdown_suffix(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """Custom markdown suffixes are respected in MyST documents."""
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.txt"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

                markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-|a|-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "example_substitution",
                },
                "source_suffix": {
                    ".rst": "restructuredtext",
                    ".txt": "markdown",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-example_substitution-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": ["myst_parser"],
                "source_suffix": {
                    ".rst": "restructuredtext",
                    ".txt": "markdown",
                },
            },
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_default_myst_sub_delimiters_code_block(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """The default MyST substitution delimiters are respected."""
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-{{a}}-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "example_substitution",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-example_substitution-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_custom_myst_sub_delimiters_code_block(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """Custom MyST substitution delimiters are respected."""
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()
        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            ```{code-block}
            :substitutions:

            $ PRE-[[a]]-POST
            ```
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)

        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "example_substitution",
                },
                "myst_sub_delimiters": ("[", "]"),
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            ```{code-block}

            $ PRE-example_substitution-POST
            ```
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_substitution_code_role(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """
        The ``substitution-code`` role replaces the placeholders defined
        in
        ``conf.py`` as specified.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()

        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
            # Title

            Example {substitution-code}`PRE-|a|-POST`
            """,
        )
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)
        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "example_substitution",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            Example {code}`PRE-example_substitution-POST`
            """,
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html

    @staticmethod
    def test_substitution_download(
        *,
        tmp_path: Path,
        make_app: Callable[..., SphinxTestApp],
    ) -> None:
        """
        The ``substitution-download`` role replaces the placeholders
        defined in
        ``conf.py`` as specified.
        """
        source_directory = tmp_path / "source"
        source_directory.mkdir()
        index_source_file = source_directory / "index.rst"
        markdown_source_file = source_directory / "markdown_document.md"
        (source_directory / "conf.py").touch()

        index_source_file_content = dedent(
            text="""\
            .. toctree::

               markdown_document
            """,
        )
        markdown_source_file_content = dedent(
            text="""\
    # Title

    {substitution-download}`txt_pre-|a|-txt_post <tgt_pre-|a|-tgt_post\t.py>`
            """,
        )
        # Importantly we have a non-space whitespace character in the target
        # name.
        downloadable_file = (
            source_directory / "tgt_pre-example_substitution-tgt_post .py"
        )
        downloadable_file.write_text(data="Sample")
        index_source_file.write_text(data=index_source_file_content)
        markdown_source_file.write_text(data=markdown_source_file_content)
        app = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={
                "extensions": [
                    "myst_parser",
                    "sphinx_substitution_extensions",
                ],
                "myst_enable_extensions": ["substitution"],
                "myst_substitutions": {
                    "a": "example_substitution",
                },
            },
        )
        app.build()
        assert app.statuscode == 0
        content_html = (app.outdir / "markdown_document.html").read_text()
        app.cleanup()

        equivalent_source = dedent(
            text="""\
            # Title

            {download}`txt_pre-example_substitution-txt_post <tgt_pre-example_substitution-tgt_post\t.py>`
            """,  # noqa: E501
        )

        markdown_source_file.write_text(data=equivalent_source)
        app_expected = make_app(
            srcdir=source_directory,
            exception_on_warning=True,
            confoverrides={"extensions": ["myst_parser"]},
        )
        app_expected.build()
        assert app_expected.statuscode == 0

        expected_content_html = (
            app_expected.outdir / "markdown_document.html"
        ).read_text()
        assert content_html == expected_content_html
