# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from pathlib import Path

from build_scripts import example_index as example_index_module
from build_scripts import gen_api_md as gen_api_md_module
from build_scripts.gen_api_md import (
    ExampleReference,
    SymbolEntry,
    _build_example_index,
    _build_symbol_index,
    _class_anchor,
    _example_link_path,
    _format_bases,
    _format_reexport_alias,
    _format_reexport_target,
    _function_anchor,
    _method_anchor,
    _process_docstring_text,
    _rewrite_symbol_refs,
    render_class,
    render_function,
    render_module,
)


def test_docs_scripts_share_validate_docs_module() -> None:
    assert example_index_module.validate_docs is gen_api_md_module.validate_docs


def _fake_class(name: str, methods: list[str] | None = None) -> dict:
    return {
        "name": name,
        "kind": "class",
        "methods": [{"name": m, "kind": "function"} for m in (methods or [])],
    }


def _fake_function(name: str) -> dict:
    return {"name": name, "kind": "function"}


def _fake_module(name: str, members: list[dict]) -> dict:
    return {"name": name, "kind": "module", "members": members}


def test_anchor_helpers_produce_unique_labels() -> None:
    assert _class_anchor("pyrit.prompt_target", "PromptTarget") == "api-pyrit_prompt_target-PromptTarget"
    assert _function_anchor("pyrit.common", "validate_log_level") == "api-pyrit_common-validate_log_level"
    assert (
        _method_anchor("pyrit.prompt_target", "PromptTarget", "send_prompt_async")
        == "api-pyrit_prompt_target-PromptTarget-send_prompt_async"
    )


def test_build_symbol_index_registers_classes_functions_and_methods() -> None:
    modules = [
        _fake_module(
            "pyrit.prompt_target",
            [
                _fake_class("PromptTarget", methods=["send_prompt_async", "apply_capabilities"]),
                _fake_function("limit_requests_per_minute"),
            ],
        ),
    ]
    index = _build_symbol_index(modules)

    # Short-name lookup
    assert len(index["PromptTarget"]) == 1
    assert index["PromptTarget"][0].kind == "class"
    assert index["PromptTarget"][0].anchor == "api-pyrit_prompt_target-PromptTarget"

    # Class.method lookup
    assert len(index["PromptTarget.send_prompt_async"]) == 1
    assert index["PromptTarget.send_prompt_async"][0].anchor == "api-pyrit_prompt_target-PromptTarget-send_prompt_async"

    # FQN lookup
    assert index["pyrit.prompt_target.PromptTarget"][0].kind == "class"
    assert index["pyrit.prompt_target.limit_requests_per_minute"][0].kind == "function"


def test_build_symbol_index_skips_private_members() -> None:
    modules = [
        _fake_module(
            "pyrit.example",
            [
                _fake_class("Public", methods=["do_thing", "_internal_helper"]),
                _fake_function("_private_func"),
            ],
        ),
    ]
    index = _build_symbol_index(modules)

    assert "_internal_helper" not in index
    assert "Public._internal_helper" not in index
    assert "_private_func" not in index
    assert "do_thing" in index


def test_build_symbol_index_marks_duplicates_as_ambiguous() -> None:
    modules = [
        _fake_module("pyrit.first", [_fake_class("Scorer")]),
        _fake_module("pyrit.second", [_fake_class("Scorer")]),
    ]
    index = _build_symbol_index(modules)

    assert len(index["Scorer"]) == 2
    # FQN entries stay distinct
    assert len(index["pyrit.first.Scorer"]) == 1
    assert len(index["pyrit.second.Scorer"]) == 1


def test_rewrite_symbol_refs_links_unique_class() -> None:
    index = {
        "SeedPrompt": [
            SymbolEntry(
                module="pyrit.models",
                kind="class",
                name="SeedPrompt",
                qualname="SeedPrompt",
                anchor="api-pyrit_models-SeedPrompt",
            )
        ]
    }
    out = _rewrite_symbol_refs("Returns a ``SeedPrompt`` instance.", index)
    assert out == "Returns a [``SeedPrompt``](#api-pyrit_models-SeedPrompt) instance."


def test_rewrite_symbol_refs_handles_single_backticks() -> None:
    index = {"Foo": [SymbolEntry(module="pyrit.x", kind="class", name="Foo", qualname="Foo", anchor="api-pyrit_x-Foo")]}
    out = _rewrite_symbol_refs("See `Foo` for details.", index)
    assert out == "See [`Foo`](#api-pyrit_x-Foo) for details."


