"""Tests for the standalone XY documentation application."""

import ast
import hashlib
import importlib.util
import inspect
import json
import re
import tomllib
import xml.etree.ElementTree as ET
from collections.abc import Iterator
from pathlib import Path
from urllib.parse import urlparse

import pytest
from reflex_base.components.memo import MemoComponent
from reflex_docgen.markdown import (
    Block,
    BoldSpan,
    CodeBlock,
    DirectiveBlock,
    HeadingBlock,
    ImageSpan,
    ItalicSpan,
    LinkSpan,
    ListBlock,
    QuoteBlock,
    Span,
    StrikethroughSpan,
    TableBlock,
    TextBlock,
    parse_document,
)
from reflex_site_shared.docs import render_markdown
from reflex_site_shared.docs.content import discover_docs
from reflex_site_shared.docs.markdown import _file_modules
from reflex_site_shared.docs.models import DocsPage
from rxconfig import config
from xy_docs.api_reference import (
    API_REFERENCE_HEADING,
    AXES_AND_ANNOTATIONS,
    CHART_FACTORY_GROUPS,
    CHROME_AND_BEHAVIOR,
    MARKS,
    append_component_api_markdown,
    axes_and_annotations_api,
    chart_containers_api,
    chart_factories_api,
    chrome_and_behavior_api,
    component_api_callables,
    component_api_markdown,
    component_api_paths,
    component_api_references,
    component_page_api,
    marks_api,
)
from xy_docs.breadcrumb import _breadcrumb_parts, xy_docs_breadcrumb
from xy_docs.config import DOCS_CONFIG, DOCS_NAVIGATION, DOCS_REDIRECTS, DOCS_SECTIONS
from xy_docs.constants import PUBLIC_DOCS_URL, PUBLIC_XY_VERSION, SOCIAL_IMAGE_URL
from xy_docs.footer import xy_docs_footer
from xy_docs.gallery import (
    _GALLERY_GROUPS,
    _gallery_preview_svg,
    chart_gallery_grid,
)
from xy_docs.markdown import (
    XyDocsMarkdownTransformer,
    _split_demo_data,
    page_with_api_reference_toc,
    render_xy_markdown_page,
)
from xy_docs.navbar import XY_REPOSITORY_URL, xy_docs_navbar
from xy_docs.sidebar import (
    CHART_FAMILY_SIDEBAR_SECTIONS,
    CHART_GALLERY_SIDEBAR_LINK,
    INTEGRATION_LINK_ICONS,
    PIE_DOCS_ROUTE,
    POLAR_DOCS_ROUTE,
    POLAR_DOCS_ROUTES,
    POLAR_SIDEBAR_SECTION,
    RADAR_DOCS_ROUTE,
    RADIAL_BAR_DOCS_ROUTE,
    SIDEBAR_SECTION_GROUPS,
    WIND_ROSE_DOCS_ROUTE,
    _section_items,
    xy_docs_sidebar,
    xy_docs_sidebar_comp,
)
from xy_docs.xy_docs import _CHART_STYLE, _DOCS_ROUTES, app

import reflex_xy
import xy
from xy.components import _MARK_APPLIERS, _POLAR_INERT_AXIS_KEYWORDS

SITEMAP_NAMESPACE = {"sitemap": "https://www.sitemaps.org/schemas/sitemap/0.9"}
DOCS_APP_ROOT = Path(__file__).resolve().parent.parent
DOCS_ROOT = DOCS_APP_ROOT.parent
EXPORTED_SITEMAP = DOCS_APP_ROOT / ".web" / "public" / "sitemap.xml"
CHECK_HTML_ROUTES_PATH = DOCS_APP_ROOT / "scripts" / "check_html_routes.py"
CHECK_HTML_ROUTES_SPEC = importlib.util.spec_from_file_location(
    "xy_docs_check_html_routes",
    CHECK_HTML_ROUTES_PATH,
)
if CHECK_HTML_ROUTES_SPEC is None or CHECK_HTML_ROUTES_SPEC.loader is None:
    msg = f"Unable to load route validator: {CHECK_HTML_ROUTES_PATH}"
    raise RuntimeError(msg)
check_html_routes = importlib.util.module_from_spec(CHECK_HTML_ROUTES_SPEC)
CHECK_HTML_ROUTES_SPEC.loader.exec_module(check_html_routes)


def _walk_spans(spans: tuple[Span, ...]) -> Iterator[LinkSpan]:
    """Yield links recursively from a Markdown span tree.

    Args:
        spans: Markdown spans to traverse.

    Yields:
        Every discovered link span.
    """
    for span in spans:
        if isinstance(span, LinkSpan):
            yield span
            yield from _walk_spans(span.children)
        elif isinstance(span, (BoldSpan, ItalicSpan, StrikethroughSpan, ImageSpan)):
            yield from _walk_spans(span.children)


def _walk_blocks(blocks: tuple[Block, ...]) -> Iterator[LinkSpan]:
    """Yield links recursively from Markdown blocks, excluding code fences.

    Args:
        blocks: Markdown blocks to traverse.

    Yields:
        Every discovered documentation link.
    """
    for block in blocks:
        if isinstance(block, (HeadingBlock, TextBlock)):
            yield from _walk_spans(block.children)
        elif isinstance(block, ListBlock):
            for item in block.items:
                yield from _walk_blocks(item.children)
        elif isinstance(block, (QuoteBlock, DirectiveBlock)):
            yield from _walk_blocks(block.children)
        elif isinstance(block, TableBlock):
            for row in (block.header, *block.rows):
                for cell in row.cells:
                    yield from _walk_spans(cell.children)


def _normalize_xy_docs_path(url: str) -> str | None:
    """Return an app-relative route for a canonical XY docs URL.

    Args:
        url: Absolute or path-only documentation URL.

    Returns:
        Normalized app route, or ``None`` when the frontend path does not match.
    """
    path = urlparse(url).path.rstrip("/") or "/"
    if path == "/docs/xy":
        return "/"
    if path.startswith("/docs/xy/"):
        return path.removeprefix("/docs/xy")
    return None


def _sitemap_routes(sitemap_path: Path) -> set[str]:
    """Read normalized XY routes from an exported sitemap.

    Args:
        sitemap_path: Generated sitemap file.

    Returns:
        Routes published beneath ``/docs/xy``.
    """
    root = ET.parse(sitemap_path).getroot()
    return {
        route
        for location in root.findall("sitemap:url/sitemap:loc", SITEMAP_NAMESPACE)
        if location.text is not None
        if (route := _normalize_xy_docs_path(location.text.strip())) is not None
    }


def _markdown_files(root: Path) -> Iterator[Path]:
    """Yield source Markdown files outside generated and vendor directories.

    Args:
        root: Documentation tree to scan.

    Yields:
        Markdown source paths in deterministic order.
    """
    skipped = {".git", ".venv", ".web", "build", "dist", "node_modules", "__pycache__"}
    for path in sorted(root.rglob("*.md")):
        if not skipped.intersection(path.relative_to(root).parts):
            yield path


def _link_line(source: str, target: str, cursor: int) -> tuple[int, int]:
    """Locate a Markdown link target after the previous match.

    Args:
        source: Original Markdown source.
        target: Parsed link target.
        cursor: Offset after the previous match.

    Returns:
        The one-based line number and next search offset, or zero and the
        unchanged offset when the target cannot be located.
    """
    needle = "](" + target
    position = source.find(needle, cursor)
    if position == -1:
        position = source.find("]: " + target, cursor)
    if position == -1:
        return 0, cursor
    return source.count("\n", 0, position) + 1, position + len(needle)


def test_public_markdown_routes_match_the_docs_navigation() -> None:
    """Discover the exact nine-section, two-level public information architecture."""
    assert tuple(title for title, _route, _icon, _leaves in DOCS_SECTIONS) == (
        "Overview",
        "Core Concepts",
        "Styling",
        "Chart Gallery",
        "Components",
        "Integrations",
        "Guides",
        "Advanced",
        "Reference",
    )
    assert (
        next(
            landing_route
            for title, landing_route, _icon, _leaves in DOCS_SECTIONS
            if title == "Chart Gallery"
        )
        == "/overview/gallery/"
    )
    section_routes = tuple(
        dict.fromkeys(
            route
            for _title, landing_route, _icon, leaves in DOCS_SECTIONS
            for route in (
                landing_route,
                *(leaf_route for _leaf_title, leaf_route in leaves),
            )
        )
    )
    assert set(section_routes) == set(DOCS_NAVIGATION)
    assert DOCS_NAVIGATION[:4] == (
        "/",
        "/overview/installation/",
        "/overview/first-chart/",
        "/guides/dataframes-and-real-data/",
    )
    assert DOCS_NAVIGATION[-1] == "/overview/benchmarks/"
    assert "/overview/gallery/" in DOCS_NAVIGATION
    assert "/charts/" not in DOCS_NAVIGATION
    core_concept_leaves = next(
        leaves for title, _route, _icon, leaves in DOCS_SECTIONS if title == "Core Concepts"
    )
    assert ("Animations", "/core-concepts/animations/") not in core_concept_leaves
    styling_leaves = next(
        leaves for title, _route, _icon, leaves in DOCS_SECTIONS if title == "Styling"
    )
    chart_gallery_leaves = next(
        leaves for title, _route, _icon, leaves in DOCS_SECTIONS if title == "Chart Gallery"
    )
    assert ("Animations", "/styling/animations/") in styling_leaves
    assert ("Chrome Slots", "/styling/chrome-slots/") in styling_leaves
    assert tuple(
        leaf
        for leaf in chart_gallery_leaves
        if leaf[1]
        in {
            PIE_DOCS_ROUTE,
            POLAR_DOCS_ROUTE,
            RADAR_DOCS_ROUTE,
            RADIAL_BAR_DOCS_ROUTE,
            WIND_ROSE_DOCS_ROUTE,
        }
    ) == (
        ("Polar", POLAR_DOCS_ROUTE),
        ("Radar", RADAR_DOCS_ROUTE),
        ("Radial Bar", RADIAL_BAR_DOCS_ROUTE),
        ("Pie & Donut", PIE_DOCS_ROUTE),
        ("Wind Rose", WIND_ROSE_DOCS_ROUTE),
    )
    assert "/styling/chrome-slots/" not in DOCS_REDIRECTS
    assert (
        max(len(tuple(part for part in route.split("/") if part)) for route in section_routes) <= 2
    )

    pages = discover_docs(DOCS_CONFIG)

    assert tuple(page.route for page in pages) == DOCS_NAVIGATION
    assert all(page.description for page in pages)


def test_docs_app_configures_the_reflex_xy_adapter() -> None:
    """Compile live documentation examples through the shipped Reflex adapter."""
    assert config.telemetry_enabled is False
    assert any(isinstance(plugin, reflex_xy.XYPlugin) for plugin in config.plugins)


def test_benchmark_demo_uses_the_shared_theme() -> None:
    from xy_docs.demos import benchmark_charts

    from xy._benchmark_theme import (
        benchmark_chart_class,
        benchmark_live_theme,
    )

    assert benchmark_chart_class() == benchmark_charts._CHART_CLASS

    live_theme = benchmark_live_theme()
    assert benchmark_charts._theme().style == {
        "background": live_theme["background"],
        "--chart-bg": live_theme["plot_background"],
        "--chart-grid": live_theme["grid_color"],
        "--chart-axis": live_theme["axis_color"],
        "--chart-text": live_theme["text_color"],
    }


def test_docs_app_does_not_override_the_builtin_toolbar_palette() -> None:
    assert not any(key.startswith("--chart-modebar-") for key in _CHART_STYLE)


def test_tailwind_styling_docs_match_the_reflex_plugin_contract() -> None:
    """Keep the Reflex scan-path guidance aligned with the configured plugin."""
    content = (DOCS_ROOT / "styling/chrome-slots.md").read_text(encoding="utf-8")

    tailwind = next(
        plugin for plugin in config.plugins if plugin.__class__.__name__ == "TailwindV4Plugin"
    )
    assert tailwind.config.get("darkMode") == "selector"
    assert "literal class strings in Tailwind's default scan" in content
    assert "original Python or Markdown file" in content
    assert "charts produced from a token or `Var`" in content
    assert "tailwind_classes=LIVE_CHART_CLASSES" in content
    assert "never becomes a DOM attribute" in content
    assert 'darkMode": "selector"' in content


def test_styling_troubleshooting_covers_common_host_and_export_failures() -> None:
    """Keep the public styling entry point useful when visual CSS fails."""
    content = (DOCS_ROOT / "styling/index.md").read_text(encoding="utf-8")

    for requirement in (
        "## Styling troubleshooting",
        "Tailwind classes are present but have no effect",
        "A custom font silently falls back",
        "Marks are WebGL/canvas geometry",
        "to_html(custom_css=...)",
        "engine=Engine.chromium",
        "overflow-hidden",
        'height="100%"',
        "normal cascade",
    ):
        assert requirement in content


def test_component_styling_matrix_covers_public_chrome_boundaries() -> None:
    """Keep the styling guide explicit about built-in and custom components."""
    content = (DOCS_ROOT / "styling/component-variations.md").read_text(encoding="utf-8")
    expected = {
        "legend_item",
        "legend_swatch",
        "tooltip(fields=..., labels=..., title=..., format=...)",
        "x_axis(...)",
        "y_axis(...)",
        "vline(x)",
        "hline(y)",
        "crosshair_x",
        "crosshair_y",
        "selection",
        "legend(render=...)",
        "tooltip(render=...)",
        "colorbar(render=...)",
    }

    missing = {value for value in expected if value not in content}
    assert not missing
    assert "button_class_name=" in content
    assert "button_style=" in content
    assert "built-in `reflex_xy.chart` adapter mounts `xy.tooltip(render=...)`" in content
    assert "Legend and colorbar replacements remain framework-owned siblings" in " ".join(
        content.split()
    )


def test_tooltip_docs_cover_named_series_and_polar_coordinates() -> None:
    """Keep the default tooltip readout aligned with the client renderer."""
    content = " ".join((DOCS_ROOT / "components/tooltips.md").read_text(encoding="utf-8").split())

    for requirement in (
        "hovered series name",
        "label the radial row `r`",
        "drop the numeric angle",
        "An authored spoke label",
        'labels={"x": ...}',
    ):
        assert requirement in content