def test_rewrite_symbol_refs_resolves_class_dot_method() -> None:
    index = {
        "PromptTarget.send_prompt_async": [
            SymbolEntry(
                module="pyrit.prompt_target",
                kind="method",
                name="send_prompt_async",
                qualname="PromptTarget.send_prompt_async",
                anchor="api-pyrit_prompt_target-PromptTarget-send_prompt_async",
            )
        ]
    }
    out = _rewrite_symbol_refs("Call ``PromptTarget.send_prompt_async`` to dispatch.", index)
    assert "[``PromptTarget.send_prompt_async``]" in out
    assert "#api-pyrit_prompt_target-PromptTarget-send_prompt_async" in out


def test_rewrite_symbol_refs_resolves_bare_method_with_current_class() -> None:
    index = {
        "PromptTarget.send_prompt_async": [
            SymbolEntry(
                module="pyrit.prompt_target",
                kind="method",
                name="send_prompt_async",
                qualname="PromptTarget.send_prompt_async",
                anchor="api-pyrit_prompt_target-PromptTarget-send_prompt_async",
            )
        ],
        "send_prompt_async": [
            SymbolEntry(
                module="pyrit.prompt_target",
                kind="method",
                name="send_prompt_async",
                qualname="PromptTarget.send_prompt_async",
                anchor="api-pyrit_prompt_target-PromptTarget-send_prompt_async",
            )
        ],
    }
    out = _rewrite_symbol_refs("Then ``send_prompt_async`` is invoked.", index, current_class="PromptTarget")
    assert "[``send_prompt_async``]" in out


def test_rewrite_symbol_refs_skips_ambiguous_names() -> None:
    entry_a = SymbolEntry(module="pyrit.a", kind="class", name="Scorer", qualname="Scorer", anchor="api-pyrit_a-Scorer")
    entry_b = SymbolEntry(module="pyrit.b", kind="class", name="Scorer", qualname="Scorer", anchor="api-pyrit_b-Scorer")
    index = {"Scorer": [entry_a, entry_b]}
    out = _rewrite_symbol_refs("Use ``Scorer``.", index)
    assert out == "Use ``Scorer``."


def test_rewrite_symbol_refs_leaves_unknown_names_alone() -> None:
    out = _rewrite_symbol_refs("This is ``True`` and ``None``.", {})
    assert out == "This is ``True`` and ``None``."


def test_rewrite_symbol_refs_resolves_fully_qualified_name() -> None:
    entry = SymbolEntry(
        module="pyrit.models",
        kind="class",
        name="SeedPrompt",
        qualname="SeedPrompt",
        anchor="api-pyrit_models-SeedPrompt",
    )
    index = {"SeedPrompt": [entry], "pyrit.models.SeedPrompt": [entry]}
    out = _rewrite_symbol_refs("Use ``pyrit.models.SeedPrompt`` here.", index)
    assert "[``pyrit.models.SeedPrompt``](#api-pyrit_models-SeedPrompt)" in out


def test_rewrite_symbol_refs_preserves_fenced_code_blocks() -> None:
    index = {
        "SeedPrompt": [
            SymbolEntry(
                module="pyrit.models",
                kind="class",
                name="SeedPrompt",
                qualname="SeedPrompt",
                anchor="api-pyrit_models-SeedPrompt",
            )
        ]
    }
    text = (
        "Outside: ``SeedPrompt``.\n"
        "```python\n"
        "x = SeedPrompt()\n"
        "# ``SeedPrompt`` should not be linked here\n"
        "```\n"
        "After: ``SeedPrompt``."
    )
    out = _rewrite_symbol_refs(text, index)
    assert "[``SeedPrompt``](#api-pyrit_models-SeedPrompt)" in out.split("```")[0]
    assert "# ``SeedPrompt`` should not be linked here" in out
    # The closing "After" sentence should also be rewritten
    assert out.endswith("After: [``SeedPrompt``](#api-pyrit_models-SeedPrompt).")


def test_rewrite_symbol_refs_skips_existing_links() -> None:
    index = {"Foo": [SymbolEntry(module="pyrit.x", kind="class", name="Foo", qualname="Foo", anchor="api-pyrit_x-Foo")]}
    text = "Already-linked: [``Foo``](#api-pyrit_x-Foo)."
    out = _rewrite_symbol_refs(text, index)
    # No double-wrap
    assert out == text


def test_rewrite_symbol_refs_handles_tilde_and_dotted_prefix() -> None:
    entry = SymbolEntry(
        module="pyrit.models",
        kind="class",
        name="SeedPrompt",
        qualname="SeedPrompt",
        anchor="api-pyrit_models-SeedPrompt",
    )
    index = {"pyrit.models.SeedPrompt": [entry]}
    out = _rewrite_symbol_refs("Tilde form ``~pyrit.models.SeedPrompt`` works.", index)
    assert "(#api-pyrit_models-SeedPrompt)" in out


def test_rewrite_symbol_refs_empty_string_passthrough() -> None:
    assert _rewrite_symbol_refs("", {}) == ""
    assert _rewrite_symbol_refs(None, {}) is None  # type: ignore[arg-type]


def test_process_docstring_text_protects_doctest_examples() -> None:
    """The escape-then-rewrite order must wrap ``>>>`` blocks in fences
    *before* the symbol rewriter runs, so a known PyRIT symbol that happens
    to appear inside a doctest example stays as raw text instead of being
    turned into a MyST link (which would break the code sample)."""
    index = {
        "SeedPrompt": [
            SymbolEntry(
                module="pyrit.models",
                kind="class",
                name="SeedPrompt",
                qualname="SeedPrompt",
                anchor="api-pyrit_models-SeedPrompt",
            )
        ]
    }
    text = (
        "Returns a ``SeedPrompt`` instance.\n"
        "\n"
        "Example:\n"
        "    >>> sp = SeedPrompt(value='hi')\n"
        "    >>> assert isinstance(sp, SeedPrompt)\n"
        "    >>> print(sp)\n"
        "After the example, ``SeedPrompt`` is linkable again."
    )
    out = _process_docstring_text(text, index, current_class=None)
    assert out is not None
    # Prose before the doctest is linked.
    assert "[``SeedPrompt``](#api-pyrit_models-SeedPrompt) instance." in out
    # Doctest contents are fenced and NOT turned into MyST links.
    assert "```python" in out
    assert ">>> sp = SeedPrompt(value='hi')" in out
    assert "[SeedPrompt]" not in out  # bare-word inside doctest stays bare
    # Prose after the doctest is linked again.
    assert out.endswith("After the example, [``SeedPrompt``](#api-pyrit_models-SeedPrompt) is linkable again.")


def test_render_function_emits_anchor_and_links_docstring_fields() -> None:
    """End-to-end render path: a function with a linkable name in its
    description, parameter description, returns description, and raises
    description should produce a unique anchor label and MyST links
    everywhere the symbol appears."""
    index = {
        "PromptTarget": [
            SymbolEntry(
                module="pyrit.prompt_target",
                kind="class",
                name="PromptTarget",
                qualname="PromptTarget",
                anchor="api-pyrit_prompt_target-PromptTarget",
            )
        ]
    }
    func = {
        "name": "build_target",
        "kind": "function",
        "is_async": False,
        "signature": [{"name": "name", "type": "str", "kind": "positional or keyword"}],
        "returns_annotation": "PromptTarget",
        "docstring": {
            "text": "Construct a ``PromptTarget`` from a name.",
            "params": [
                {"name": "name", "type": "str", "desc": "Identifier for the ``PromptTarget``."},
            ],
            "returns": [{"type": "PromptTarget", "desc": "The constructed ``PromptTarget``."}],
            "raises": [{"type": "ValueError", "desc": "If no ``PromptTarget`` matches the name."}],
        },
    }
    out = render_function(func, module="pyrit.factories", symbol_index=index)

    # Anchor label is emitted for the function heading.
    assert "(api-pyrit_factories-build_target)=" in out
    # The function name still appears in the heading.
    assert "### `build_target`" in out
    # Every docstring field has been rewritten to link to the known symbol.
    expected_link = "[``PromptTarget``](#api-pyrit_prompt_target-PromptTarget)"
    assert out.count(expected_link) == 4