def test_styling_docs_cover_every_public_dom_slot() -> None:
    """Make a new stable browser slot fail docs CI until it is documented."""
    chrome = (DOCS_ROOT / "styling/chrome-slots.md").read_text(encoding="utf-8")
    variations = (DOCS_ROOT / "styling/component-variations.md").read_text(encoding="utf-8")

    assert all(f"`{slot}`" in chrome for slot in xy.CHART_DOM_SLOTS)
    for slot in ("root", "title", "chrome", "canvas", "labels", "badge", "badge_item"):
        assert f"`{slot}`" in variations


def test_styling_docs_cover_every_rendered_mark_family() -> None:
    """Tie the mark-style matrix to the actual declarative mark registry."""
    content = (DOCS_ROOT / "styling/mark-styles.md").read_text(encoding="utf-8").lower()

    missing = {
        kind
        for kind in _MARK_APPLIERS
        if kind not in content and kind.replace("_", " ") not in content
    }
    assert not missing


def test_styling_gallery_covers_retained_advanced_surfaces() -> None:
    """Keep the focused gallery explicit about its retained advanced surfaces."""
    content = (DOCS_ROOT / "styling/gallery.md").read_text(encoding="utf-8").lower()

    required_marks = {
        "area",
        "ecdf",
        "heatmap",
        "contour",
        "hexbin",
        "error_band",
        "errorbar",
        "segments",
        "stem",
        "triangle_mesh",
    }
    assert all(f"`{mark}`" in content for mark in required_marks)

    required_surfaces = {
        "custom legend",
        "styled cursor tooltip",
        "reduction badge",
        "facet_chart",
        "to_html(custom_css=",
        "xy.tooltip(",
    }
    assert all(surface in content for surface in required_surfaces)
    assert "colorbar_bar" in content
    assert "colorbar_tick" in content
    assert "colorbar_title" in content


def test_chart_examples_are_wide_copyable_demos_without_a_toc() -> None:
    """Keep the dedicated examples page spacious and copy-ready."""
    page = next(page for page in discover_docs(DOCS_CONFIG) if page.route == "/styling/examples/")
    document = parse_document(page.content)
    demos = [
        block
        for block in document.blocks
        if isinstance(block, CodeBlock)
        and {"demo", "exec", "toggle", "preview-code"} <= set(block.flags)
    ]

    assert len(demos) >= 4
    assert all(block.content.count("def ") == 1 for block in demos)
    assert all("reflex_xy.chart" in block.content for block in demos)
    assert all('"grid_opacity": 0' in block.content for block in demos)
    assert all('"axis_color": "#00000000"' in block.content for block in demos)
    assert all("xy.vline(" not in block.content for block in demos)
    assert sum("xy.area(" in block.content for block in demos) >= 2
    assert sum("xy.bar(" in block.content or "xy.column(" in block.content for block in demos) >= 4

    stacked_demo = next(block for block in demos if "def stacked_product_mix" in block.content)
    assert stacked_demo.content.count("xy.column(") == 3
    assert stacked_demo.content.count("corner_radius=(6, 0)") == 1
    assert "base=growth_base" in stacked_demo.content
    assert "base=enterprise_base" in stacked_demo.content

    rendered_page = str(render_xy_markdown_page(page))
    demo_count = len(demos)
    # Only demos whose marked data section exceeds ten nonblank lines render a
    # third trigger/panel; the rest keep their data in the Code tab.
    data_demos = sum(_split_demo_data(block.content)[0] is not None for block in demos)
    assert data_demos == 2
    assert rendered_page.count("XYChart") == demo_count + 6
    assert rendered_page.count('value:"preview"') == demo_count * 2
    assert rendered_page.count('value:"code"') == demo_count * 2
    assert rendered_page.count('value:"data"') == data_demos * 2
    assert rendered_page.count("xy-example-tab cursor-pointer") == demo_count * 2 + data_demos
    assert rendered_page.count("xy-example-tab-list relative") == demo_count
    assert (
        rendered_page.count("flex w-full flex-col gap-2 overflow-hidden px-2 pb-2 pt-4")
        == demo_count
    )
    assert rendered_page.count("w-full p-2 [&>div]:!m-0") == demo_count
    assert rendered_page.count("min-h-[430px] w-full px-1 py-2 sm:px-2 sm:py-7") == data_demos
    assert "xy-chart-examples" in rendered_page
    assert "max-width: 88rem" in rendered_page
    assert "div:has(#toc-navigation)" in rendered_page
    assert "display: none" in rendered_page
    assert rendered_page.count('id:"responsive-combo-chart"') == 1
    assert rendered_page.count('id:"responsive-combo-chart-demo"') == 1
    assert "var(--chart-" not in page.content
    assert "currentColor" not in page.content
    assert '"transparent"' not in page.content


def test_demo_data_tabs_require_more_than_ten_nonblank_lines() -> None:
    """Keep short literals with Code and reserve Data tabs for long datasets."""
    short_data = "\n".join(f"row_{index} = {index}" for index in range(10))
    long_data = "\n".join(f"row_{index} = {index}" for index in range(11))
    chart_code = "chart = xy.line_chart(xy.line([0, 1], [0, 1]))"

    short_tab, short_code = _split_demo_data(f"{short_data}\n\n# --- chart ---\n{chart_code}")
    long_tab, long_code = _split_demo_data(f"{long_data}\n\n# --- chart ---\n{chart_code}")

    assert short_tab is None
    assert short_data in short_code
    assert chart_code in short_code
    assert "# --- chart ---" not in short_code
    assert long_tab == long_data
    assert long_code == chart_code


def test_animation_replay_demos_reuse_example_chrome_and_controls() -> None:
    """Keep animation demos visually aligned with the polished examples."""
    page = next(page for page in discover_docs(DOCS_CONFIG) if page.route == "/styling/animations/")
    content = page.content

    assert content.count("ui.button(") == 8
    assert content.count('ui.icon("PlayIcon"') == 5
    assert content.count('variant="outline"') == 8
    assert content.count("flex w-full justify-end px-4 pt-4 sm:px-5 sm:pt-5") == 3
    assert "rx.button(" not in content
    assert 'aria_label="Play mark override animation"' in content
    assert 'aria_label="Play lifecycle animation"' in content
    assert "2 exits · 2 enters · 3 retained" in content
    assert '"sales": [54, 16, 46, 22, 60]' in content
    assert "random.uniform(-0.75, 0.75)" in content
    assert "StreamingAnimationDemo.append_points(1)" in content
    assert "StreamingAnimationDemo.append_points(5)" in content
    assert "StreamingAnimationDemo.append_points(12)" in content
    assert "linear-gradient(#8e51ff4d 5%, #8e51ff00 95%)" in content
    assert '"axis_color": "#00000000"' in content

    rendered = str(render_xy_markdown_page(page))
    assert rendered.count("border border-secondary-a4 bg-secondary-1") >= 8
    assert rendered.count("Play animation") >= 5


def test_palette_playground_drives_a_reactive_chart_grid() -> None:
    """Keep preset and custom colors wired to the state-backed XY figures."""
    from xy_docs.playground import (
        BERRY_PALETTE,
        PLAYGROUND_PALETTES,
        ChartPlaygroundState,
        chart_playground,
    )

    rendered_page = str(chart_playground())
    state_source = inspect.getsource(ChartPlaygroundState)
    state = ChartPlaygroundState(_reflex_internal_init=True)

    assert rendered_page.count("XYChart") == 6
    assert rendered_page.count('type:"color"') == 3
    assert rendered_page.count("apply_palette") == len(PLAYGROUND_PALETTES) == 5
    assert "reset_palette" in rendered_page
    for role in ("Primary", "Secondary", "Accent"):
        assert f"{role} chart color" in rendered_page
        assert f"set_{role.lower()}_color" in rendered_page
    assert state_source.count('self.preset = "Custom"') == 3
    assert "self.primary, self.secondary, self.accent = BERRY_PALETTE" in state_source
    assert state_source.count("xy.theme(palette=") == 6
    assert "_HIDDEN_AXIS_STYLE" not in state_source
    assert '"axis_color": "#00000000"' not in state_source
    assert "xy.x_axis(tick_count=6, show=False)" in state_source
    assert "xy.y_axis(domain=(0, 80), show=False, grid=True)" in state_source
    for handler, value, role in (
        (ChartPlaygroundState.set_primary_color, "#123456", "primary"),
        (ChartPlaygroundState.set_secondary_color, "#abcdef", "secondary"),
        (ChartPlaygroundState.set_accent_color, "#fedcba", "accent"),
    ):
        handler.fn(state, value)
        assert state.preset == "Custom"
        assert getattr(state, role) == value
    ChartPlaygroundState.reset_palette.fn(state)
    assert state.preset == "Berry"
    assert (state.primary, state.secondary, state.accent) == BERRY_PALETTE
    assert "xy-palette-playground" in rendered_page
    assert "grid grid-cols-1 gap-5 xl:grid-cols-2" in rendered_page
    for label, palette in PLAYGROUND_PALETTES:
        assert label in rendered_page
        assert len(palette) == 3
        assert all(re.fullmatch(r"#[0-9a-f]{6}", color) for color in palette)
    assert {
        "momentum",
        "comparison",
        "product_mix",
        "funnel",
        "traffic_share",
        "channel_mix",
    } <= set(ChartPlaygroundState.computed_vars)
    assert "div:has(#toc-navigation)" in rendered_page
    assert "display: none" in rendered_page


def test_custom_color_docs_require_literal_palette_entries() -> None:
    """Do not promise browser-only colors for renderer-neutral indexed palettes."""
    content = (DOCS_ROOT / "styling/customize.md").read_text(encoding="utf-8")
    colormap_rule = content[
        content.index("One rule follows from that") : content.index(
            "### Recolor categories with a chart palette"
        )
    ]

    assert "not as a colormap stop or an `xy.theme(palette=...)` entry" in colormap_rule
    assert "indexed color lookups used by static renderers" in colormap_rule


def test_markdown_heading_links_are_route_local_after_client_navigation() -> None:
    """Do not let a previously visited docs route leak into heading self-links."""
    pages = {page.route: page for page in discover_docs(DOCS_CONFIG)}

    for route in ("/overview/gallery/", "/styling/gallery/"):
        rendered = str(render_xy_markdown_page(pages[route]))
        assert 'to:"#' in rendered
        assert "router_rx_state_" not in rendered


def test_colorbar_docs_match_the_declarative_and_custom_boundaries() -> None:
    """Keep inferred built-ins distinct from opaque framework replacements."""
    content = (DOCS_ROOT / "components/colorbars.md").read_text(encoding="utf-8")

    for option in (
        "title=",
        "orientation=",
        "ticks=",
        "colorbar(show=False)",
    ):
        assert option in content
    for slot in (
        "colorbar",
        "colorbar_bar",
        "colorbar_extension",
        "colorbar_line",
        "colorbar_tick",
        "colorbar_minor_tick",
        "colorbar_title",
    ):
        assert f"`{slot}`" in content
    assert "The last compatible continuous mark wins" in content
    assert "does not currently mount custom chrome" in content


def test_custom_font_docs_cover_browser_and_static_export_boundaries() -> None:
    """Keep font loading advice honest across browser and native outputs."""
    content = (DOCS_ROOT / "styling/themes-and-tokens.md").read_text(encoding="utf-8")

    for requirement in (
        "## Custom fonts and export limitations",
        "@font-face",
        'style={"font_family":',
        "Engine.chromium",
        "Toolbar PNG",
        "Toolbar SVG",
        "Native PNG",
        "Python `to_svg()`",
        "baked bitmap font",
    ):
        assert requirement in content


def test_theme_component_demo_uses_site_color_mode_tokens() -> None:
    """Keep the introductory theme demo neutral, vivid, and mode-responsive."""
    content = (DOCS_ROOT / "styling/themes-and-tokens.md").read_text(encoding="utf-8")
    demo = content.split("~~~python demo exec", 1)[1].split("~~~", 1)[0]

    for token in (
        "--demo-surface:#ffffff",
        "dark:[--demo-surface:#000000]",
        "--demo-grid:#e5e7eb",
        'color="#f43f5e"',
        'fill="linear-gradient(#f43f5e4d 5%, #f43f5e00 95%)"',
        "xy.legend(show=False)",
        '"grid_opacity": 0',
    ):
        assert token in demo


def test_generated_mark_api_does_not_claim_canvas_marks_are_dom_nodes() -> None:
    """Keep generated class_name descriptions aligned with canvas rendering."""
    for factory in MARKS:
        if "class_name" not in inspect.signature(factory).parameters:
            continue
        docstring = inspect.getdoc(factory) or ""
        assert "DOM class name applied to the mark" not in docstring
        assert "Adapter-only trace metadata" in docstring


def test_styling_docs_cover_every_annotation_factory_and_alias() -> None:
    """Keep every public annotation primitive visible in the styling guide."""
    content = (DOCS_ROOT / "styling/component-variations.md").read_text(encoding="utf-8")
    factories = (
        "vline",
        "hline",
        "x_band",
        "y_band",
        "text",
        "label",
        "marker",
        "arrow",
        "threshold",
        "threshold_zone",
        "callout",
    )

    assert all(f"`{factory}" in content for factory in factories)


def test_what_is_xy_restores_the_sdf_hero_and_ends_with_a_short_pitch() -> None:
    """Keep the original visual opening and the merged Why XY section."""
    content = (DOCS_ROOT / "index.md").read_text(encoding="utf-8")

    heading = content.index("# What is `xy`?")
    styling = content.index("**Styled by your CSS, not ours.**")
    hero = content.index("~~~python demo-only exec")
    early_alpha = content.index("**Early alpha.**")
    start_here = content.index("## Start here")
    why_xy = content.index("## Why XY")
    why_copy = content[why_xy:]

    assert heading < styling < hero < early_alpha < start_here < why_xy
    assert content.rfind("\n## ") + 1 == why_xy
    assert "from xy_docs.demos.xy_sdf_plots import xy_sdf_plot_grid" in content
    assert "All four interactive charts" in content
    assert "View the customizable Python source" in content
    assert "/docs/xy/overview/first-chart/" in content
    assert "/docs/xy/overview/why-xy/" not in content
    assert not (DOCS_ROOT / "overview/why-xy.md").exists()
    # The pitch-length cap measures prose (pitch plus the closing install
    # call-to-action); embedded demo and shell fences don't count.
    why_prose = re.sub(r"~~~.*?~~~|```.*?```", "", why_copy, flags=re.DOTALL)
    assert len(why_prose.split()) < 260
    assert "recorded live interactive sweep" in why_copy
    assert "Compare by workflow, not by slogan" not in why_copy