def test_render_function_uses_method_anchor_when_class_name_given() -> None:
    """Methods get a class-scoped anchor and the current_class context lets
    the rewriter resolve bare same-class method references."""
    index = {
        "PromptTarget.send_prompt_async": [
            SymbolEntry(
                module="pyrit.prompt_target",
                kind="method",
                name="send_prompt_async",
                qualname="PromptTarget.send_prompt_async",
                anchor="api-pyrit_prompt_target-PromptTarget-send_prompt_async",
            )
        ]
    }
    method = {
        "name": "validate",
        "kind": "function",
        "signature": [],
        "docstring": {"text": "Then ``send_prompt_async`` is invoked by the runtime."},
    }
    out = render_function(
        method,
        heading_level="####",
        module="pyrit.prompt_target",
        class_name="PromptTarget",
        symbol_index=index,
    )

    assert "(api-pyrit_prompt_target-PromptTarget-validate)=" in out
    assert "#### `validate`" in out
    assert "[``send_prompt_async``](#api-pyrit_prompt_target-PromptTarget-send_prompt_async)" in out


def _prompt_target_entry() -> SymbolEntry:
    return SymbolEntry(
        module="pyrit.prompt_target",
        kind="class",
        name="PromptTarget",
        qualname="PromptTarget",
        anchor="api-pyrit_prompt_target-PromptTarget",
    )


def test_format_bases_links_known_pyrit_base() -> None:
    index = {"PromptTarget": [_prompt_target_entry()]}
    out = _format_bases(["PromptTarget"], index)
    assert out == "[`PromptTarget`](#api-pyrit_prompt_target-PromptTarget)"


def test_format_bases_keeps_external_base_as_plain_code_span() -> None:
    """Bases not in the symbol index (stdlib types like ``str``/``Enum``) stay
    as plain backtick code spans instead of being mangled into broken links."""
    out = _format_bases(["str", "Enum"], {})
    assert out == "`str`, `Enum`"


def test_format_bases_links_mixed_pyrit_and_external() -> None:
    """A mix of resolvable and external bases produces a clean
    comma-separated list with only the known names linked."""
    index = {"PromptTarget": [_prompt_target_entry()]}
    out = _format_bases(["PromptTarget", "ABC", "Identifiable"], index)
    assert out == "[`PromptTarget`](#api-pyrit_prompt_target-PromptTarget), `ABC`, `Identifiable`"


def test_format_bases_empty_or_none_returns_empty_string() -> None:
    assert _format_bases([], {}) == ""
    # Without a symbol index we still emit plain code spans.
    assert _format_bases(["str"], None) == "`str`"


def test_render_class_emits_linked_bases_line() -> None:
    """End-to-end: a class with a known PyRIT base renders the ``Bases:`` line
    as a MyST link rather than a plain code span."""
    index = {"PromptTarget": [_prompt_target_entry()]}
    cls = {"name": "MyTarget", "kind": "class", "bases": ["PromptTarget", "str"]}
    out = render_class(cls, module="pyrit.factories", symbol_index=index)

    assert "(api-pyrit_factories-MyTarget)=" in out
    assert "Bases: [`PromptTarget`](#api-pyrit_prompt_target-PromptTarget), `str`" in out
    # No accidental wrapper backticks around the whole comma-joined list.
    assert "Bases: `PromptTarget" not in out


def test_render_class_without_bases_omits_bases_line() -> None:
    cls = {"name": "Standalone", "kind": "class", "bases": []}
    out = render_class(cls, module="pyrit.misc", symbol_index={})
    assert "Bases:" not in out


def test_format_reexport_alias_prefers_module_qualified_lookup() -> None:
    """The alias usually lives on the re-exporting module, so the FQN form
    (``mod_name.alias_name``) is tried before the short name."""
    canonical = SymbolEntry(
        module="pyrit.models",
        kind="class",
        name="SeedPrompt",
        qualname="SeedPrompt",
        anchor="api-pyrit_models-SeedPrompt",
    )
    re_exported = SymbolEntry(
        module="pyrit",
        kind="class",
        name="SeedPrompt",
        qualname="SeedPrompt",
        anchor="api-pyrit-SeedPrompt",
    )
    index = {
        "SeedPrompt": [canonical, re_exported],
        "pyrit.SeedPrompt": [re_exported],
        "pyrit.models.SeedPrompt": [canonical],
    }
    out = _format_reexport_alias("pyrit", "SeedPrompt", index)
    # Picks the alias's own page rather than the canonical definition page.
    assert out == "[`SeedPrompt`](#api-pyrit-SeedPrompt)"


def test_format_reexport_alias_falls_back_to_short_name() -> None:
    """When no module-qualified entry exists, the short-name rewriter still
    links unambiguous names so the re-export remains navigable."""
    entry = _prompt_target_entry()
    index = {"PromptTarget": [entry], "pyrit.prompt_target.PromptTarget": [entry]}
    out = _format_reexport_alias("pyrit", "PromptTarget", index)
    assert out == "[`PromptTarget`](#api-pyrit_prompt_target-PromptTarget)"


def test_format_reexport_alias_leaves_unresolvable_name_plain() -> None:
    out = _format_reexport_alias("pyrit.misc", "Mystery", {})
    assert out == "`Mystery`"


def test_format_reexport_target_links_fqn_when_indexed() -> None:
    entry = _prompt_target_entry()
    index = {"pyrit.prompt_target.PromptTarget": [entry]}
    out = _format_reexport_target("pyrit.prompt_target.PromptTarget", index)
    assert out == "[`pyrit.prompt_target.PromptTarget`](#api-pyrit_prompt_target-PromptTarget)"


def test_format_reexport_target_leaves_unresolvable_target_plain() -> None:
    out = _format_reexport_target("pyrit.unknown.Symbol", {})
    assert out == "`pyrit.unknown.Symbol`"


def test_format_reexport_target_empty_returns_empty() -> None:
    assert _format_reexport_target("", {}) == ""


def test_render_module_links_both_reexport_sides() -> None:
    """End-to-end: a module with an alias whose FQN target is in the index
    renders both the alias name and the target as MyST links."""
    canonical = _prompt_target_entry()
    re_exported = SymbolEntry(
        module="pyrit",
        kind="class",
        name="PromptTarget",
        qualname="PromptTarget",
        anchor="api-pyrit-PromptTarget",
    )
    index = {
        "PromptTarget": [canonical, re_exported],
        "pyrit.PromptTarget": [re_exported],
        "pyrit.prompt_target.PromptTarget": [canonical],
    }
    module = _fake_module(
        "pyrit",
        [{"name": "PromptTarget", "kind": "alias", "target": "pyrit.prompt_target.PromptTarget"}],
    )
    out = render_module(module, symbol_index=index)

    assert "## Re-exports" in out
    assert "[`PromptTarget`](#api-pyrit-PromptTarget)" in out
    assert "[`pyrit.prompt_target.PromptTarget`](#api-pyrit_prompt_target-PromptTarget)" in out
    assert " → " in out


def test_render_module_leaves_unresolvable_reexport_target_plain() -> None:
    """When a re-export target points outside the index (e.g. a fake/external
    path), it stays as a plain code span instead of becoming a broken link."""
    canonical = _prompt_target_entry()
    index = {
        "PromptTarget": [canonical],
        "pyrit.prompt_target.PromptTarget": [canonical],
    }
    module = _fake_module(
        "pyrit",
        [{"name": "Mystery", "kind": "alias", "target": "pyrit.unknown.Mystery"}],
    )
    out = render_module(module, symbol_index=index)

    assert "- `Mystery` → `pyrit.unknown.Mystery`" in out
    # Plain code spans, not links.
    assert "(#" not in out.split("## Re-exports")[1]


def test_render_module_emits_module_level_anchor_in_frontmatter() -> None:
    """The page-level label is emitted as a frontmatter ``label:`` field so
    cross-page references like ``[](#api-pyrit_prompt_target)`` target the
    page itself. MyST consumes the H1 as the page title and discards any
    label placed in the body before it, so frontmatter is the only reliable
    place to bind a page-level anchor."""
    module = _fake_module("pyrit.prompt_target", [_fake_class("PromptTarget")])
    out = render_module(module, symbol_index={})

    assert "label: api-pyrit_prompt_target" in out
    # Heading still present.
    assert "# pyrit.prompt_target" in out
    # Frontmatter still wraps the page.
    assert out.startswith("---")
    assert "short_title: prompt_target" in out
    # Label is inside the frontmatter, not after it.
    fm_end = out.index("---\n", 4)  # skip the opening "---"
    assert out.index("label: api-pyrit_prompt_target") < fm_end


def test_render_module_label_uses_module_slug_for_nested_packages() -> None:
    module = _fake_module("pyrit.executor.attack", [_fake_class("AttackStrategy")])
    out = render_module(module, symbol_index={})

    assert "label: api-pyrit_executor_attack" in out
    assert "# pyrit.executor.attack" in out