def test_core_concepts_keep_essential_binding_content_visible() -> None:
    """Render core semantics as normal content and avoid teaching default no-ops."""
    data = (DOCS_ROOT / "core-concepts/data.md").read_text(encoding="utf-8")
    configuration = (DOCS_ROOT / "core-concepts/configuration.md").read_text(encoding="utf-8")
    overview = (DOCS_ROOT / "index.md").read_text(encoding="utf-8")

    assert "## Color strings" in data
    assert "### Color strings" not in data
    assert "~~~md alert info\n## Color strings" not in data
    assert "density=None" not in configuration
    assert "~~~md alert warning\n**Early alpha.**" in overview
    assert "### Early alpha" not in overview


def test_sdf_demo_assets_remain_reproducible_for_deeper_examples() -> None:
    """Keep the cached SDF demo and its licensed font reproducible."""
    demo_source = (DOCS_APP_ROOT / "xy_docs/demos/xy_sdf_plots.py").read_text(encoding="utf-8")
    font = DOCS_APP_ROOT / "xy_docs/assets/InstrumentSans-wdth-wght.ttf"
    license_file = DOCS_APP_ROOT / "xy_docs/assets/OFL.txt"

    assert "sample_points: int = 50_000" in demo_source
    assert "density_points: int = 1_000_000" in demo_source
    assert "density_display_points: int = 250_000" in demo_source
    assert "@lru_cache(maxsize=4)" in demo_source
    assert (
        'class_name="grid w-full grid-cols-1 gap-0 overflow-hidden md:grid-cols-2"' in demo_source
    )
    assert "InstrumentSans-wdth-wght.ttf" in demo_source
    assert 'width="100%"' in demo_source
    assert 'height="auto"' in demo_source
    assert hashlib.sha256(font.read_bytes()).hexdigest() == (
        "b24f1812584816958afcf22e22d08e44318c5e51651e25d2438efdde389b33b1"
    )
    assert "SIL OPEN FONT LICENSE Version 1.1" in license_file.read_text(encoding="utf-8")


def test_sdf_plot_grid_is_cached_and_uses_reflex_toolbar_tokens() -> None:
    """Build once while keeping every chart toolbar tied to Reflex tokens."""
    from dataclasses import replace

    from xy_docs.demos.xy_sdf_plots import (
        DEFAULT_CONFIG,
        build_sdf_plots,
        xy_sdf_plot_grid,
    )

    config = replace(
        DEFAULT_CONFIG,
        font_size=180,
        x_height_bins=12,
        sample_points=1_000,
        density_points=2_000,
        density_display_points=1_500,
        bin_min_count=2,
        heatmap_stride=4,
        contour_cells_per_bin=4,
    )
    first = build_sdf_plots(config)
    assert build_sdf_plots(config) is first
    assert first.reading_order == (
        first.bins_scatter,
        first.heatmap,
        first.contours,
        first.million_scatter,
    )
    density_mark = first.million_scatter.children[0]
    assert density_mark.kind == "scatter"
    assert len(density_mark.x) == config.density_display_points
    assert density_mark.props["density"] is False
    sizes = density_mark.props["size"]
    assert density_mark.props["size_range"] == (float(sizes.min()), float(sizes.max()))
    assert sizes.min() >= config.density_size_offset
    assert sizes.max() <= config.density_size_offset + config.density_size_scale

    rendered = str(xy_sdf_plot_grid(config))
    expected_tokens = {
        "--chart-text": "var(--secondary-11)",
        "--chart-focus": "var(--primary-9)",
    }

    for name, value in expected_tokens.items():
        assert rendered.count(f'["{name}"] : "{value}"') == 4
    assert 'id:"xy-sdf-plot-grid"' in rendered
    assert "gap-0" in rendered


def test_sdf_heatmap_outside_support_uses_page_background() -> None:
    """Use the page surface outside support without changing the PDF ramp."""
    from xy_docs.demos import xy_sdf_plots as demo

    colors = demo._heatmap_colors(
        demo.np.array([[0.0, 1e-12, 1.0]]),
        demo.DEFAULT_CONFIG,
        demo.DEFAULT_PALETTE,
    )
    background = demo.DEFAULT_PALETTE.dark_background.removeprefix("#")
    expected = [int(background[index : index + 2], 16) / 255 for index in (0, 2, 4)]
    assert demo.DEFAULT_PALETTE.dark_background == "#090A0B"
    assert colors[0, 0, :3] == pytest.approx(expected)
    assert colors[0, 1, :3] != pytest.approx(expected)
    assert colors[0, 2, :3] != pytest.approx(expected)
    assert demo.np.allclose(colors[..., 3], 1)


def test_sdf_distance_transform_is_exact_without_scipy() -> None:
    """Keep the signed-distance model exact without a heavyweight dependency."""
    from xy_docs.demos import xy_sdf_plots as demo

    mask = demo.np.array(
        [
            [True, True, True, True, True],
            [True, False, True, False, True],
            [True, True, True, True, True],
            [True, True, False, True, True],
        ]
    )
    zero_cells = demo.np.argwhere(~mask)
    expected = demo.np.empty(mask.shape, dtype=float)
    for cell in demo.np.ndindex(mask.shape):
        expected[cell] = demo.np.sqrt(demo.np.min(demo.np.sum((zero_cells - cell) ** 2, axis=1)))

    assert demo.np.allclose(demo._distance_transform_edt(mask), expected)

    manifest = tomllib.loads((DOCS_APP_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
    assert not any(
        dependency.lower().startswith("scipy") for dependency in manifest["project"]["dependencies"]
    )


def test_public_docs_use_the_xy_namespace_without_the_legacy_alias() -> None:
    """Keep examples, generated references, and docs tests on the public name."""
    legacy_alias = "".join(("f", "c"))
    forbidden = (
        f"import xy as {legacy_alias}",
        f"{legacy_alias}.",
        f"data-{legacy_alias}-",
        f".{legacy_alias}-",
        f'namespace="{legacy_alias}"',
    )
    sources = {page.source_path for page in discover_docs(DOCS_CONFIG)}
    sources.update((DOCS_APP_ROOT / "xy_docs").rglob("*.py"))
    sources.update((DOCS_APP_ROOT / "tests").rglob("*.py"))

    violations = [
        f"{path.relative_to(DOCS_ROOT)}:{line_number}: {line.strip()}"
        for path in sorted(sources)
        for line_number, line in enumerate(
            path.read_text(encoding="utf-8").splitlines(),
            1,
        )
        if any(token in line for token in forbidden)
    ]

    assert not violations, "\n".join(violations)


def test_public_python_fences_are_valid_python() -> None:
    """Keep every Python-labelled source block syntactically copyable."""
    fence_pattern = re.compile(
        r"^(?P<fence>`{3}|~{3})python[^\n]*\n(?P<body>.*?)^(?P=fence)\s*$",
        re.MULTILINE | re.DOTALL,
    )
    failures: list[str] = []

    for path in _markdown_files(DOCS_ROOT):
        for match in fence_pattern.finditer(path.read_text(encoding="utf-8")):
            try:
                ast.parse(match.group("body"), filename=path.as_posix())
            except SyntaxError as error:
                failures.append(f"{path.relative_to(DOCS_ROOT)}: {error}")

    assert not failures, "\n".join(failures)


def test_handwritten_output_reference_tracks_the_public_api() -> None:
    """Keep the handwritten Engine and Chart method reference synchronized."""
    method_reference = (DOCS_ROOT / "api-reference/figure-methods.md").read_text(encoding="utf-8")
    method_blocks = [
        block for block in parse_document(method_reference).blocks if isinstance(block, CodeBlock)
    ]

    assert len(method_blocks) == 4
    assert all(block.language == "python" for block in method_blocks)
    assert "animation_progress=None" in method_blocks[0].content
    for method in (xy.Chart.to_html, xy.Chart.html):
        assert "animation_progress" in inspect.signature(method).parameters

    type_reference = (DOCS_ROOT / "api-reference/public-types.md").read_text(encoding="utf-8")
    for engine in xy.Engine:
        assert f"`{engine.__class__.__name__}.{engine.name}`" in type_reference


def test_live_preview_markdown_builds_real_xy_components(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Compile every live preview through docgen and the static XY adapter."""
    app_payload_dir = DOCS_APP_ROOT / "assets" / "xy"
    app_payloads_before = {path: path.read_bytes() for path in app_payload_dir.glob("*.xyf")}
    monkeypatch.chdir(tmp_path)
    pages = [
        page
        for page in discover_docs(DOCS_CONFIG)
        if any(marker in page.content for marker in check_html_routes.LIVE_PREVIEW_MARKERS)
    ]

    assert pages
    referenced_payloads: set[str] = set()
    for page in pages:
        rendered = str(
            render_markdown(
                page.content,
                virtual_filepath=page.relative_path.as_posix(),
                filename=page.source_path.as_posix(),
            )
        )
        assert "XYChart" in rendered, page.relative_path
        assert "Interactive preview coming soon" not in page.content
        page_payloads = set(re.findall(r'src:"(?:/docs/xy)?/xy/([^"]+\.xyf)"', rendered))
        assert page_payloads, page.relative_path
        referenced_payloads.update(page_payloads)

    generated_payloads = {path.name for path in (tmp_path / "assets" / "xy").glob("*.xyf")}
    assert generated_payloads == referenced_payloads
    assert {
        path: path.read_bytes() for path in app_payload_dir.glob("*.xyf")
    } == app_payloads_before


def test_complete_styling_examples_render_live_previews() -> None:
    """Do not show component-producing styling examples as source code only."""
    violations: list[str] = []
    for path in sorted((DOCS_ROOT / "styling").glob("*.md")):
        content = path.read_text(encoding="utf-8")
        for fence, body in re.findall(r"~~~(python[^\n]*)\n(.*?)\n~~~", content, re.DOTALL):
            if re.search(r"^def\s+\w+\(", body, re.MULTILINE) and "demo exec" not in fence:
                violations.append(f"{path.relative_to(DOCS_ROOT)}: {fence}")

    assert not violations, "\n".join(violations)


def test_every_core_concepts_python_example_renders_a_live_chart() -> None:
    """Keep Core Concepts examples visual instead of leaving code-only snippets."""
    violations: list[str] = []
    example_count = 0
    for path in sorted((DOCS_ROOT / "core-concepts").glob("*.md")):
        content = path.read_text(encoding="utf-8")
        for fence, body in re.findall(r"~~~(python[^\n]*)\n(.*?)\n~~~", content, re.DOTALL):
            example_count += 1
            if "demo exec" not in fence or "reflex_xy.chart" not in body:
                violations.append(f"{path.relative_to(DOCS_ROOT)}: {fence}")

    assert example_count == 17
    assert not violations, "\n".join(violations)


def test_single_chart_demos_use_one_tabbed_parent_card() -> None:
    """Keep one tabbed preview surface without a nested chart-owned card."""
    gallery = (DOCS_ROOT / "styling/gallery.md").read_text(encoding="utf-8")
    blocks = re.findall(r"~~~(python[^\n]*)\n(.*?)\n~~~", gallery, re.DOTALL)
    trend = next(body for _fence, body in blocks if "trend_mark_atlas_preview" in body)
    facets = next(body for _fence, body in blocks if "styled_facet_preview" in body)

    chrome_slots = (DOCS_ROOT / "styling/chrome-slots.md").read_text(encoding="utf-8")
    chrome_blocks = re.findall(r"~~~(python[^\n]*)\n(.*?)\n~~~", chrome_slots, re.DOTALL)
    tailwind_chrome = next(
        body for _fence, body in chrome_blocks if "tailwind_chrome_preview" in body
    )

    assert "rounded-2xl border border-slate-200 bg-white" not in trend
    assert "overflow-hidden rounded-xl border border-slate-200" not in facets
    assert "rounded-xl border border-slate-200 bg-white" not in tailwind_chrome

    content = """~~~python demo exec
import reflex as rx

def one_chart_preview():
    return rx.box("chart", width="100%")
~~~"""
    for virtual_filepath in ("docs/styling/test.md", "docs/charts/test.md"):
        rendered = str(
            XyDocsMarkdownTransformer(
                virtual_filepath=virtual_filepath,
                filename=virtual_filepath,
            ).transform(parse_document(content))
        )
        assert rendered.count("xy-example-tab-list relative") == 1
        assert rendered.count("xy-example-tab cursor-pointer") == 2
        assert rendered.count("relative w-full overflow-hidden rounded-xl") == 1
        assert "flex w-full flex-col gap-2 overflow-hidden px-2 pb-2 pt-4" in rendered
        assert "w-full p-2 [&>div]:!m-0" in rendered
        assert "min-h-[430px] w-full p-2" not in rendered
        assert "sm:py-7" not in rendered
    assert "bg-secondary-2" not in rendered


def test_styling_demos_pair_light_surfaces_with_readable_text() -> None:
    """Prevent light demo panels from inheriting low-contrast dark-mode text."""
    violations: list[str] = []
    for path in sorted((DOCS_ROOT / "styling").glob("*.md")):
        content = path.read_text(encoding="utf-8")
        for _fence, body in re.findall(
            r"~~~(python demo exec[^\n]*)\n(.*?)\n~~~",
            content,
            re.DOTALL,
        ):
            if "bg-white" in body and "dark:bg" not in body:
                violations.append(f"{path.relative_to(DOCS_ROOT)}: unpaired bg-white")
            if re.search(r'(?:plot_background|"background")\s*[:=]\s*"#f', body):
                violations.append(f"{path.relative_to(DOCS_ROOT)}: fixed light background")
            if '"background": "rgb(255 255 255' in body and '"color":' not in body:
                violations.append(f"{path.relative_to(DOCS_ROOT)}: light component without color")

    assert not violations, "\n".join(violations)

    gallery = (DOCS_ROOT / "styling/gallery.md").read_text(encoding="utf-8")
    assert '"background": "var(--chart-bg)"' in gallery
    assert '"colorbar_tick": {"color": "var(--chart-text)"}' in gallery


@pytest.mark.parametrize(
    ("relative_path", "demo_name", "expected_live_blocks"),
    (("core-concepts/index.md", "composition_model_demo", 2),),
)
def test_beginner_examples_use_tabbed_demos(
    relative_path: str,
    demo_name: str,
    expected_live_blocks: int,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Keep beginner code and its live result together in XY's tab model."""
    source_path = DOCS_ROOT / relative_path
    content = source_path.read_text(encoding="utf-8")
    live_blocks = [
        block
        for block in parse_document(content).blocks
        if isinstance(block, CodeBlock)
        and block.language == "python"
        and {"demo", "exec"} <= set(block.flags)
    ]

    assert len(live_blocks) == expected_live_blocks
    assert "demo-only" not in live_blocks[0].flags
    assert demo_name in live_blocks[0].content
    assert "reflex_xy.chart" in live_blocks[0].content

    export_calls: list[tuple[tuple, dict]] = []

    def forbid_export(*args, **kwargs) -> None:
        export_calls.append((args, kwargs))
        msg = "DocDemo executed chart.to_html()"
        raise AssertionError(msg)

    monkeypatch.chdir(tmp_path)
    rendered = str(
        XyDocsMarkdownTransformer(
            virtual_filepath=f"tests/docdemo/{relative_path}",
            filename=source_path.as_posix(),
        ).transform(parse_document(content))
    )

    assert rendered.count("xy-example-tab-list relative") == expected_live_blocks
    assert rendered.count("xy-example-tab cursor-pointer") == expected_live_blocks * 2
    assert rendered.count('value:"preview"') == expected_live_blocks * 2
    assert rendered.count('value:"code"') == expected_live_blocks * 2
    assert 'value:"data"' not in rendered
    assert rendered.count("XYChart") == expected_live_blocks
    assert demo_name in rendered
    assert export_calls == []
    assert not (tmp_path / "scatter.html").exists()


def test_composition_demos_keep_distinct_assets_after_cached_render(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Keep each demo bound to its own globals when the page module is reused."""
    source_path = DOCS_ROOT / "core-concepts" / "index.md"
    content = source_path.read_text(encoding="utf-8")
    virtual_filepath = "tests/docdemo/core-concepts/cache-regression.md"
    payload_pattern = re.compile(r'src:"(?:/docs/xy)?/xy/([^"]+\.xyf)"')

    def render() -> str:
        return str(
            XyDocsMarkdownTransformer(
                virtual_filepath=virtual_filepath,
                filename=source_path.as_posix(),
            ).transform(parse_document(content))
        )

    monkeypatch.chdir(tmp_path)
    first_payloads = payload_pattern.findall(render())
    second_payloads = payload_pattern.findall(render())

    assert len(first_payloads) == 2
    assert len(set(first_payloads)) == 2
    assert second_payloads == first_payloads


def test_repeated_identical_fence_keeps_the_page_namespace_accumulating(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """A duplicated fence must not roll the page back to its first occurrence.

    Snapshots are keyed by fence source *and* occurrence: two identical
    fences are two positions in the page's fence sequence, and the second one
    restoring the first one's namespace would discard everything the fences
    between them defined — the shared renderer skips re-execution and lets
    the namespace keep accumulating, and this seam must not change that.
    """
    duplicated = "shared = 1\n"
    content = "\n".join(
        (
            "~~~python exec",
            duplicated.rstrip("\n"),
            "~~~",
            "",
            "~~~python exec",
            "between = 2",
            "~~~",
            "",
            "~~~python exec",
            duplicated.rstrip("\n"),
            "~~~",
            "",
            "~~~python exec",
            "def result():",
            "    return (shared, between)",
            "~~~",
        )
    )
    virtual_filepath = "tests/docdemo/repeated-fence/page.md"

    def render() -> dict:
        transformer = XyDocsMarkdownTransformer(
            virtual_filepath=virtual_filepath,
            filename=virtual_filepath,
        )
        transformer.transform(parse_document(content))
        return transformer.env

    monkeypatch.chdir(tmp_path)
    first, second = render(), render()
    # `between` survives the duplicate fence on the first render and on every
    # later one; a KeyError here is the regression.
    assert first["result"]() == (1, 2)
    assert second["result"]() == (1, 2)


def test_editing_a_page_drops_the_fence_snapshots_keyed_to_its_old_layout(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """A page edit must not let a fence restore a snapshot from another position.

    Snapshot keys carry the fence's occurrence index, so removing or reordering
    a duplicated fence renumbers every surviving copy. Without invalidation the
    renumbered fence adopts the snapshot the *other* copy left behind — a
    namespace captured before the bindings that now precede it — and the page
    silently renders from stale data. The dev server re-renders in-process on
    reload, so this is the live path, not a hypothetical one.
    """
    source_path = tmp_path / "editable.md"

    def page_of(*fences: str) -> DocsPage:
        content = "\n".join(f"~~~python exec\n{body}\n~~~\n" for body in fences)
        source_path.write_text(content)
        return DocsPage(
            source_path=source_path,
            relative_path=Path("editable.md"),
            route="/docs/xy/editable",
            title="Editable",
            description=None,
            metadata={},
            content=content,
        )

    duplicated = "shared = 1"
    monkeypatch.chdir(tmp_path)

    # First layout: the duplicate sits at occurrence 1, after `between`, so
    # occurrence 0's snapshot is the one captured before `between` existed.
    render_xy_markdown_page(
        page_of(duplicated, "between = 2", duplicated, "first = (shared, between)")
    )
    # The edit removes the leading copy, renumbering the survivor to occurrence
    # 0 — and revises the trailing fence, as a real edit does, so it is a cache
    # miss and runs against whatever namespace the restore left behind.
    render_xy_markdown_page(page_of("between = 2", duplicated, "second = (shared, between)"))

    module = _file_modules[str(source_path.resolve())]
    assert module.__dict__["second"] == (1, 2)


def _assigned_names(target: ast.expr) -> set[str]:
    """Every top-level name an assignment target binds, unpacking included."""
    if isinstance(target, ast.Name):
        return {target.id}
    if isinstance(target, (ast.Tuple, ast.List)):
        return {name for element in target.elts for name in _assigned_names(element)}
    if isinstance(target, ast.Starred):
        return _assigned_names(target.value)
    return set()  # attribute/subscript targets rebind no module-level name


#: Statements that run at module scope and can hold further bindings in their
#: bodies. `FunctionDef`/`ClassDef` are deliberately absent: they bind their own
#: name (handled below) but their bodies are a separate scope, so a name
#: assigned inside one is not a module-level binding and cannot shadow a
#: sibling fence's.
_NESTED_SCOPE_FREE_BODIES = (
    ast.For,
    ast.AsyncFor,
    ast.While,
    ast.If,
    ast.With,
    ast.AsyncWith,
    ast.Try,
    ast.Match,
)


def _module_bindings(node: ast.AST) -> set[str]:
    """Module-level names one statement binds, recursing into nested bodies."""
    bound: set[str] = set()
    if isinstance(node, ast.Assign):
        for target in node.targets:
            bound |= _assigned_names(target)
    elif isinstance(node, (ast.AnnAssign, ast.AugAssign)):
        bound |= _assigned_names(node.target)
    elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
        bound.add(node.name)  # the body is another scope; the name is not
        return bound
    elif isinstance(node, (ast.Import, ast.ImportFrom)):
        bound |= {(alias.asname or alias.name).split(".")[0] for alias in node.names}
    elif isinstance(node, (ast.For, ast.AsyncFor)):
        bound |= _assigned_names(node.target)
    elif isinstance(node, (ast.With, ast.AsyncWith)):
        for item in node.items:
            if item.optional_vars is not None:
                bound |= _assigned_names(item.optional_vars)
    if isinstance(node, ast.Try):
        bound |= {handler.name for handler in node.handlers if handler.name}
    if isinstance(node, ast.Match):
        for case in node.cases:
            bound |= {
                capture.name
                for capture in ast.walk(case.pattern)
                if isinstance(capture, (ast.MatchAs, ast.MatchStar)) and capture.name
            }
    if isinstance(node, _NESTED_SCOPE_FREE_BODIES):
        for child in ast.iter_child_nodes(node):
            if isinstance(child, ast.stmt):
                bound |= _module_bindings(child)
    # Walrus binds at the enclosing scope wherever the expression appears.
    for inner in ast.walk(node):
        if isinstance(inner, ast.NamedExpr):
            bound |= _assigned_names(inner.target)
    return bound


def _bound_names(body: str) -> set[str]:
    """Every module-level name one fence binds.

    Plain assignment is not the only way to shadow a name a sibling fence also
    owns: annotated, augmented and unpacked assignment, `def`/`class`, imports,
    `for`/`with`/`except`/`match` targets, and walrus all do it — and the
    compound statements among them nest, so a binding can sit inside a
    top-level `for` or `if` body. Missing any of those forms under-reports
    rebound names and silently drops pages from the guarded test, which then
    still passes while covering less.
    """
    return {name for node in ast.parse(body).body for name in _module_bindings(node)}


def _names_rebound_across_exec_fences(content: str) -> set[str]:
    """Top-level names that more than one exec fence of a page binds."""
    seen: set[str] = set()
    rebound: set[str] = set()
    for fence, body in re.findall(r"~~~python([^\n]*)\n(.*?)\n~~~", content, re.DOTALL):
        if "exec" not in fence:
            continue
        bound = _bound_names(body)
        rebound |= bound & seen
        seen |= bound
    return rebound


def test_rebound_name_detection_covers_every_binding_form() -> None:
    """Pin the detector itself: the page filter above is only as complete as
    the binding forms it recognizes."""
    fence = "~~~python exec\n{body}\n~~~"
    declared = (
        "plain",
        "annotated",
        "augmented",
        "unpacked",
        "spread",
        "fn",
        "Cls",
        "np",
        "pi",
        "looped",
        "handle",
        "caught",
        "matched",
        "walrus",
        "nested",
    )
    page = "\n".join(
        fence.format(body=body)
        for body in (
            "\n".join(f"{name} = 0" for name in declared),
            "plain = 2",
            "annotated: int = 2",
            "augmented += 2",
            "unpacked, other = (2, 3)",
            "*spread, tail = [2, 3]",
            "def fn():\n    return 2",
            "class Cls:\n    pass",
            "import numpy as np",
            "from math import pi",
            "for looped in range(2):\n    pass",
            "with open(__file__) as handle:\n    pass",
            "try:\n    pass\nexcept ValueError as caught:\n    pass",
            "match [1]:\n    case [matched]:\n        pass",
            "if (walrus := 2):\n    pass",
            # A binding inside a top-level compound body is still module scope.
            "for _ in range(1):\n    if True:\n        nested = 2",
            # Neither rebinds a module-level name: attribute/subscript targets,
            # and names assigned inside a function body (a separate scope).
            "obj.attr = 2\nmapping['key'] = 2",
            "def _unrelated():\n    plain = 3\n    return plain",
        )
    )
    assert _names_rebound_across_exec_fences(page) == set(declared)


def test_repeated_page_renders_rebuild_every_demo_from_its_own_data(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Keep cached fences bound to their own globals, not the page's last ones.

    Pages are evaluated more than once per process — the frontend compile, then
    the `reflex_xy` worker-startup pass over unevaluated pages — and demos that
    reuse names such as `months` must not pick up a later demo's arrays on the
    second pass. Content-addressed payload names make that visible: identical
    demo data renders identical `.xyf` sources.
    """
    monkeypatch.chdir(tmp_path)
    payload_pattern = re.compile(r'src:"(?:/docs/xy)?/xy/([^"]+\.xyf)"')
    pages = [
        page
        for page in discover_docs(DOCS_CONFIG)
        if _names_rebound_across_exec_fences(page.content)
    ]

    assert pages
    for page in pages:
        route = page.relative_path.as_posix()
        virtual_filepath = f"tests/docdemo/repeated-render/{route}"
        renders = [
            str(
                XyDocsMarkdownTransformer(
                    virtual_filepath=virtual_filepath,
                    filename=page.source_path.as_posix(),
                ).transform(parse_document(page.content))
            )
            for _ in range(2)
        ]

        first, second = (payload_pattern.findall(rendered) for rendered in renders)
        assert first, route
        assert second == first, route


def test_exact_readout_demo_is_backed_by_live_reflex_state() -> None:
    """Keep the readout driven by chart events instead of compiled sample values."""
    source_path = DOCS_ROOT / "core-concepts" / "interactions.md"
    content = source_path.read_text(encoding="utf-8")
    live_block = next(
        block
        for block in parse_document(content).blocks
        if isinstance(block, CodeBlock)
        and block.language == "python"
        and "def exact_readout_demo" in block.content
    )

    for marker in (
        "class ExactReadoutState(rx.State)",
        "reflex_xy.inline(exact_readout_chart)",
        "on_point_click=ExactReadoutState.pick_point",
        "on_select_end=ExactReadoutState.select_points",
        "ExactReadoutState.picked_row",
        "ExactReadoutState.selected_x",
        "ExactReadoutState.selected_y",
    ):
        assert marker in live_block.content
    assert "chart.pick(" not in live_block.content
    assert "chart.select_range(" not in live_block.content


def test_first_chart_shows_clean_code_with_hidden_live_previews(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Keep the beginner script copy-paste clean while demo-only fences render results."""
    source_path = DOCS_ROOT / "overview" / "first-chart.md"
    content = source_path.read_text(encoding="utf-8")
    python_blocks = [
        block
        for block in parse_document(content).blocks
        if isinstance(block, CodeBlock) and block.language == "python"
    ]
    shown = [block for block in python_blocks if "exec" not in block.flags]
    hidden = [block for block in python_blocks if {"demo-only", "exec"} <= set(block.flags)]

    # No visible DocDemo blocks: everything the reader copies stays plumbing-free.
    assert not [b for b in python_blocks if {"demo", "exec"} <= set(b.flags)]
    assert len(hidden) == 2
    assert all("reflex_xy.chart" in block.content for block in hidden)
    assert "def first_chart_demo():" in hidden[0].content
    assert "def million_point_demo():" in hidden[1].content

    script = shown[0].content
    assert 'chart.to_html("scatter.html")' in script
    assert "reflex_xy" not in script
    assert "__main__" not in script
    assert "def " not in script
    assert any("2_500_000" in block.content for block in shown)

    export_calls: list[tuple[tuple, dict]] = []

    def forbid_export(*args, **kwargs) -> None:
        export_calls.append((args, kwargs))
        msg = "hidden demo executed chart.to_html()"
        raise AssertionError(msg)

    monkeypatch.setattr(xy.Chart, "to_html", forbid_export)
    monkeypatch.chdir(tmp_path)
    rendered = str(
        render_markdown(
            content,
            virtual_filepath="tests/docdemo/overview/first-chart.md",
            filename=source_path.as_posix(),
        )
    )
    assert rendered.count("XYChart") == 2
    assert export_calls == []
    assert not (tmp_path / "scatter.html").exists()


def test_installation_uses_uv_first_package_manager_tabs() -> None:
    """Present install commands as tabs with uv selected by default."""
    source_path = DOCS_ROOT / "overview" / "installation.md"
    rendered = str(
        render_markdown(
            source_path.read_text(encoding="utf-8"),
            virtual_filepath="overview/installation.md",
            filename=source_path.as_posix(),
        )
    )

    assert 'defaultValue:"tab1"' in rendered
    assert rendered.count('className:"pill-tab"') == 2
    assert rendered.index('value:"tab1"},"uv"') < rendered.index('value:"tab2"},"pip"')
    assert 'code:"uv add xy"' in rendered
    assert 'code:"python -m pip install xy"' in rendered


def test_installation_distinguishes_supported_targets_from_pypi_artifacts() -> None:
    """Do not present missing release wheels as missing platform support."""
    content = (DOCS_ROOT / "overview" / "installation.md").read_text(encoding="utf-8")
    windows_row = next(line for line in content.splitlines() if line.startswith("| Windows |"))
    wasm_row = next(line for line in content.splitlines() if line.startswith("| WebAssembly |"))

    assert all(value in windows_row for value in ("`x86_64`", "`x86`", "`arm64`"))
    assert "| Supported | Not included |" in windows_row
    assert "Pyodide 314" in wasm_row
    assert "`wasm32` | Supported | Not in 0.0.1 (on PyPI since 0.0.3) |" in wasm_row
    assert "Windows is supported by XY's native core and release pipeline." in content
    assert "0.0.1 PyPI upload does not include Windows wheels" in content
    assert "runtime-verified WebAssembly wheel" in content
    assert "`pyemscripten_2026_0_wasm32` platform" in content
    assert "%pip install xy" in content


def test_chart_gallery_grid_renders_every_type_as_inline_svg(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Show every chart type as an editable preview without WebGL payloads."""
    app_payload_dir = DOCS_APP_ROOT / "assets" / "xy"
    app_payloads_before = {path: path.read_bytes() for path in app_payload_dir.glob("*.xyf")}
    monkeypatch.chdir(tmp_path)

    rendered = str(chart_gallery_grid())
    chart_section = next(
        leaves for title, _landing_route, _icon, leaves in DOCS_SECTIONS if title == "Chart Gallery"
    )
    assert len(chart_section) == 21
    assert "XYChart" not in rendered
    assert rendered.count("dangerouslySetInnerHTML") == 35
    assert rendered.count('id:"xy-chart-gallery"') == 1
    assert rendered.count("main:has(#xy-chart-gallery) > div:has(#toc-navigation)") == 1
    assert rendered.count("main:has(#xy-chart-gallery) > div:has(article #xy-chart-gallery)") == 1
    assert rendered.count("display: none") == 1
    assert rendered.count("max-width: 88rem") == 1
    assert rendered.count("2xl:grid-cols-3") == 9
    assert rendered.count("aspect-[320/232]") == 35
    assert rendered.count("shadow-large") == 35
    assert rendered.count("transition-bg") == 35
    assert "--gallery-preview-surface: #fff" in rendered
    assert "--gallery-preview-fill: #efeaff" in rendered
    assert "--gallery-preview-soft: #dccfff" in rendered
    assert "--gallery-preview-bar: #dccfff" in rendered
    assert "--gallery-preview-stroke: #a790f0" in rendered
    assert "--gallery-preview-strong: #8067d7" in rendered
    assert "--gallery-preview-muted" not in rendered
    assert "object-contain" not in rendered
    assert "object-center" not in rendered
    assert "xy-tailwind-bridge" not in rendered
    assert rendered.count("size:14") == 35
    assert "size:6" not in rendered
    for chart_type in (
        "Funnel",
        "Line",
        "Area",
        "Step",
        "Stairs",
        "Scatter",
        "Polar",
        "Radar",
        "Radial Bar",
        "Pie & Donut",
        "Wind Rose",
        "Bar",
        "Column",
        "Histogram",
        "ECDF",
        "Box",
        "Violin",
        "Hexbin",
        "Heatmap",
        "Contour",
        "Error Band",
        "Error Bar",
        "Stem",
        "Segments",
        "Sankey",
        "Threshold",
        "Triangle Mesh",
        "Horizontal Line",
        "Vertical Line",
        "Bands",
        "Callout",
        "Arrow",
        "Label",
        "Text",
        "Threshold Zone",
        "Facet Chart",
        "Layered Marks",
    ):
        assert chart_type in rendered
    for _title, route in chart_section:
        assert f'to:"{route}"' in rendered
        assert f'to:"/docs/xy{route}"' not in rendered
    for group in _GALLERY_GROUPS:
        if group.route is not None:
            assert f'to:"{group.route}"' in rendered
            assert f'"aria-label":"Open the {group.title} chart family guide"' in rendered
        for item in group.items:
            route = item.route or group.route
            assert route is not None
            assert f'to:"{route}' in rendered
            assert f'"aria-label":"Open the {item.title} guide"' in rendered
    assert '"aria-label":"Open the Bar, Column, and Scatter chart family guide"' not in rendered
    assert 'to:"/components/annotations/"' in rendered
    assert 'to:"/charts/annotations/"' not in rendered

    assert not list((tmp_path / "assets" / "xy").glob("*.xyf"))
    assert {
        path: path.read_bytes() for path in app_payload_dir.glob("*.xyf")
    } == app_payloads_before


def test_chart_gallery_inline_svgs_share_the_component_preview_style() -> None:
    """Keep all code-native previews on the Reflex 320 by 232 tile system."""
    previews = {
        item.title: _gallery_preview_svg(item.title)
        for group in _GALLERY_GROUPS
        for item in group.items
    }

    assert len(previews) == 35
    for svg in previews.values():
        assert 'viewBox="0 0 320 232"' in svg
        assert '<rect x="52" y="62" width="216" height="108" rx="12"' in svg
        assert "var(--gallery-preview-surface)" in svg
        assert "<style>" not in svg
        assert 'class="preview-' in svg
        assert "effect2_dropShadow" in svg

    rendered = str(chart_gallery_grid())
    assert rendered.count("#xy-chart-gallery .preview-fill { fill:") == 1
    assert rendered.count("#xy-chart-gallery .preview-line {") == 1

    assert "M109 142.5C81.5 142.5 67.5 83 42 83V172H284.5V100H276" in previews["Area"]
    assert "M42 83C67.5 83 81.5 142.5 109 142.5C136.5 142.5 140.5 103 160 103" in previews["Area"]
    assert previews["Scatter"].count("<circle ") == 14
    for scatter_tone in (
        "preview-scatter-low",
        "preview-scatter-mid",
        "preview-scatter-high",
    ):
        assert scatter_tone in previews["Scatter"]
        assert rendered.count(f"#xy-chart-gallery .{scatter_tone} {{") == 1


def test_polar_gallery_previews_share_a_safe_visual_frame() -> None:
    """Keep every polar preview centered, equally scaled, and clear of its clip."""
    polar_previews = {
        title: _gallery_preview_svg(title)
        for title in ("Polar", "Radar", "Radial Bar", "Pie & Donut", "Wind Rose")
    }

    for svg in polar_previews.values():
        assert '<g class="preview-polar-art" data-preview-radius="48">' in svg

    assert "M160 68L205.7 101.2L188.2 154.8" in polar_previews["Radar"]
    assert 'r="55"' not in polar_previews["Radial Bar"]
    assert "M164.8 61.2" not in polar_previews["Radial Bar"]
    assert "M144.5 174" not in polar_previews["Radial Bar"]
    assert (
        '<g class="preview-radial-bars" data-bar-count="12" data-inner-radius="13">'
        in polar_previews["Radial Bar"]
    )
    assert polar_previews["Radial Bar"].count("A13 13 0 0 0") == 12
    assert "M160 116L" not in polar_previews["Radial Bar"]
    assert "preview-fill-soft" not in polar_previews["Radial Bar"]

    pie = polar_previews["Pie & Donut"]
    assert '<g class="preview-donut-segments" data-segment-count="4" data-inner-radius="27">' in pie
    assert pie.count('class="preview-fill-strong"') == 2
    assert 'class="preview-fill-soft"' in pie
    assert 'class="preview-fill"' in pie
    assert "M146 112H174M151 122H169" in pie

    wind_rose = polar_previews["Wind Rose"]
    assert (
        '<g class="preview-wind-bands" data-direction-count="8" data-band-count="3">' in wind_rose
    )
    assert wind_rose.count("M160 116L") == 8
    assert "M160 68V164M112 116H208" in wind_rose
    assert wind_rose.count("<path ") == 4
    for band, fill_class in (
        ("low", "preview-fill"),
        ("medium", "preview-fill-soft"),
        ("high", "preview-fill-strong"),
    ):
        assert f'class="{fill_class}" data-speed-band="{band}"' in wind_rose


def test_chart_gallery_cards_link_to_family_pages_with_live_demo_anchors() -> None:
    """Anchor only chart types with dedicated live sections."""
    pages = {page.route: page for page in discover_docs(DOCS_CONFIG)}
    rendered = str(chart_gallery_grid())
    anchored_items: list[tuple[str, str]] = []

    for group in _GALLERY_GROUPS:
        if group.route is not None:
            assert "#" not in group.route
            assert group.route in pages

        for item in group.items:
            route = item.route or group.route
            assert route is not None
            assert "#" not in route
            assert route in pages
            destination = f"{route}#{item.fragment}" if item.fragment else route
            assert f'to:"{destination}"' in rendered
            if item.fragment:
                anchored_items.append((item.title, destination))

    assert anchored_items == [
        ("Step + Stairs", "/charts/area-chart/#step-and-stairs"),
        ("Threshold", "/components/annotations/#threshold"),
        ("Horizontal Line", "/components/annotations/#horizontal-line"),
        ("Bands", "/components/annotations/#bands"),
        ("Arrow", "/components/annotations/#arrow"),
        ("Label", "/components/annotations/#label"),
        ("Text", "/components/annotations/#text"),
        ("Facet Chart", "/components/facets-and-layers/#facet-chart"),
    ]

    line_and_area = pages["/charts/area-chart/"]
    assert "### Step and Stairs" in line_and_area.content
    assert "def step_and_stairs_demo():" in line_and_area.content
    assert "xy.step(" in line_and_area.content
    assert "xy.stairs(" in line_and_area.content
    assert 'id:"step-and-stairs"' in str(render_xy_markdown_page(line_and_area))

    # Every chart type now has its own dedicated, SEO-focused page carrying its
    # mark's live demo.
    standalone_chart_marks = {
        "/charts/line-chart/": "xy.line(",
        "/charts/polar-chart/": "xy.polar_chart(",
        "/charts/radar-chart/": "xy.radar_chart(",
        "/charts/radial-bar-chart/": "xy.polar_bar_chart(",
        "/charts/pie-chart/": "xy.polar_bar_chart(",
        "/charts/wind-rose/": "xy.wind_rose(",
        "/charts/bar-chart/": "xy.bar(",
        "/charts/histogram/": "xy.histogram(",
        "/charts/ecdf/": "xy.ecdf(",
        "/charts/box-plot/": "xy.box(",
        "/charts/violin-plot/": "xy.violin(",
        "/charts/heatmap/": "xy.heatmap(",
        "/charts/hexbin/": "xy.hexbin(",
        "/charts/contour-plot/": "xy.contour(",
        "/charts/stem-plot/": "xy.stem(",
        "/charts/segments/": "xy.segments(",
        "/charts/sankey/": "xy.sankey_chart(",
        "/components/triangle-mesh/": "xy.triangle_mesh(",
    }
    for route, mark in standalone_chart_marks.items():
        assert route in pages, route
        assert mark in pages[route].content, route
    assert "xy.pie_chart(" in pages["/charts/pie-chart/"].content

    # Bar and column share one page: the bar page carries the column chart as a
    # subsidiary section.
    assert "xy.column(" in pages["/charts/bar-chart/"].content
    assert "/charts/column-chart/" not in pages

    dedicated_pages = {
        "/components/annotations/": {
            "threshold": ("def threshold_demo():", "xy.threshold("),
            "horizontal-line": ("def horizontal_line_demo():", "xy.hline("),
            "bands": ("def bands_demo():", "xy.x_band("),
            "arrow": ("def arrow_demo():", "xy.arrow("),
            "label": ("def label_demo():", "xy.label("),
            "text": ("def text_demo():", "xy.text("),
        },
        "/components/facets-and-layers/": {
            "facet-chart": ("def facet_chart_demo():", "xy.facet_chart("),
        },
    }
    for route, examples in dedicated_pages.items():
        page = pages[route]
        rendered_page = str(render_xy_markdown_page(page))
        for fragment, requirements in examples.items():
            assert f'id:"{fragment}"' in rendered_page
            assert all(requirement in page.content for requirement in requirements)


def test_chart_gallery_combines_only_the_requested_related_tiles() -> None:
    """Keep requested combined tiles and section ordering explicit."""
    titles = {item.title for group in _GALLERY_GROUPS for item in group.items}
    section_titles = [group.title for group in _GALLERY_GROUPS]

    assert len(titles) == 35
    assert section_titles[:3] == [
        "Line and Area",
        "Distributions",
        "Bar, Column, and Scatter",
    ]
    combined_group = _GALLERY_GROUPS[2]
    assert combined_group.route is None
    assert [(item.title, item.route) for item in combined_group.items] == [
        ("Bar + Column", "/charts/bar-chart/"),
        ("Scatter", "/charts/scatter/"),
    ]
    polar_group = _GALLERY_GROUPS[5]
    assert polar_group.title == "Polar Charts"
    assert polar_group.route == "/charts/polar-chart/"
    assert [(item.title, item.route) for item in polar_group.items] == [
        ("Polar", None),
        ("Radar", "/charts/radar-chart/"),
        ("Radial Bar", "/charts/radial-bar-chart/"),
        ("Pie & Donut", "/charts/pie-chart/"),
        ("Wind Rose", "/charts/wind-rose/"),
    ]
    specialized_group = _GALLERY_GROUPS[6]
    assert specialized_group.title == "Specialized"
    assert [(item.title, item.route) for item in specialized_group.items] == [
        ("Stem", "/charts/stem-plot/"),
        ("Segments", "/charts/segments/"),
        ("Sankey", "/charts/sankey/"),
        ("Funnel", "/charts/funnel-chart/"),
        ("Triangle Mesh", "/components/triangle-mesh/"),
    ]
    assert {"Step + Stairs", "Bar + Column"} <= titles
    assert {"Step", "Stairs", "Bar", "Column"}.isdisjoint(titles)
    assert {
        "Box",
        "Violin",
        "Hexbin",
        "Heatmap",
        "Contour",
        "Polar",
        "Radar",
        "Radial Bar",
        "Pie & Donut",
        "Wind Rose",
        "Error Band",
        "Error Bar",
        "Sankey",
        "Facet Chart",
        "Layered Marks",
    } <= titles


def test_polar_guides_track_the_current_coordinate_system_contract() -> None:
    """Keep the public guide aligned with the runtime's polar-specific seams."""
    polar = (DOCS_ROOT / "charts" / "polar-chart.md").read_text(encoding="utf-8")
    radar = (DOCS_ROOT / "charts" / "radar-chart.md").read_text(encoding="utf-8")
    radial_bar = (DOCS_ROOT / "charts" / "radial-bar-chart.md").read_text(encoding="utf-8")
    pie = (DOCS_ROOT / "charts" / "pie-chart.md").read_text(encoding="utf-8")
    wind_rose = (DOCS_ROOT / "charts" / "wind-rose.md").read_text(encoding="utf-8")
    matplotlib = (DOCS_ROOT / "integrations" / "matplotlib.md").read_text(encoding="utf-8")
    annotations = (DOCS_ROOT / "components" / "annotations.md").read_text(encoding="utf-8")
    limitations = (DOCS_ROOT / "api-reference" / "limitations-and-alpha-status.md").read_text(
        encoding="utf-8"
    )

    for fragment in (
        "scales the radial maximum",
        "keeps the radial minimum fixed",
        # Zoom is off by default on polar (polar-axes.md §8), and the guide has
        # to carry both halves of that: why, and the exact way back in. Reset
        # follows the zoom capability, so the page must not promise it either.
        "**Zoom is off by default.**",
        "reset-axis policy is empty",
        # Reset is not unconditionally absent: an authored `reset_axes` grants it
        # with zoom still off, and `default_drag_action` is narrowed under polar.
        "An explicit `reset_axes` also grants reset",
        'accepts only `"auto"` and `"none"` here',
        "xy.interaction_config(zoom=True)",
        "def zoomable_polar_demo():",
        "authored fractional degree",
        "splits a line into visible runs",
        "`line`, `scatter`, and `area` are limited to",
        "Heatmap/contour grids are not rejected",
        "`sector=(start, end)`",
        '`grid_shape="linear"`',
        "`hole` and `origin` are mutually exclusive",
        "def polar_field_demo():",
        "def polar_uncertainty_demo():",
        'plt.subplots(subplot_kw={"projection": "polar"})',
        'ax.set_thetagrids([0, 90, 180, 270], ["N", "E", "S", "W"])',
        "it is not a direct `plt.subplots(polar=True)` argument",
        "radial area against `r=0`",
        "`set_thetamin()`/`set_thetamax()`",
        "`set_rorigin()`/`get_rorigin()`",
        "reactivating an existing",
        "Explicit `padding=(top, right, bottom, left)`",
    ):
        assert fragment in polar or fragment in matplotlib

    for fragment in (
        "def radar_demo():",
        "def radar_outline_demo():",
        "fill=False",
        "the outline inherits",
        "`line_color` is omitted",
        "Supply at least three categories",
        "Column-name strings are not resolved",
        'grid_shape="linear"',
    ):
        assert fragment in radar

    for fragment in (
        "same-length sequence",
        "complete annulus",
        "def radial_bar_demo():",
        "def allocation_overview_demo():",
        "def training_summary_demo():",
        "def cache_tiers_demo():",
        "RADIAL_DATA",
        "ALLOCATION_DATA",
        "TRAINING_METRICS",
        "CACHE_TIERS",
        "chart_examples_layout_marker",
        "`corner_radius`",
        "`stroke_width`",
        "linear-gradient(to top",
        "`padding=(top, right, bottom, left)`",
        "sector=(-120.0, 120.0)",
    ):
        assert fragment in radial_bar
    assert radial_bar.count("~~~python demo exec") == 4
    assert radial_bar.index("## Basic Radial Bar Chart") < radial_bar.index(
        "## Allocation Overview"
    )
    assert radial_bar.count("xy.modebar(show=False),") == 4
    assert 'xy.legend(loc="right")' in radial_bar
    assert '("Direct", 0, 6, "#5b3cc4")' in radial_bar

    for fragment in (
        "def basic_pie_demo():",
        "def market_share_demo():",
        "def progress_rings_demo():",
        "def revenue_mix_demo():",
        "def reliability_score_demo():",
        "MARKET_SERIES",
        "PROGRESS_STATS",
        "REVENUE_SERIES",
        "RELIABILITY_BANDS",
        "xy.polar_bar_chart(",
        "background-colored stroke",
        "corner_radius=12",
        "GAUGE_SPAN = 240.0",
        "browser/static exports remain available",
    ):
        assert fragment in pie
    assert pie.count("~~~python demo exec") == 5
    assert pie.index("## Basic Pie Chart") < pie.index("## Market Share")
    assert "PIE_DATA = [" in pie
    assert 'PURPLE_SHADES = ["#6e56cf"' in pie
    assert 'xy.legend(loc="left")' in pie
    assert pie.count("xy.modebar(show=False),") == 5
    assert "def donut_demo():" not in radial_bar

    for fragment in (
        "def wind_rose_demo():",
        "`sectors=` controls",
        "`speed_bins` is omitted",
        "Directions are compass bearings in degrees",
    ):
        assert fragment in wind_rose

    polar_guides = "\n".join((polar, radar, radial_bar, pie, wind_rose))
    assert "projection surface is scaffolded but not wired end to end" not in polar_guides
    assert "Downsample very large polar datasets" not in polar_guides
    assert "is not yet routed through the polar constructor" not in matplotlib
    assert "customization is not yet preserved" not in polar_guides
    for stale_fragment in (
        "it does not create or clip the chart",
        "`set_thetamin()` and `set_thetamax()` also remain unsupported",
        "Polar heatmaps, contours, error bars, histograms",
        "General categorical θ axes, partial sectors",
        "Partial-sector layouts are not implemented",
        "validated log-r semantics",
        "Each polar trace is limited to 200,000 points",
        # Zoom, and the reset that follows it, are opt-in on polar (polar-axes.md
        # §8). No polar guide may promise either as part of the default contract,
        # and none may claim a non-wind-rose radius is merely layout — an ordinary
        # `polar_chart` carrying measured radii is the documented opt-in case.
        "Wheel and modebar zoom scale the radial maximum",
        "radial wheel zoom, double-click reset",
        "hover, radial-only zoom",
        "its radius is layout rather than data",
    ):
        assert stale_fragment not in polar_guides
        assert stale_fragment not in matplotlib

    annotation_guides = "\n".join((polar, annotations, limitations))
    for fragment in ("`text`", "`label`", "`marker`", "`arrow`", "`callout`"):
        assert fragment in annotation_guides
    assert "Polar rules and bands remain deferred" in annotation_guides
    for stale_fragment in (
        "not yet consistent across the browser",
        "Polar point annotations are not yet consistent",
        "Polar annotation projection is still renderer-specific",
        "Do not rely on point annotations",
        "The annotation exception above",
    ):
        assert stale_fragment not in annotation_guides


def test_annotations_have_one_canonical_guide_and_a_legacy_redirect() -> None:
    """Keep annotation guidance consolidated without breaking the old chart URL."""
    pages = discover_docs(DOCS_CONFIG)
    annotation_pages = [page for page in pages if page.title == "Annotations in Python"]
    chart_section = next(
        leaves for title, _landing_route, _icon, leaves in DOCS_SECTIONS if title == "Chart Gallery"
    )

    assert [page.route for page in annotation_pages] == ["/components/annotations/"]
    assert ("Annotations", "/charts/annotations/") not in chart_section
    assert DOCS_REDIRECTS["/charts/annotations/"] == "/components/annotations/"

    redirect = app._unevaluated_pages["charts/annotations"]
    rendered_meta = "\n".join(str(component) for component in redirect.meta)
    assert redirect.context == {"sitemap": None}
    assert "https://reflex.dev/docs/xy/components/annotations/" in rendered_meta
    assert "0; url=/docs/xy/components/annotations/" in rendered_meta
    assert "Open the combined Annotations guide" in str(redirect.component())


def test_live_preview_route_validator_requires_real_xy_payloads(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Accept a compiled XYChart only when its referenced payload is valid."""
    client_root = tmp_path / "client"
    module_path = tmp_path / "route.jsx"
    payload_path = client_root / "docs" / "xy" / "xy" / "abc123.xyf"
    payload_path.parent.mkdir(parents=True)
    payload_path.write_bytes(b"XYBF\x01\x00payload")
    module_path.write_text(
        'jsx(XYChart,{src:"/docs/xy/xy/abc123.xyf"})',
        encoding="utf-8",
    )
    monkeypatch.setattr(check_html_routes, "CLIENT_ROOT", client_root)

    check_html_routes.validate_live_preview("/charts/scatter/", module_path)


@pytest.mark.parametrize(
    ("module_source", "payload", "error"),
    (
        ('src:"/docs/xy/xy/abc123.xyf"', b"XYBFpayload", "does not compile XYChart"),
        ("jsx(XYChart,{})", None, "has no static XY payload"),
        (
            'jsx(XYChart,{src:"/docs/xy/xy/abc123.xyf"})',
            None,
            "Missing XY payload",
        ),
        (
            'jsx(XYChart,{src:"/docs/xy/xy/abc123.xyf"})',
            b"not-an-xy-payload",
            "Invalid XY payload",
        ),
    ),
)
def test_live_preview_route_validator_rejects_incomplete_builds(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    module_source: str,
    payload: bytes | None,
    error: str,
) -> None:
    """Reject routes missing the component, reference, file, or XY payload header."""
    client_root = tmp_path / "client"
    module_path = tmp_path / "route.jsx"
    module_path.write_text(module_source, encoding="utf-8")
    if payload is not None:
        payload_path = client_root / "docs" / "xy" / "xy" / "abc123.xyf"
        payload_path.parent.mkdir(parents=True)
        payload_path.write_bytes(payload)
    monkeypatch.setattr(check_html_routes, "CLIENT_ROOT", client_root)

    with pytest.raises(RuntimeError, match=error):
        check_html_routes.validate_live_preview("/charts/scatter/", module_path)


def test_inline_svg_gallery_validator_requires_every_styled_preview(tmp_path: Path) -> None:
    """Accept only the complete code-native gallery in the production route."""
    module_path = tmp_path / "route.jsx"
    preview = 'viewBox=\\"0 0 320 232\\"'
    surface = "gallery-preview-surface aspect-[320/232] shadow-large"
    expected = check_html_routes.INLINE_SVG_PREVIEW_COUNT
    module_path.write_text(preview * expected + surface, encoding="utf-8")

    check_html_routes.validate_inline_svg_gallery("/overview/gallery/", module_path)

    module_path.write_text(preview * (expected - 1) + surface, encoding="utf-8")
    with pytest.raises(
        RuntimeError,
        match=rf"{expected - 1} previews, expected {expected}",
    ):
        check_html_routes.validate_inline_svg_gallery("/overview/gallery/", module_path)


@pytest.mark.xfail(
    not EXPORTED_SITEMAP.is_file(),
    reason="Build the XY docs frontend before validating Markdown links.",
    run=False,
)
def test_xy_markdown_docs_links_match_exported_sitemap() -> None:
    """Validate every XY docs link in every Markdown source against the sitemap."""
    valid_routes = _sitemap_routes(EXPORTED_SITEMAP)
    errors: list[str] = []

    for path in _markdown_files(DOCS_ROOT):
        source = path.read_text(encoding="utf-8")
        try:
            document = parse_document(source)
        except Exception as error:
            errors.append(f"{path}: failed to parse Markdown ({error})")
            continue

        cursor = 0
        for link in _walk_blocks(document.blocks):
            target_path = urlparse(link.target).path
            if not (target_path == "/docs/xy" or target_path.startswith("/docs/xy/")):
                continue
            line, cursor = _link_line(source, link.target, cursor)
            location = f"{path}:{line}" if line else str(path)
            route = _normalize_xy_docs_path(link.target)
            if route is None:
                errors.append(f"{location}: wrong docs prefix: {link.target}")
                continue
            if "_" in route:
                errors.append(f"{location}: underscore in route: {link.target}")
            if route not in valid_routes:
                errors.append(f"{location}: missing route: {link.target}")

    assert not errors, "\n".join(errors)


def test_xy_sitemap_path_normalization_requires_the_exact_frontend_path() -> None:
    """Normalize canonical locations without accepting sibling docs sites."""
    assert _normalize_xy_docs_path("https://reflex.dev/docs/xy/") == "/"
    assert (
        _normalize_xy_docs_path("https://reflex.dev/docs/xy/charts/scatter/?source=test#example")
        == "/charts/scatter"
    )
    assert _normalize_xy_docs_path("https://reflex.dev/docs/charts/") is None


def test_xy_link_walker_resolves_references_and_ignores_code_fences() -> None:
    """Match rendered Markdown semantics instead of scanning source text."""
    document = parse_document(
        "See [charts][charts].\n\n"
        "[charts]: /docs/xy/charts/\n\n"
        "```markdown\n[not a link](/docs/xy/missing/)\n```\n"
    )

    assert [link.target for link in _walk_blocks(document.blocks)] == ["/docs/xy/charts/"]


def test_xy_sidebar_reuses_memoized_official_navigation_rows() -> None:
    """Render quick tabs plus labeled groups of accordions and direct links."""
    component = xy_docs_sidebar_comp(url="/core-concepts/axes-and-scales/")
    assert isinstance(component, MemoComponent)

    instance = str(xy_docs_sidebar("/core-concepts/axes-and-scales/"))
    rendered = str(xy_docs_sidebar_comp._definition.component)
    grouped_sections = tuple(
        section
        for _group_title, _group_route, sections in SIDEBAR_SECTION_GROUPS
        for section in sections
    )

    assert "/core-concepts/axes-and-scales/" in instance
    assert re.findall(
        r'jsx\(RadixThemesText,\{as:"p",className:"m-0 text-sm font-\[525\]"\},"([^"]+)"\)',
        rendered,
    ) == [
        row_title
        for title, landing_route, _icon, leaves in grouped_sections
        for row_title in (
            (*(leaf_title for leaf_title, _leaf_route in leaves),)
            if title == "Integrations"
            and not any(leaf_route == landing_route for _leaf_title, leaf_route in leaves)
            else (
                (leaf_title for leaf_title, _leaf_route in leaves)
                if title == "Integrations"
                else (title,)
            )
        )
    ]
    expected_leaf_count = sum(
        len(leaves) + int(not any(route == landing_route for _title, route in leaves))
        for title, landing_route, _icon, leaves in grouped_sections
        if title != "Integrations" and leaves
    )
    accordion_count = sum(
        title != "Integrations" and bool(leaves)
        for title, _landing_route, _icon, leaves in grouped_sections
    )
    direct_link_count = len(INTEGRATION_LINK_ICONS) + 1
    assert rendered.count('jsx("details"') == accordion_count
    assert rendered.count('jsx("summary"') == accordion_count
    assert rendered.count("group/details") == accordion_count
    assert rendered.count("guideMarginClass") == expected_leaf_count
    assert (
        rendered.count(
            "absolute left-0 top-1/2 -z-10 h-8 w-full -translate-y-1/2 rounded-lg bg-secondary-3"
        )
        == direct_link_count
    )
    assert (
        rendered.count(
            "ml-[2.5rem] flex h-8 w-[calc(100%-2.5rem)] items-center "
            "justify-start text-secondary-11 transition-colors "
            "group-hover:text-primary-10 dark:group-hover:text-primary-9 "
            "xl:max-w-[14rem]"
        )
        == direct_link_count
    )
    assert rendered.count("group relative block h-8 w-full no-underline") == direct_link_count
    assert len(grouped_sections) == len(DOCS_SECTIONS) + 5
    sidebar_section_titles = [section[0] for section in grouped_sections]
    assert set(sidebar_section_titles) == {
        *(section[0] for section in DOCS_SECTIONS if section[0] != "Chart Gallery"),
        "Core Charts",
        "Distributions",
        "Density & Fields",
        "Specialized",
        "Polar Charts",
    }
    assert sidebar_section_titles.count("Overview") == 2
    assert "Chart Gallery" not in sidebar_section_titles
    learning_sections = SIDEBAR_SECTION_GROUPS[0][2]
    chart_sections = SIDEBAR_SECTION_GROUPS[1][2]
    other_sections = SIDEBAR_SECTION_GROUPS[2][2]
    assert [section[0] for section in learning_sections] == [
        "Overview",
        "Core Concepts",
        "Styling",
        "Advanced",
    ]
    assert chart_sections == (
        CHART_GALLERY_SIDEBAR_LINK,
        *CHART_FAMILY_SIDEBAR_SECTIONS,
        POLAR_SIDEBAR_SECTION,
        DOCS_SECTIONS[4],
    )
    assert CHART_GALLERY_SIDEBAR_LINK == (
        "Overview",
        "/overview/gallery/",
        "chart-column",
        (),
    )
    assert [section[0] for section in CHART_FAMILY_SIDEBAR_SECTIONS] == [
        "Core Charts",
        "Distributions",
        "Density & Fields",
        "Specialized",
    ]
    expected_non_polar_routes = tuple(
        route
        for _title, route in DOCS_SECTIONS[3][3]
        if route not in {polar_route for _polar_title, polar_route in POLAR_DOCS_ROUTES}
    )
    grouped_non_polar_routes = tuple(
        route
        for _title, _landing_route, _icon, leaves in CHART_FAMILY_SIDEBAR_SECTIONS
        for _leaf_title, route in leaves
    )
    assert grouped_non_polar_routes == expected_non_polar_routes
    assert len(grouped_non_polar_routes) == len(set(grouped_non_polar_routes))
    assert POLAR_DOCS_ROUTES == (
        ("Overview", POLAR_DOCS_ROUTE),
        ("Radar", RADAR_DOCS_ROUTE),
        ("Radial Bar", RADIAL_BAR_DOCS_ROUTE),
        ("Pie & Donut", PIE_DOCS_ROUTE),
        ("Wind Rose", WIND_ROSE_DOCS_ROUTE),
    )
    assert POLAR_SIDEBAR_SECTION == (
        "Polar Charts",
        POLAR_DOCS_ROUTE,
        "radar",
        POLAR_DOCS_ROUTES,
    )
    for _title, route in POLAR_DOCS_ROUTES:
        assert f'href:"{route}"' in rendered
    assert "Advanced" not in {section[0] for section in other_sections}
    for group_title in ("Learning", "Charts", "Other"):
        assert group_title in rendered
    for category, route in (
        ("Learn", "/"),
        ("Gallery", "/overview/gallery/"),
        ("API Reference", "/api-reference/"),
    ):
        assert f'aria-label":"Navigate to {category}"' in rendered
        assert f'to:"{route}"' in rendered
    assert 'aria-label":"Navigate to Charts"' not in rendered
    assert 'aria-label":"Navigate to Components"' not in rendered
    assert "Axes and Scales" in rendered
    for route in (
        "/integrations/reflex/",
        "/integrations/notebooks/",
        "/integrations/matplotlib/",
    ):
        assert f'to:"{route}"' in rendered
    for icon in (
        "LucideAtom",
        "LucideNotebookTabs",
        "LucideChartNoAxesCombined",
    ):
        assert icon in rendered
    assert "LucidePlug" not in rendered
    assert rendered.count('"aria-current":((') == direct_link_count
    assert ">XY<" not in rendered


@pytest.mark.parametrize(
    ("route", "expected_open_group"),
    (
        ("/charts/scatter/", "Core Charts"),
        ("/charts/ecdf/", "Distributions"),
        ("/charts/contour-plot/", "Density & Fields"),
        ("/charts/segments/", "Specialized"),
        ("/charts/radial-bar-chart/", "Polar Charts"),
        ("/charts/pie-chart/", "Polar Charts"),
        ("/overview/gallery/", None),
    ),
)
def test_xy_sidebar_opens_only_the_current_chart_family(
    route: str,
    expected_open_group: str | None,
) -> None:
    """Open one exact chart family without expanding unrelated accordions."""
    chart_families = (*CHART_FAMILY_SIDEBAR_SECTIONS, POLAR_SIDEBAR_SECTION)
    open_groups = [
        title
        for title, landing_route, icon, leaves in chart_families
        if "open:true" in str(_section_items(title, landing_route, icon, leaves, route)[0])
    ]

    assert open_groups == ([] if expected_open_group is None else [expected_open_group])
    gallery_link = str(_section_items(*CHART_GALLERY_SIDEBAR_LINK, route)[0])
    assert 'jsx("details"' not in gallery_link
    assert ('"aria-current":(true ? "page"' in gallery_link) == (
        route == CHART_GALLERY_SIDEBAR_LINK[1]
    )


def test_xy_navbar_uses_xy_links_github_and_the_official_drawer() -> None:
    """Keep the XY navbar focused while retaining its mobile drawer trigger."""
    component = xy_docs_navbar()
    assert isinstance(component, MemoComponent)

    rendered = str(xy_docs_navbar._definition.component)

    assert 'href:"/"' in rendered
    assert '"aria-label":"Reflex XY"' in rendered
    assert "M29 16H32V10H39V7H32V4H39V1H29V16" in rendered
    assert 'href:"/docs/xy/"' in rendered
    assert 'href:"/docs/xy/integrations/reflex/"' in rendered
    assert "Overview" in rendered
    assert "Reflex Integration" in rendered
    assert 'variant:"ghost"},"Build with AI"' not in rendered
    assert 'variant:"ghost"},"Framework"' not in rendered
    assert 'variant:"ghost"},"Cloud"' not in rendered
    assert 'variant:"ghost"},"XY"' not in rendered
    assert "Forum" not in rendered
    assert "Discord Community" not in rendered
    assert 'variant:"primary"' in rendered
    assert "View XY on GitHub -" not in rendered
    assert rendered.count("View XY on GitHub") == 2
    assert 'target:"_blank"' in rendered
    assert 'rel:"noopener noreferrer"' in rendered
    assert "Open sidebar" in rendered
    assert "Menu01Icon" in rendered
    assert "Cancel01Icon" in rendered
    assert "Mobile documentation navigation" not in rendered
    assert "<details" not in rendered
    assert "<summary" not in rendered
    assert XY_REPOSITORY_URL in rendered
    assert "XY's initial launch is here" in rendered
    assert "Get started" in rendered
    assert "Reserve your spot" not in rendered
    assert "https://luma.com/a1ty77bt" not in rendered
    assert "Reflex Agent Toolkit is launching" not in rendered


def test_xy_breadcrumb_opens_the_official_docs_sidebar_drawer() -> None:
    """Reuse the complete memoized sidebar in the mobile page-header drawer."""
    page = next(page for page in discover_docs(DOCS_CONFIG) if page.route == "/charts/scatter/")

    rendered = str(xy_docs_breadcrumb(page, xy_docs_sidebar(page.route)))

    assert "Drawer.Root" in rendered
    assert "Drawer.Trigger" in rendered
    assert "Chart Gallery" in rendered
    assert "Scatter" in rendered
    assert "/overview/gallery/" in rendered
    assert "/charts/scatter/" in rendered
    assert "ArrowDown01Icon" in rendered


def test_xy_breadcrumb_shortens_the_modebar_page_label() -> None:
    """Keep the longest component route from overflowing the mobile header."""
    page = next(
        page
        for page in discover_docs(DOCS_CONFIG)
        if page.route == "/components/modebars-and-interaction-controls/"
    )

    rendered = str(xy_docs_breadcrumb(page, xy_docs_sidebar(page.route)))

    assert "Modebars & Controls" in rendered
    assert "Modebars And Interaction Controls" not in rendered


@pytest.mark.parametrize(
    ("route", "expected"),
    (
        ("/core-concepts/", (("Composition Model", "/core-concepts/"),)),
        (
            "/core-concepts/data/",
            (
                ("Core Concepts", "/core-concepts/"),
                ("Data and Columns", "/core-concepts/data/"),
            ),
        ),
        (
            "/core-concepts/axes-and-scales/",
            (
                ("Core Concepts", "/core-concepts/"),
                ("Axes and Scales", "/core-concepts/axes-and-scales/"),
            ),
        ),
        (
            "/core-concepts/interactions/",
            (
                ("Core Concepts", "/core-concepts/"),
                ("Interactions and Selections", "/core-concepts/interactions/"),
            ),
        ),
        (
            "/core-concepts/large-data-and-performance/",
            (
                ("Core Concepts", "/core-concepts/"),
                (
                    "Large Data and Performance",
                    "/core-concepts/large-data-and-performance/",
                ),
            ),
        ),
        (
            "/core-concepts/configuration/",
            (
                ("Core Concepts", "/core-concepts/"),
                ("Configuration", "/core-concepts/configuration/"),
            ),
        ),
    ),
)
def test_core_concept_breadcrumbs_use_page_titles(
    route: str,
    expected: tuple[tuple[str, str], ...],
) -> None:
    """Use frontmatter titles instead of lossy slug title-casing."""
    page = next(page for page in discover_docs(DOCS_CONFIG) if page.route == route)

    assert _breadcrumb_parts(page) == expected


def test_xy_footer_is_project_specific_and_keeps_source_aware_links() -> None:
    """Target XY support and source pages without parent-product dead ends."""
    page = next(page for page in discover_docs(DOCS_CONFIG) if page.route == "/overview/gallery/")

    rendered = str(xy_docs_footer(page))

    assert "https://github.com/reflex-dev/xy/issues/new" in rendered
    assert "Issue%20with%20reflex.dev/docs/xy/overview/gallery/" in rendered
    assert "Path%3A%20/docs/xy/overview/gallery/%0A%0A" in rendered
    assert "https://github.com/reflex-dev/xy/blob/main/docs/overview/gallery.md" in rendered
    assert 'to:"/guides/getting-help/"' in rendered
    assert 'to:"/guides/deployment-recipes/"' in rendered
    assert 'to:"/docs/xy/' not in rendered
    assert "SECURITY.md" in rendered
    assert "Book a Demo" not in rendered
    assert "reflex.dev/docs/getting-started" not in rendered


def test_every_docs_route_has_canonical_and_social_metadata() -> None:
    """Publish branded canonical and social metadata for every route."""
    assert len(_DOCS_ROUTES) + len(DOCS_REDIRECTS) == len(app._unevaluated_pages)
    assert PUBLIC_XY_VERSION == "0.0.1"
    assert (
        (DOCS_APP_ROOT / "assets/xy-social-card.png").read_bytes().startswith(b"\x89PNG\r\n\x1a\n")
    )

    for route in _DOCS_ROUTES:
        route_key = route.path.strip("/") or "index"
        page = app._unevaluated_pages[route_key]
        canonical = f"{PUBLIC_DOCS_URL}{route.path}"
        rendered_meta = "\n".join(str(component) for component in page.meta)

        assert page.context == {"sitemap": {"loc": canonical}}
        assert page.image == SOCIAL_IMAGE_URL
        assert f'href:"{canonical}",rel:"canonical"' in rendered_meta
        assert 'property:"og:title"' in rendered_meta
        assert f'content:"{canonical}",property:"og:url"' in rendered_meta
        assert 'content:"summary_large_image",name:"twitter:card"' in rendered_meta
        assert f'content:"{SOCIAL_IMAGE_URL}",name:"twitter:image"' in rendered_meta


def test_component_api_uses_generated_shared_tables() -> None:
    """Render every public factory group through the shared API table."""
    groups = (
        (MARKS, marks_api),
        (AXES_AND_ANNOTATIONS, axes_and_annotations_api),
        (CHROME_AND_BEHAVIOR, chrome_and_behavior_api),
    )

    for functions, render in groups:
        rendered = str(render())
        for function in functions:
            assert f"xy.{function.__name__}" in rendered
        assert "Props" in rendered
        assert "Description" in rendered

    containers = str(chart_containers_api())
    assert "Shared chart props" in containers
    assert "title" in containers
    assert "link_group" in containers
    assert "kind" not in containers

    factories = str(chart_factories_api())
    for group_name, group_factories in CHART_FACTORY_GROUPS:
        assert group_name in factories
        for factory in group_factories:
            assert f"xy.{factory.__name__}" in factories
    assert "Props" in factories
    assert "Description" in factories


def test_component_guides_append_frontmatter_driven_api_tables() -> None:
    """Mirror Reflex library docs by generating component APIs at page end."""
    expected = {
        "/components/marks/": tuple(f"xy.{function.__name__}" for function in MARKS),
        "/components/axes/": ("xy.x_axis", "xy.y_axis"),
        "/components/legends/": ("xy.legend",),
        "/components/tooltips/": ("xy.tooltip",),
        "/components/colorbars/": ("xy.colorbar",),
        "/components/modebars-and-interaction-controls/": (
            "xy.modebar",
            "xy.interaction_config",
        ),
        "/components/annotations/": (
            "xy.vline",
            "xy.hline",
            "xy.x_band",
            "xy.y_band",
            "xy.threshold",
            "xy.threshold_zone",
            "xy.text",
            "xy.label",
            "xy.marker",
            "xy.arrow",
            "xy.callout",
        ),
    }
    pages = {page.route: page for page in discover_docs(DOCS_CONFIG)}

    for route, component_paths in expected.items():
        page = pages[route]
        assert component_api_paths(page.metadata) == component_paths
        assert (
            tuple(
                f"xy.{component.__name__}" for component in component_api_callables(component_paths)
            )
            == component_paths
        )
        assert page_with_api_reference_toc(page).content.endswith(f"\n\n## {API_REFERENCE_HEADING}")

    overview = pages["/components/"]
    assert component_api_paths(overview.metadata) == ()
    assert page_with_api_reference_toc(overview) is overview

    axes_rendered = str(render_xy_markdown_page(pages["/components/axes/"]))
    api_reference_index = axes_rendered.index(API_REFERENCE_HEADING)
    assert api_reference_index < axes_rendered.rindex("xy.x_axis")
    assert axes_rendered.rindex("xy.x_axis") < axes_rendered.rindex("xy.y_axis")
    assert 'id:"api-reference"' in axes_rendered
    assert "Props" in axes_rendered
    assert "Description" in axes_rendered
    assert "Preview" in axes_rendered[:api_reference_index]
    assert "Preview" not in axes_rendered[api_reference_index:]


def test_chart_gallery_pages_append_factory_api_tables() -> None:
    """Generate focused chart-factory tables without changing the gallery index."""
    expected = {
        "/charts/line-chart/": ("xy.line_chart",),
        "/charts/area-chart/": (
            "xy.area_chart",
            "xy.step_chart",
            "xy.stairs_chart",
        ),
        "/charts/scatter/": ("xy.scatter_chart",),
        "/charts/polar-chart/": (
            "xy.polar_chart",
            "xy.theta_axis",
            "xy.r_axis",
        ),
        "/charts/radar-chart/": ("xy.radar_chart",),
        "/charts/radial-bar-chart/": ("xy.polar_bar_chart",),
        "/charts/pie-chart/": ("xy.pie_chart",),
        "/charts/wind-rose/": ("xy.wind_rose",),
        "/charts/bar-chart/": ("xy.bar_chart", "xy.column_chart"),
        "/charts/histogram/": ("xy.histogram_chart",),
        "/charts/ecdf/": ("xy.ecdf_chart",),
        "/charts/box-plot/": ("xy.box_chart",),
        "/charts/violin-plot/": ("xy.violin_chart",),
        "/charts/heatmap/": ("xy.heatmap_chart",),
        "/charts/hexbin/": ("xy.hexbin_chart",),
        "/charts/contour-plot/": ("xy.contour_chart",),
        "/charts/uncertainty/": ("xy.error_band_chart", "xy.errorbar_chart"),
        "/charts/stem-plot/": ("xy.stem_chart",),
        "/charts/segments/": ("xy.segments_chart",),
        "/charts/sankey/": ("xy.sankey_chart",),
        "/components/triangle-mesh/": ("xy.triangle_mesh_chart",),
        "/components/facets-and-layers/": ("xy.chart", "xy.facet_chart"),
    }
    pages = {page.route: page for page in discover_docs(DOCS_CONFIG)}

    for route, factory_paths in expected.items():
        page = pages[route]
        assert component_api_paths(page.metadata) == factory_paths
        assert page_with_api_reference_toc(page).content.endswith(f"\n\n## {API_REFERENCE_HEADING}")

    gallery = pages["/overview/gallery/"]
    assert component_api_paths(gallery.metadata) == ()
    assert page_with_api_reference_toc(gallery) is gallery

    scatter_rendered = str(render_xy_markdown_page(pages["/charts/scatter/"]))
    assert scatter_rendered.index(API_REFERENCE_HEADING) < scatter_rendered.rindex(
        "xy.scatter_chart"
    )
    assert "Props" in scatter_rendered
    scatter_api_index = scatter_rendered.index(API_REFERENCE_HEADING)
    assert "Preview" in scatter_rendered[:scatter_api_index]
    assert "Preview" not in scatter_rendered[scatter_api_index:]

    # The FAQ renders below the generated API section on both surfaces.
    scatter_faq_question = "How do I make a scatter plot in Python?"
    assert scatter_rendered.index(API_REFERENCE_HEADING) < scatter_rendered.index(
        scatter_faq_question
    )
    scatter_markdown = append_component_api_markdown(
        pages["/charts/scatter/"].content, pages["/charts/scatter/"].metadata
    )
    assert scatter_markdown.index(f"## {API_REFERENCE_HEADING}") < scatter_markdown.index("## FAQ")


def test_chart_factory_api_expands_forwarded_chart_props() -> None:
    """Document the real Chart kwargs instead of a generic ``**props`` row."""
    shared_names = tuple(
        parameter.name
        for parameter in inspect.signature(xy.Chart).parameters.values()
        if parameter.name not in {"kind", "children"}
    )

    line_reference = component_api_references(("xy.line_chart",))[0]
    line_names = tuple(parameter.name for parameter in line_reference.parameters)
    assert line_names == ("*children", *shared_names)
    assert "**props" not in line_names
    assert line_reference.parameters[0].description == (
        "Marks, axes, annotations, and chart chrome."
    )
    assert line_reference.parameters[line_names.index("width")].description == (
        'Chart width in pixels or a CSS size such as ``"100%"``.'
    )

    facet_reference = component_api_references(("xy.facet_chart",))[0]
    facet_names = tuple(parameter.name for parameter in facet_reference.parameters)
    assert facet_names[:8] == (
        "*children",
        "by",
        "cols",
        "share_x",
        "share_y",
        "link",
        "link_select",
        "gap",
    )
    assert facet_names[8:] == shared_names
    assert "**props" not in facet_names
    assert len(facet_names) == len(set(facet_names))

    markdown = component_api_markdown((line_reference,))
    assert "| `title` |" in markdown
    assert "| `width` |" in markdown
    assert "| `zoom_limits` |" in markdown
    assert "| `link_axes` |" in markdown


def test_polar_axis_api_expands_forwarded_axis_props() -> None:
    """Show the underlying x/y options instead of an opaque ``**kwargs`` row."""
    theta_reference, radial_reference = component_api_references(("xy.theta_axis", "xy.r_axis"))
    theta_names = tuple(parameter.name for parameter in theta_reference.parameters)
    radial_names = tuple(parameter.name for parameter in radial_reference.parameters)
    theta_refused = frozenset({*_POLAR_INERT_AXIS_KEYWORDS, "reverse"})
    radial_refused = frozenset(_POLAR_INERT_AXIS_KEYWORDS)
    x_axis_names = tuple(
        name for name in inspect.signature(xy.x_axis).parameters if name not in theta_refused
    )
    y_axis_names = tuple(
        name for name in inspect.signature(xy.y_axis).parameters if name not in radial_refused
    )

    assert theta_names[:5] == ("unit", "zero", "direction", "sector", "grid_shape")
    assert theta_names[5:] == x_axis_names
    assert radial_names[:2] == ("hole", "origin")
    assert radial_names[2:] == y_axis_names
    assert "**kwargs" not in {*theta_names, *radial_names}
    # The polar axes refuse these outright, so the table must not offer them.
    assert not theta_refused & set(theta_names)
    assert not radial_refused & set(radial_names)
    assert "reverse" in radial_names
    assert theta_reference.parameters[theta_names.index("tick_values")].description
    assert theta_reference.parameters[theta_names.index("sector")].description
    assert radial_reference.parameters[radial_names.index("domain")].description
    assert radial_reference.parameters[radial_names.index("hole")].description


def test_other_api_owned_pages_append_focused_tables() -> None:
    """Document styling factories and the public Reflex adapter surface in place."""
    expected = {
        "/styling/animations/": ("xy.animation", "xy.spring"),
        "/styling/themes-and-tokens/": ("xy.theme",),
        "/integrations/reflex/": (
            "reflex_xy.chart",
            "reflex_xy.data",
            "reflex_xy.figure",
            "reflex_xy.scatter_chart",
            "reflex_xy.line_chart",
            "reflex_xy.inline",
            "reflex_xy.append",
        ),
    }
    pages = {page.route: page for page in discover_docs(DOCS_CONFIG)}

    for route, api_paths in expected.items():
        page = pages[route]
        assert component_api_paths(page.metadata) == api_paths
        assert page_with_api_reference_toc(page).content.endswith(f"\n\n## {API_REFERENCE_HEADING}")

    adapter_api = str(
        component_page_api(component_api_references(expected["/integrations/reflex/"]))
    )
    for api_path in expected["/integrations/reflex/"]:
        assert api_path in adapter_api
    assert "Props" in adapter_api
    assert "Preview" not in adapter_api


def test_reflex_integration_renders_the_state_backed_data_example() -> None:
    """Keep the primary @rxy.data example visible as a live chart demo."""
    page = next(
        page for page in discover_docs(DOCS_CONFIG) if page.route == "/integrations/reflex/"
    )
    live_blocks = [
        block
        for block in parse_document(page.content).blocks
        if isinstance(block, CodeBlock) and {"demo", "exec"} <= set(block.flags)
    ]

    assert len(live_blocks) == 2
    state_backed = next(block for block in live_blocks if "@rxy.data" in block.content)
    assert "rxy.scatter_chart(" in state_backed.content
    state_rendered = str(
        XyDocsMarkdownTransformer(
            virtual_filepath=str(page.source_path.resolve()),
            filename=str(page.source_path),
        ).code_block(state_backed)
    )
    assert "XYChart" in state_rendered


@pytest.mark.parametrize(
    ("metadata", "exception"),
    (
        ({"components": "xy.line"}, TypeError),
        ({"components": ["rx.line"]}, ValueError),
        ({"components": ["xy.not_a_component"]}, ValueError),
        ({"components": ["reflex_xy.not_a_component"]}, ValueError),
        ({"components": ["xy.line", "xy.line"]}, ValueError),
    ),
)
def test_component_api_frontmatter_rejects_invalid_declarations(
    metadata: dict[str, object],
    exception: type[Exception],
) -> None:
    """Fail docs compilation when component API metadata becomes stale."""
    with pytest.raises(exception):
        component_api_references(component_api_paths(metadata))


def test_component_api_html_and_markdown_share_cached_metadata() -> None:
    """Keep website and agent tables in sync without resolving twice."""
    paths = ("xy.x_axis",)
    references = component_api_references(paths)
    assert component_api_references(paths) is references

    rendered_html = str(component_page_api(references))
    rendered_markdown = component_api_markdown(references)
    assert rendered_markdown.startswith(f"## {API_REFERENCE_HEADING}\n")
    assert "### xy.x_axis" in rendered_markdown
    assert "| Prop | Type | Description |" in rendered_markdown
    for parameter in references[0].parameters:
        assert parameter.name in rendered_html
        assert parameter.name in rendered_markdown
        assert json.dumps(parameter.description)[1:-1] in rendered_html
        assert parameter.description in rendered_markdown


def test_documented_factories_describe_every_parameter() -> None:
    """Keep generated API descriptions sourced from complete docstrings."""
    factories = (
        *MARKS,
        *AXES_AND_ANNOTATIONS,
        *CHROME_AND_BEHAVIOR,
        xy.facet_chart,
    )

    for factory in factories:
        docstring = inspect.getdoc(factory) or ""
        assert "Args:" in docstring, factory.__name__
        for parameter in inspect.signature(factory).parameters.values():
            assert f"{parameter.name}:" in docstring, (
                factory.__name__,
                parameter.name,
            )

    chart_docstring = inspect.getdoc(xy.Chart.__init__) or ""
    assert "Args:" in chart_docstring
    for parameter in inspect.signature(xy.Chart).parameters.values():
        assert f"{parameter.name}:" in chart_docstring, (
            "Chart",
            parameter.name,
        )