def _write_doc(doc_root: Path, relative_path: str, content: str) -> None:
    path = doc_root / relative_path
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")


def _write_toc(doc_root: Path, *files: str) -> Path:
    toc_path = doc_root / "myst.yml"
    entries = "\n".join(f"    - file: {file}" for file in files)
    toc_path.write_text(f"project:\n  toc:\n{entries}\n", encoding="utf-8")
    return toc_path


def test_build_example_index_scans_toc_user_docs_and_used_imports(tmp_path: Path) -> None:
    symbol_index = _build_symbol_index(
        [
            _fake_module(
                "pyrit.targets",
                [
                    _fake_class("DirectTarget"),
                    _fake_class("AliasedTarget"),
                    _fake_class("UnusedTarget"),
                    _fake_class("LookalikeTarget"),
                    _fake_function("used_function"),
                    _fake_function("unused_function"),
                    _fake_class("InternalTarget"),
                ],
            )
        ]
    )
    toc_path = _write_toc(
        tmp_path,
        "guide/targets.md",
        "guide/invalid.md",
        "contributing/development.md",
    )
    _write_doc(
        tmp_path,
        "guide/targets.md",
        """---
title: Target examples
---

   ```python
   from pyrit.targets import AliasedTarget as Target
   from pyrit.targets import DirectTarget, UnusedTarget
   from pyrit.targets import unused_function, used_function
   from pyrite.targets import LookalikeTarget

   DirectTarget()
   Target()
   used_function()
   LookalikeTarget()
   ```
""",
    )
    _write_doc(tmp_path, "guide/invalid.md", "# Invalid\n\n```python\nthis is not valid Python !!!\n```\n")
    _write_doc(
        tmp_path,
        "contributing/development.md",
        "# Internal development\n\n```python\nfrom pyrit.targets import InternalTarget\nInternalTarget()\n```\n",
    )
    _write_doc(
        tmp_path,
        "guide/not-in-toc.md",
        "# Orphan\n\n```python\nfrom pyrit.targets import UnusedTarget\nUnusedTarget()\n```\n",
    )

    result = _build_example_index(doc_root=tmp_path, toc_path=toc_path, symbol_index=symbol_index)
    expected = [ExampleReference(title="Target examples", path="guide/targets.md")]

    assert result[_class_anchor("pyrit.targets", "DirectTarget")] == expected
    assert result[_class_anchor("pyrit.targets", "AliasedTarget")] == expected
    assert _class_anchor("pyrit.targets", "UnusedTarget") not in result
    assert _class_anchor("pyrit.targets", "LookalikeTarget") not in result
    assert result[_function_anchor("pyrit.targets", "used_function")] == expected
    assert _function_anchor("pyrit.targets", "unused_function") not in result
    assert _class_anchor("pyrit.targets", "InternalTarget") not in result


def test_build_example_index_uses_jupytext_companions_dedupes_and_sorts(tmp_path: Path) -> None:
    symbol_index = _build_symbol_index([_fake_module("pyrit.targets", [_fake_class("PromptTarget")])])
    toc_path = _write_toc(tmp_path, "guide/zulu.ipynb", "guide/alpha.ipynb")
    for stem in ("zulu", "alpha"):
        _write_doc(tmp_path, f"guide/{stem}.ipynb", "{}")
    _write_doc(
        tmp_path,
        "guide/zulu.py",
        """# %% [markdown]
# # Zulu guide

# %%
from pyrit.targets import PromptTarget as Target

# %%
Target()
Target()
""",
    )
    _write_doc(
        tmp_path,
        "guide/alpha.py",
        """# %% [markdown]
# # Alpha guide

# %%
from pyrit.targets import PromptTarget

# %%
PromptTarget()
""",
    )

    result = _build_example_index(doc_root=tmp_path, toc_path=toc_path, symbol_index=symbol_index)

    assert result[_class_anchor("pyrit.targets", "PromptTarget")] == [
        ExampleReference(title="Alpha guide", path="guide/alpha.ipynb"),
        ExampleReference(title="Zulu guide", path="guide/zulu.ipynb"),
    ]


def test_build_example_index_resolves_module_alias_and_unique_short_name(tmp_path: Path) -> None:
    symbol_index = _build_symbol_index(
        [
            _fake_module(
                "pyrit.targets",
                [_fake_class("ModuleTarget"), _fake_class("DeepTarget"), _fake_function("execute")],
            ),
            _fake_module("pyrit.prompt_target", [_fake_class("TargetCapabilities")]),
            _fake_module("pyrit.models", [_fake_class("TargetCapabilities")]),
            _fake_module("pyrit.first", [_fake_class("AmbiguousTarget")]),
            _fake_module("pyrit.second", [_fake_class("AmbiguousTarget")]),
        ]
    )
    toc_path = _write_toc(tmp_path, "guide/imports.md")
    _write_doc(
        tmp_path,
        "guide/imports.md",
        """# Import styles

```python
import pyrit.targets as targets
from pyrit.prompt_target.common.target_capabilities import TargetCapabilities
from pyrit.targets.internal.deep import AmbiguousTarget, DeepTarget

targets.ModuleTarget.execute()
TargetCapabilities()
DeepTarget()
AmbiguousTarget()
```
""",
    )

    result = _build_example_index(doc_root=tmp_path, toc_path=toc_path, symbol_index=symbol_index)
    expected = [ExampleReference(title="Import styles", path="guide/imports.md")]

    assert result[_class_anchor("pyrit.targets", "ModuleTarget")] == expected
    assert result[_class_anchor("pyrit.targets", "DeepTarget")] == expected
    assert result[_class_anchor("pyrit.prompt_target", "TargetCapabilities")] == expected
    assert _class_anchor("pyrit.models", "TargetCapabilities") not in result
    assert _function_anchor("pyrit.targets", "execute") not in result
    assert _class_anchor("pyrit.first", "AmbiguousTarget") not in result
    assert _class_anchor("pyrit.second", "AmbiguousTarget") not in result


def test_build_example_index_reads_standalone_notebook_code_cells(tmp_path: Path) -> None:
    symbol_index = _build_symbol_index([_fake_module("pyrit.targets", [_fake_class("NotebookTarget")])])
    toc_path = _write_toc(tmp_path, "guide/notebook.ipynb")
    _write_doc(
        tmp_path,
        "guide/notebook.ipynb",
        """{
  "cells": [
    {"cell_type": "markdown", "source": ["# Notebook guide"]},
    {"cell_type": "code", "source": ["from pyrit.targets import NotebookTarget\\n", "NotebookTarget()"]}
  ]
}""",
    )

    result = _build_example_index(doc_root=tmp_path, toc_path=toc_path, symbol_index=symbol_index)

    assert result[_class_anchor("pyrit.targets", "NotebookTarget")] == [
        ExampleReference(title="Notebook guide", path="guide/notebook.ipynb")
    ]


def test_render_module_adds_examples_to_functions_and_classes() -> None:
    module = _fake_module(
        "pyrit.examples",
        [_fake_function("run_example"), _fake_class("ExampleTarget", methods=["run"])],
    )
    examples_by_anchor = {
        _function_anchor("pyrit.examples", "run_example"): [
            ExampleReference(title="Function guide", path="guide/function.md")
        ],
        _class_anchor("pyrit.examples", "ExampleTarget"): [
            ExampleReference(title="Class guide", path="guide/class.ipynb")
        ],
    }

    out = render_module(module, symbol_index={}, examples_by_anchor=examples_by_anchor)

    assert out.count("**Examples:**") == 2
    assert "- [Function guide](../guide/function.md)" in out
    assert "- [Class guide](../guide/class.ipynb)" in out
    class_section = out.split("## `ExampleTarget`", 1)[1]
    assert class_section.index("- [Class guide]") < class_section.index("**Methods:**")


def test_example_link_path_uses_configured_api_directory_depth() -> None:
    assert (
        _example_link_path(
            "guide/example.md",
            api_md_dir=Path("doc/reference/generated/api"),
            doc_root=Path("doc"),
        )
        == "../../../guide/example.md"
    )


def test_render_module_does_not_attach_examples_to_methods() -> None:
    module = _fake_module("pyrit.examples", [_fake_class("ExampleTarget", methods=["run"])])
    examples_by_anchor = {
        _method_anchor("pyrit.examples", "ExampleTarget", "run"): [
            ExampleReference(title="Method guide", path="guide/method.md")
        ]
    }

    out = render_module(module, symbol_index={}, examples_by_anchor=examples_by_anchor)

    assert "Method guide" not in out
