"""
The module contains `DOMNode`, the base class for any object within the Textual Document Object Model,
which includes all Widgets, Screens, and Apps.

"""

from __future__ import annotations

import re
import threading
from functools import lru_cache, partial
from inspect import getfile
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    ClassVar,
    Iterable,
    Sequence,
    Type,
    TypeVar,
    cast,
    overload,
)

import rich.repr
from rich.highlighter import ReprHighlighter
from rich.style import NULL_STYLE as RICH_NULL_STYLE
from rich.style import Style
from rich.text import Text
from rich.tree import Tree

from textual._context import NoActiveAppError, active_message_pump
from textual._node_list import NodeList
from textual._types import WatchCallbackType
from textual.binding import Binding, BindingsMap, BindingType
from textual.cache import LRUCache
from textual.color import BLACK, WHITE, Color
from textual.css._error_tools import friendly_list
from textual.css.constants import VALID_DISPLAY, VALID_VISIBILITY
from textual.css.errors import DeclarationError, StyleValueError
from textual.css.match import match
from textual.css.parse import is_id_selector, parse_declarations, parse_selectors
from textual.css.query import InvalidQueryFormat, NoMatches, TooManyMatches, WrongType
from textual.css.styles import RenderStyles, Styles
from textual.css.tokenize import IDENTIFIER
from textual.css.tokenizer import TokenError
from textual.message_pump import MessagePump
from textual.reactive import Reactive, ReactiveError, _Mutated, _watch
from textual.style import Style as VisualStyle
from textual.timer import Timer
from textual.walk import walk_breadth_first, walk_breadth_search_id, walk_depth_first
from textual.worker_manager import WorkerManager

if TYPE_CHECKING:
    from typing_extensions import Self, TypeAlias
    from _typeshed import SupportsRichComparison

    from rich.console import RenderableType
    from textual.app import App
    from textual.css.query import DOMQuery, QueryType
    from textual.css.types import CSSLocation
    from textual.message import Message
    from textual.screen import Screen
    from textual.widget import Widget
    from textual.worker import Worker, WorkType, ResultType

from typing_extensions import Literal

_re_identifier = re.compile(IDENTIFIER)


WalkMethod: TypeAlias = Literal["depth", "breadth"]
"""Valid walking methods for the [`DOMNode.walk_children` method][textual.dom.DOMNode.walk_children]."""


ReactiveType = TypeVar("ReactiveType")


QueryOneCacheKey: TypeAlias = "tuple[int, str, Type[Widget] | None]"
"""The key used to cache query_one results."""


class BadIdentifier(Exception):
    """Exception raised if you supply a `id` attribute or class name in the wrong format."""


def check_identifiers(description: str, *names: str) -> None:
    """Validate identifier and raise an error if it fails.

    Args:
        description: Description of where identifier is used for error message.
        *names: Identifiers to check.
    """
    match = _re_identifier.fullmatch
    for name in names:
        if match(name) is None:
            raise BadIdentifier(
                f"{name!r} is an invalid {description}; "
                "identifiers must contain only letters, numbers, underscores, or hyphens, and must not begin with a number."
            )


class DOMError(Exception):
    """Base exception class for errors relating to the DOM."""


class NoScreen(DOMError):
    """Raised when the node has no associated screen."""


class _ClassesDescriptor:
    """A descriptor to manage the `classes` property."""

    def __get__(
        self, obj: DOMNode, objtype: type[DOMNode] | None = None
    ) -> frozenset[str]:
        """A frozenset of the current classes on the widget."""
        return frozenset(obj._classes)

    def __set__(self, obj: DOMNode, classes: str | Iterable[str]) -> None:
        """Replaces classes entirely."""
        if isinstance(classes, str):
            class_names = set(classes.split())
        else:
            class_names = set(classes)
        check_identifiers("class name", *class_names)
        obj._classes = class_names
        obj.update_node_styles()


@rich.repr.auto
class DOMNode(MessagePump):
    """The base class for object that can be in the Textual DOM (App and Widget)"""

    DEFAULT_CSS: ClassVar[str] = ""
    """Default TCSS."""

    DEFAULT_CLASSES: ClassVar[str] = ""
    """Default classes argument if not supplied."""

    COMPONENT_CLASSES: ClassVar[set[str]] = set()
    """Virtual DOM nodes, used to expose styles to line API widgets."""

    BINDING_GROUP_TITLE: str | None = None
    """Title of widget used where bindings are displayed (such as in the key panel)."""

    BINDINGS: ClassVar[list[BindingType]] = []
    """A list of key bindings."""

    # Indicates if the CSS should be automatically scoped
    SCOPED_CSS: ClassVar[bool] = True
    """Should default css be limited to the widget type?"""

    HELP: ClassVar[str | None] = None
    """Optional help text shown in help panel (Markdown format)."""

    # True if this node inherits the CSS from the base class.
    _inherit_css: ClassVar[bool] = True

    # True if this node inherits the component classes from the base class.
    _inherit_component_classes: ClassVar[bool] = True

    # True to inherit bindings from base class
    _inherit_bindings: ClassVar[bool] = True

    # List of names of base classes that inherit CSS
    _css_type_names: ClassVar[frozenset[str]] = frozenset()

    # Name of the widget in CSS
    _css_type_name: str = ""

    # Generated list of bindings
    _merged_bindings: ClassVar[BindingsMap | None] = None

    _reactives: ClassVar[dict[str, Reactive]]

    _decorated_handlers: dict[type[Message], list[tuple[Callable, str | None]]]

    # Names of potential computed reactives
    _computes: ClassVar[frozenset[str]]

    _PSEUDO_CLASSES: ClassVar[dict[str, Callable[[App[Any]], bool]]] = {}
    """Pseudo class checks."""

    def __init__(
        self,
        *,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
    ) -> None:
        self._classes: set[str] = set()
        self._name = name
        self._id = None
        if id is not None:
            check_identifiers("id", id)
            self._id = id

        _classes = classes.split() if classes else []
        check_identifiers("class name", *_classes)
        self._classes.update(_classes)

        self._nodes: NodeList = NodeList(self)
        self._css_styles: Styles = Styles(self)
        self._inline_styles: Styles = Styles(self)
        self.styles: RenderStyles = RenderStyles(
            self, self._css_styles, self._inline_styles
        )
        # A mapping of class names to Styles set in COMPONENT_CLASSES
        self._component_styles: dict[str, RenderStyles] = {}

        self._auto_refresh: float | None = None
        self._auto_refresh_timer: Timer | None = None
        self._css_types = {cls.__name__ for cls in self._css_bases(self.__class__)}
        self._bindings = (
            BindingsMap()
            if self._merged_bindings is None
            else self._merged_bindings.copy()
        )
        self._has_hover_style: bool = False
        self._has_focus_within: bool = False
        self._has_order_style: bool = False
        """The node has an ordered dependent pseudo-style (`:odd`, `:even`, `:first-of-type`, `:last-of-type`, `:first-child`, `:last-child`)"""
        self._has_odd_or_even: bool = False
        """The node has the pseudo class `odd` or `even`."""
        self._reactive_connect: (
            dict[str, tuple[MessagePump, Reactive[object] | object]] | None
        ) = None
        self._pruning = False
        self._query_one_cache: LRUCache[QueryOneCacheKey, DOMNode] = LRUCache(1024)
        self._trap_focus = False

        super().__init__()

    def _get_dom_base(self) -> DOMNode:
        """Get the DOM base node (typically self).

        All DOM queries on this node will use the return value as the root node.
        This method allows the App to query the default screen, and not the active screen.

        Returns:
            DOMNode.
        """
        return self

    def set_reactive(
        self, reactive: Reactive[ReactiveType], value: ReactiveType
    ) -> None:
        """Sets a reactive value *without* invoking validators or watchers.

        Example:
            ```python
            self.set_reactive(App.theme, "textual-light")
            ```

        Args:
            reactive: A reactive property (use the class scope syntax, i.e. `MyClass.my_reactive`).
            value: New value of reactive.

        Raises:
            AttributeError: If the first argument is not a reactive.
        """
        name = reactive.name
        if not isinstance(reactive, Reactive):
            raise TypeError("A Reactive class is required; for example: MyApp.theme")
        if name not in self._reactives:
            raise AttributeError(
                f"No reactive called {name!r}; Have you called super().__init__(...) in the {self.__class__.__name__} constructor?"
            )
        setattr(self, f"_reactive_{name}", value)

    def mutate_reactive(self, reactive: Reactive[ReactiveType]) -> None:
        """Force an update to a mutable reactive.

        Example:
            ```python
            self.reactive_name_list.append("Jessica")
            self.mutate_reactive(MyClass.reactive_name_list)
            ```

        Textual will automatically detect when a reactive is set to a new value, but it is unable
        to detect if a value is _mutated_ (such as updating a list, dict, or attribute of an object).
        If you do wish to use a collection or other mutable object in a reactive, then you can call
        this method after your reactive is updated. This will ensure that all the reactive _superpowers_
        work.

        !!! note

            This method will cause watchers to be called, even if the value hasn't changed.

        Args:
            reactive: A reactive property (use the class scope syntax, i.e. `MyClass.my_reactive`).
        """

        internal_name = f"_reactive_{reactive.name}"
        value = getattr(self, internal_name)
        reactive._set(self, value, always=True)

    def data_bind(
        self,
        *reactives: Reactive[Any],
        **bind_vars: Reactive[Any] | object,
    ) -> Self:
        """Bind reactive data so that changes to a reactive automatically change the reactive on another widget.

        Reactives may be given as positional arguments or keyword arguments.
        See the [guide on data binding](/guide/reactivity#data-binding).

        Example:
            ```python
            def compose(self) -> ComposeResult:
                yield WorldClock("Europe/London").data_bind(WorldClockApp.time)
                yield WorldClock("Europe/Paris").data_bind(WorldClockApp.time)
                yield WorldClock("Asia/Tokyo").data_bind(WorldClockApp.time)
            ```

        Raises:
            ReactiveError: If the data wasn't bound.

        Returns:
            Self.
        """
        _rich_traceback_omit = True

        parent = active_message_pump.get()

        if self._reactive_connect is None:
            self._reactive_connect = {}
        bind_vars = {**{reactive.name: reactive for reactive in reactives}, **bind_vars}
        for name, reactive in bind_vars.items():
            if name not in self._reactives:
                raise ReactiveError(
                    f"Unable to bind non-reactive attribute {name!r} on {self}"
                )
            if isinstance(reactive, Reactive) and not isinstance(
                parent, reactive.owner
            ):
                raise ReactiveError(
                    f"Unable to bind data; {reactive.owner.__name__} is not defined on {parent.__class__.__name__}."
                )
            self._reactive_connect[name] = (parent, reactive)
        if self._is_mounted:
            self._initialize_data_bind()
        else:
            self.call_later(self._initialize_data_bind)
        return self

    def _initialize_data_bind(self) -> None:
        """initialize a data binding.

        Args:
            compose_parent: The node doing the binding.
        """
        if not self._reactive_connect:
            return
        for variable_name, (compose_parent, reactive) in self._reactive_connect.items():

            def make_setter(variable_name: str) -> Callable[[object], None]:
                """Make a setter for the given variable name.

                Args:
                    variable_name: Name of variable being set.

                Returns:
                    A callable which takes the value to set.
                """

                def setter(value: object) -> None:
                    """Set bound data."""
                    _rich_traceback_omit = True
                    Reactive._initialize_object(self)
                    # Wrap the value in `_Mutated` so the setter knows to invoke watchers etc.
                    setattr(self, variable_name, _Mutated(value))

                return setter

            assert isinstance(compose_parent, DOMNode)
            setter = make_setter(variable_name)
            if isinstance(reactive, Reactive):
                self.watch(
                    compose_parent,
                    reactive.name,
                    setter,
                    init=True,
                )
            else:
                self.call_later(partial(setter, reactive))
        self._reactive_connect = None

    def compose_add_child(self, widget: Widget) -> None:
        """Add a node to children.

        This is used by the compose process when it adds children.
        There is no need to use it directly, but you may want to override it in a subclass
        if you want children to be attached to a different node.

        Args:
            widget: A Widget to add.
        """
        self._nodes._append(widget)

    @property
    def children(self) -> Sequence["Widget"]:
        """A view on to the children.

        Returns:
            The node's children.
        """
        return self._nodes

    @property
    def displayed_children(self) -> Sequence[Widget]:
        """The displayed children (where `node.display==True`).

        Returns:
            A sequence of widgets.
        """
        return self._nodes.displayed

    @property
    def displayed_and_visible_children(self) -> Sequence[Widget]:
        """The displayed children (where `node.display==True` and `node.visible==True`).

        Returns:
            A sequence of widgets.
        """
        return self._nodes.displayed_and_visible

    @property
    def is_empty(self) -> bool:
        """Are there no displayed children?"""
        return not any(child.display for child in self._nodes)

    def sort_children(
        self,
        *,
        key: Callable[[Widget], SupportsRichComparison] | None = None,
        reverse: bool = False,
    ) -> None:
        """Sort child widgets with an optional key function.

        If `key` is not provided then widgets will be sorted in the order they are constructed.

        Example:
            ```python
            # Sort widgets by name
            screen.sort_children(key=lambda widget: widget.name or "")
            ```

        Args:
            key: A callable which accepts a widget and returns something that can be sorted,
                or `None` to sort without a key function.
            reverse: Sort in descending order.
        """
        self._nodes._sort(key=key, reverse=reverse)
        self.refresh(layout=True)

    @property
    def auto_refresh(self) -> float | None:
        """Number of seconds between automatic refresh, or `None` for no automatic refresh."""
        return self._auto_refresh

    @auto_refresh.setter
    def auto_refresh(self, interval: float | None) -> None:
        if self._auto_refresh_timer is not None:
            self._auto_refresh_timer.stop()
            self._auto_refresh_timer = None
        if interval is not None:
            self._auto_refresh_timer = self.set_interval(
                interval, self.automatic_refresh, name=f"auto refresh {self!r}"
            )
        self._auto_refresh = interval

    @property
    def workers(self) -> WorkerManager:
        """The app's worker manager. Shortcut for `self.app.workers`."""
        return self.app.workers

    def trap_focus(self, trap_focus: bool = True) -> None:
        """Trap the focus.

        When applied to a container, this will limit tab-to-focus to the children of that
        container (once focus is within that container).

        This can be useful for widgets that act like modal dialogs, where you want to restrict
        the user to the controls within the dialog.

        Args:
            trap_focus: `True` to trap focus. `False` to restore default behavior.
        """
        self._trap_focus = trap_focus

    def run_worker(
        self,
        work: WorkType[ResultType],
        name: str | None = "",
        group: str = "default",
        description: str = "",
        exit_on_error: bool = True,
        start: bool = True,
        exclusive: bool = False,
        thread: bool = False,
    ) -> Worker[ResultType]:
        """Run work in a worker.

        A worker runs a function, coroutine, or awaitable, in the *background* as an async task or as a thread.

        Args:
            work: A function, async function, or an awaitable object to run in a worker.
            name: A short string to identify the worker (in logs and debugging).
            group: A short string to identify a group of workers.
            description: A longer string to store longer information on the worker.
            exit_on_error: Exit the app if the worker raises an error. Set to `False` to suppress exceptions.
            start: Start the worker immediately.
            exclusive: Cancel all workers in the same group.
            thread: Mark the worker as a thread worker.

        Returns:
            New Worker instance.
        """

        # If we're running a worker from inside a secondary thread,
        # do so in a thread-safe way.
        if self.app._thread_id != threading.get_ident():
            creator = partial(self.app.call_from_thread, self.workers._new_worker)
        else:
            creator = self.workers._new_worker
        worker: Worker[ResultType] = creator(
            work,
            self,
            name=name,
            group=group,
            description=description,
            exit_on_error=exit_on_error,
            start=start,
            exclusive=exclusive,
            thread=thread,
        )
        return worker

    @property
    def is_modal(self) -> bool:
        """Is the node a modal?"""
        return False

    @property
    def is_on_screen(self) -> bool:
        """Check if the node was displayed in the last screen update."""
        return False

    def automatic_refresh(self) -> None:
        """Perform an automatic refresh.

        This method is called when you set the `auto_refresh` attribute.
        You could implement this method if you want to perform additional work
        during an automatic refresh.

        """
        if self.is_on_screen:
            self.refresh()

    def __init_subclass__(
        cls,
        inherit_css: bool = True,
        inherit_bindings: bool = True,
        inherit_component_classes: bool = True,
    ) -> None:
        super().__init_subclass__()

        reactives = cls._reactives = {}
        for base in reversed(cls.__mro__):
            reactives.update(
                {
                    name: reactive
                    for name, reactive in base.__dict__.items()
                    if isinstance(reactive, Reactive)
                }
            )

        cls._inherit_css = inherit_css
        cls._inherit_bindings = inherit_bindings
        cls._inherit_component_classes = inherit_component_classes
        css_type_names: set[str] = set()
        bases = cls._css_bases(cls)
        cls._css_type_name = bases[0].__name__
        for base in bases:
            css_type_names.add(base.__name__)
        cls._merged_bindings = cls._merge_bindings()
        cls._css_type_names = frozenset(css_type_names)
        cls._computes = frozenset(
            [
                name.lstrip("_")[8:]
                for name in dir(cls)
                if name.startswith(("_compute_", "compute_"))
            ]
        )

    def get_component_styles(self, *names: str) -> RenderStyles:
        """Get a "component" styles object (must be defined in COMPONENT_CLASSES classvar).

        Args:
            names: Names of the components.

        Raises:
            KeyError: If the component class doesn't exist.

        Returns:
            A Styles object.
        """

        styles = RenderStyles(self, Styles(), Styles())

        for name in names:
            if name not in self._component_styles:
                raise KeyError(f"No {name!r} key in COMPONENT_CLASSES")
            component_styles = self._component_styles[name]
            styles.node = component_styles.node
            styles.base.merge(component_styles.base)
            styles.inline.merge(component_styles.inline)
            styles._updates += 1

        return styles

    def _post_mount(self):
        """Called after the object has been mounted."""
        _rich_traceback_omit = True
        Reactive._initialize_object(self)

    def notify_style_update(self) -> None:
        """Called after styles are updated.

        Implement this in a subclass if you want to clear any cached data when the CSS is reloaded.
        """

    @property
    def _node_bases(self) -> Sequence[Type[DOMNode]]:
        """The DOMNode bases classes (including self.__class__)"""
        # Node bases are in reversed order so that the base class is lower priority
        return self._css_bases(self.__class__)

    @classmethod
    @lru_cache(maxsize=None)
    def _css_bases(cls, base: Type[DOMNode]) -> Sequence[Type[DOMNode]]:
        """Get the DOMNode base classes, which inherit CSS.

        Args:
            base: A DOMNode class

        Returns:
            An iterable of DOMNode classes.
        """
        classes: list[type[DOMNode]] = []
        _class = base
        while True:
            classes.append(_class)
            if not _class._inherit_css:
                break
            for _base in _class.__bases__:
                if issubclass(_base, DOMNode):
                    _class = _base
                    break
            else:
                break
        return classes

    @classmethod
    def _merge_bindings(cls) -> BindingsMap:
        """Merge bindings from base classes.

        Returns:
            Merged bindings.
        """
        bindings: list[BindingsMap] = []

        for base in reversed(cls.__mro__):
            if issubclass(base, DOMNode):
                if not base._inherit_bindings:
                    bindings.clear()
                bindings.append(
                    BindingsMap(
                        base.__dict__.get("BINDINGS", []),
                    )
                )

        keys: dict[str, list[Binding]] = {}
        for bindings_ in bindings:
            for key, key_bindings in bindings_.key_to_bindings.items():
                keys[key] = key_bindings

        new_bindings = BindingsMap.from_keys(keys)
        return new_bindings

    def _post_register(self, app: App) -> None:
        """Called when the widget is registered

        Args:
            app: Parent application.
        """

    def __rich_repr__(self) -> rich.repr.Result:
        # Being a bit defensive here to guard against errors when calling repr before initialization
        if hasattr(self, "_name"):
            yield "name", self._name, None
        if hasattr(self, "_id"):
            yield "id", self._id, None
        if hasattr(self, "_classes") and self._classes:
            yield "classes", " ".join(self._classes)

    def _get_default_css(self) -> list[tuple[CSSLocation, str, int, str]]:
        """Gets the CSS for this class and inherited from bases.

        Default CSS is inherited from base classes, unless `inherit_css` is set to
        `False` when subclassing.

        Returns:
            A list of tuples containing (LOCATION, SOURCE, SPECIFICITY, SCOPE) for this
                class and inherited from base classes.
        """

        css_stack: list[tuple[CSSLocation, str, int, str]] = []

        def get_location(base: Type[DOMNode]) -> CSSLocation:
            """Get the original location of this DEFAULT_CSS.

            Args:
                base: The class from which the default css was extracted.

            Returns:
                The filename where the class was defined (if possible) and the class
                    variable the CSS was extracted from.
            """
            try:
                return (getfile(base), f"{base.__name__}.DEFAULT_CSS")
            except (TypeError, OSError):
                return ("", f"{base.__name__}.DEFAULT_CSS")

        for tie_breaker, base in enumerate(self._node_bases):
            css: str = base.__dict__.get("DEFAULT_CSS", "")
            if css:
                scoped: bool = base.__dict__.get("SCOPED_CSS", True)
                css_stack.append(
                    (
                        get_location(base),
                        css,
                        -tie_breaker,
                        base._css_type_name if scoped else "",
                    )
                )
        return css_stack

    @classmethod
    @lru_cache(maxsize=None)
    def _get_component_classes(cls) -> frozenset[str]:
        """Gets the component classes for this class and inherited from bases.

        Component classes are inherited from base classes, unless
        `inherit_component_classes` is set to `False` when subclassing.

        Returns:
            A set with all the component classes available.
        """

        component_classes: set[str] = set()
        for base in cls._css_bases(cls):
            component_classes.update(base.__dict__.get("COMPONENT_CLASSES", set()))
            if not base.__dict__.get("_inherit_component_classes", True):
                break

        return frozenset(component_classes)

    @property
    def parent(self) -> DOMNode | None:
        """The parent node.

        All nodes have parent once added to the DOM, with the exception of the App which is the *root* node.
        """
        return cast("DOMNode | None", self._parent)

    @property
    def screen(self) -> "Screen[object]":
        """The screen containing this node.

        Returns:
            A screen object.

        Raises:
            NoScreen: If this node isn't mounted (and has no screen).
        """
        # Get the node by looking up a chain of parents
        # Note that self.screen may not be the same as self.app.screen
        from textual.screen import Screen

        node: MessagePump | None = self
        try:
            while node is not None and not isinstance(node, Screen):
                node = node._parent
        except AttributeError:
            raise RuntimeError(
                "Widget is missing attributes; have you called the constructor in your widget class?"
            ) from None
        if not isinstance(node, Screen):
            raise NoScreen("node has no screen")
        return node

    @property
    def id(self) -> str | None:
        """The ID of this node, or None if the node has no ID."""
        return self._id

    @id.setter
    def id(self, new_id: str) -> str:
        """Sets the ID (may only be done once).

        Args:
            new_id: ID for this node.

        Raises:
            ValueError: If the ID has already been set.
        """
        check_identifiers("id", new_id)
        self._nodes.updated()
        if self._id is not None:
            raise ValueError(
                f"Node 'id' attribute may not be changed once set (current id={self._id!r})"
            )
        self._id = new_id
        return new_id

    @property
    def name(self) -> str | None:
        """The name of the node."""
        return self._name

    @property
    def css_identifier(self) -> str:
        """A CSS selector that identifies this DOM node."""
        tokens = [self.__class__.__name__]
        if self.id is not None:
            tokens.append(f"#{self.id}")
        return "".join(tokens)

    @property
    def css_identifier_styled(self) -> Text:
        """A syntax highlighted CSS identifier.

        Returns:
            A Rich Text object.
        """
        tokens = Text.styled(self.__class__.__name__)
        if self.id is not None:
            tokens.append(f"#{self.id}", style="bold")
        if self.classes:
            tokens.append(".")
            tokens.append(".".join(class_name for class_name in self.classes), "italic")
        if self.name:
            tokens.append(f"[name={self.name}]", style="underline")
        return tokens

    classes = _ClassesDescriptor()
    """CSS class names for this node."""

    @property
    def pseudo_classes(self) -> frozenset[str]:
        """A (frozen) set of all pseudo classes."""
        return frozenset(self.get_pseudo_classes())

    @property
    def css_path_nodes(self) -> list[DOMNode]:
        """A list of nodes from the App to this node, forming a "path".

        Returns:
            A list of nodes, where the first item is the App, and the last is this node.
        """
        result: list[DOMNode] = [self]
        append = result.append

        node: DOMNode = self
        while isinstance((node := node._parent), DOMNode):
            append(node)
        return result[::-1]

    @property
    def _selector_names(self) -> set[str]:
        """Get a set of selectors applicable to this widget.

        Returns:
            Set of selector names.
        """
        selectors: set[str] = {
            "*",
            *(f".{class_name}" for class_name in self._classes),
            *self._css_types,
        }
        if self._id is not None:
            selectors.add(f"#{self._id}")
        return selectors

    @property
    def display(self) -> bool:
        """Should the DOM node be displayed?

        May be set to a boolean to show or hide the node, or to any valid value for the `display` rule.

        Example:
            ```python
            my_widget.display = False  # Hide my_widget
            ```
        """
        return self.styles.display != "none" and not (
            self._closing or self._closed or self._pruning
        )

    @display.setter
    def display(self, new_val: bool | str) -> None:
        """
        Args:
            new_val: Shortcut to set the ``display`` CSS property.
                ``False`` will set ``display: none``. ``True`` will set ``display: block``.
                A ``False`` value will prevent the DOMNode from consuming space in the layout.
        """
        # TODO: This will forget what the original "display" value was, so if a user
        #  toggles to False then True, we'll reset to the default "block", rather than
        #  what the user initially specified.
        if isinstance(new_val, bool):
            self.styles.display = "block" if new_val else "none"
        elif new_val in VALID_DISPLAY:
            self.styles.display = new_val
        else:
            raise StyleValueError(
                f"invalid value for display (received {new_val!r}, "
                f"expected {friendly_list(VALID_DISPLAY)})",
            )

    @property
    def visible(self) -> bool:
        """Is this widget visible in the DOM?

        If a widget hasn't had its visibility set explicitly, then it inherits it from its
        DOM ancestors.

        This may be set explicitly to override inherited values.
        The valid values include the valid values for the `visibility` rule and the booleans
        `True` or `False`, to set the widget to be visible or invisible, respectively.

        When a node is invisible, Textual will reserve space for it, but won't display anything.
        """
        own_value = self.styles.get_rule("visibility")
        if own_value is not None:
            return own_value != "hidden"
        return self.parent.visible if self.parent else True

    @visible.setter
    def visible(self, new_value: bool | str) -> None:
        if isinstance(new_value, bool):
            self.styles.visibility = "visible" if new_value else "hidden"
        elif new_value in VALID_VISIBILITY:
            self.styles.visibility = new_value
        else:
            raise StyleValueError(
                f"invalid value for visibility (received {new_value!r}, "
                f"expected {friendly_list(VALID_VISIBILITY)})"
            )

    @property
    def tree(self) -> Tree:
        """A Rich tree to display the DOM.

        Log this to visualize your app in the textual console.

        Example:
            ```python
            self.log(self.tree)
            ```

        Returns:
            A Tree renderable.
        """
        from rich.pretty import Pretty

        def render_info(node: DOMNode) -> Pretty:
            """Render a node for the tree."""
            return Pretty(node)

        tree = Tree(render_info(self))

        def add_children(tree, node):
            for child in node.children:
                info = render_info(child)
                branch = tree.add(info)
                if tree.children:
                    add_children(branch, child)

        add_children(tree, self)
        return tree

    @property
    def css_tree(self) -> Tree:
        """A Rich tree to display the DOM, annotated with the node's CSS.

        Log this to visualize your app in the textual console.

        Example:
            ```python
            self.log(self.css_tree)
            ```

        Returns:
            A Tree renderable.
        """
        from rich.columns import Columns
        from rich.console import Group
        from rich.panel import Panel
        from rich.pretty import Pretty

        from textual.widget import Widget

        def render_info(node: DOMNode) -> Columns:
            """Render a node for the tree."""
            if isinstance(node, Widget):
                info = Columns(
                    [
                        Pretty(node),
                        highlighter(f"region={node.region!r}"),
                        highlighter(
                            f"virtual_size={node.virtual_size!r}",
                        ),
                    ]
                )
            else:
                info = Columns([Pretty(node)])
            return info

        highlighter = ReprHighlighter()
        tree = Tree(render_info(self))

        def add_children(tree: Tree, node: DOMNode) -> None:
            """Add children to the tree."""
            for child in node.children:
                info: RenderableType = render_info(child)
                css = child.styles.css
                if css:
                    info = Group(
                        info,
                        Panel.fit(
                            Text(child.styles.css),
                            border_style="dim",
                            title="css",
                            title_align="left",
                        ),
                    )
                branch = tree.add(info)
                if tree.children:
                    add_children(branch, child)

        add_children(tree, self)
        return tree

    @property
    def text_style(self) -> Style:
        """Get the text style object.

        A widget's style is influenced by its parent. for instance if a parent is bold, then
        the child will also be bold.

        Returns:
            A Rich Style.
        """
        return Style.combine(
            node.styles.text_style for node in reversed(self.ancestors_with_self)
        )

    @property
    def selection_style(self) -> Style:
        """The style of selected text."""
        style = self.screen.get_component_rich_style(
            "screen--selection", default=RICH_NULL_STYLE
        )
        return style

    @property
    def rich_style(self) -> Style:
        """Get a Rich Style object for this DOMNode.

        Returns:
            A Rich style.
        """
        background = Color(0, 0, 0, 0)
        color = Color(255, 255, 255, 0)

        style = Style()
        opacity = 1.0

        for node in reversed(self.ancestors_with_self):
            styles = node.styles
            has_rule = styles.has_rule
            opacity *= styles.opacity
            if has_rule("background"):
                text_background = background + styles.background.tint(
                    styles.background_tint
                )
                background += (
                    styles.background.tint(styles.background_tint)
                ).multiply_alpha(opacity)
            else:
                text_background = background
            if has_rule("color"):
                color = styles.color
            style += styles.text_style
            if has_rule("auto_color") and styles.auto_color:
                color = text_background.get_contrast_text(color.a)

        style += Style.from_color(
            (background + color).rich_color if (background.a or color.a) else None,
            background.rich_color if background.a else None,
        )
        return style

    def check_consume_key(self, key: str, character: str | None) -> bool:
        """Check if the widget may consume the given key.

        This should be implemented in widgets that handle [`Key`][textual.events.Key] events and
        stop propagation (such as Input and TextArea).

        Implementing this method will hide key bindings from the footer and key panel that would
        be *consumed* by the focused widget.

        Args:
            key: A key identifier.
            character: A character associated with the key, or `None` if there isn't one.

        Returns:
            `True` if the widget may capture the key in its `Key` event handler, or `False` if it won't.
        """
        return False

    def _get_title_style_information(
        self, background: Color
    ) -> tuple[Color, Color, VisualStyle]:
        """Get a Visual Style object for titles.

        Args:
            background: The background color.

        Returns:
            A Rich style.
        """

        styles = self.styles
        if styles.auto_border_title_color:
            color = background.get_contrast_text(styles.border_title_color.a)
        else:
            color = styles.border_title_color
        return (
            color,
            styles.border_title_background,
            VisualStyle.from_rich_style(styles.border_title_style),
        )

    def _get_subtitle_style_information(
        self, background: Color
    ) -> tuple[Color, Color, VisualStyle]:
        """Get a Rich Style object for subtitles.

        Args:
            background: The background color.

        Returns:
            A Rich style.
        """
        styles = self.styles
        if styles.auto_border_subtitle_color:
            color = background.get_contrast_text(styles.border_subtitle_color.a)
        else:
            color = styles.border_subtitle_color
        return (
            color,
            styles.border_subtitle_background,
            VisualStyle.from_rich_style(styles.border_subtitle_style),
        )

    @property
    def background_colors(self) -> tuple[Color, Color]:
        """Background colors adjusted for opacity.

        Returns:
            `(<background color>, <color>)`
        """
        base_background = background = Color(0, 0, 0, 0)
        opacity = 1.0
        for node in reversed(self.ancestors_with_self):
            styles = node.styles
            base_background = background
            opacity *= styles.opacity
            background += styles.background.tint(styles.background_tint).multiply_alpha(
                opacity
            )
        return (base_background, background)

    @property
    def colors(self) -> tuple[Color, Color, Color, Color]:
        """The widget's background and foreground colors, and the parent's background and foreground colors.

        Returns:
            `(<parent background>, <parent color>, <background>, <color>)`
        """
        base_background = background = WHITE
        base_color = color = BLACK
        for node in reversed(self.ancestors_with_self):
            styles = node.styles
            base_background = background
            background += styles.background.tint(styles.background_tint)
            if styles.has_rule("color"):
                base_color = color
                if styles.auto_color:
                    color = background.get_contrast_text(color.a)
                else:
                    color = styles.color

        return (base_background, base_color, background, color)

    @property
    def ancestors_with_self(self) -> list[DOMNode]:
        """A list of ancestor nodes found by tracing a path all the way back to App.

        Note:
            This is inclusive of ``self``.

        Returns:
            A list of nodes.
        """
        nodes: list[MessagePump | None] = [self]
        add_node = nodes.append
        node: MessagePump | None = self
        while (node := node._parent) is not None:
            add_node(node)
        return cast("list[DOMNode]", nodes)

    @property
    def ancestors(self) -> list[DOMNode]:
        """A list of ancestor nodes found by tracing a path all the way back to App.

        Returns:
            A list of nodes.
        """
        nodes: list[MessagePump | None] = []
        add_node = nodes.append
        node: MessagePump | None = self
        while (node := node._parent) is not None:
            add_node(node)
        return cast("list[DOMNode]", nodes)

    def watch(
        self,
        obj: DOMNode,
        attribute_name: str,
        callback: WatchCallbackType,
        init: bool = True,
    ) -> None:
        """Watches for modifications to reactive attributes on another object.

        Example:
            ```python
            def on_theme_change(old_value:str, new_value:str) -> None:
                # Called when app.theme changes.
                print(f"App.theme went from {old_value} to {new_value}")

            self.watch(self.app, "theme", self.on_theme_change, init=False)
            ```

        Args:
            obj: Object containing attribute to watch.
            attribute_name: Attribute to watch.
            callback: A callback to run when attribute changes.
            init: Check watchers on first call.
        """
        _watch(self, obj, attribute_name, callback, init=init)

    def get_pseudo_classes(self) -> set[str]:
        """Pseudo classes for a widget.

        Returns:
            Names of the pseudo classes.
        """

        return {
            name
            for name, check_class in self._PSEUDO_CLASSES.items()
            if check_class(self)
        }

    def reset_styles(self) -> None:
        """Reset styles back to their initial state."""
        from textual.widget import Widget

        for node in self.walk_children(with_self=True):
            node._css_styles.reset()
            if isinstance(node, Widget):
                node._set_dirty()
                node._layout_required = True

    def _add_child(self, node: Widget) -> None:
        """Add a new child node.

        !!! note
            For tests only.

        Args:
            node: A DOM node.
        """
        self._nodes._append(node)
        node._attach(self)

    def _add_children(self, *nodes: Widget) -> None:
        """Add multiple children to this node.

        !!! note
            For tests only.

        Args:
            *nodes: Positional args should be new DOM nodes.
        """
        _append = self._nodes._append
        for node in nodes:
            node._attach(self)
            _append(node)
            node._add_children(*node._pending_children)

    WalkType = TypeVar("WalkType", bound="DOMNode")

    if TYPE_CHECKING:

        @overload
        def walk_children(
            self,
            filter_type: type[WalkType],
            *,
            with_self: bool = False,
            method: WalkMethod = "depth",
            reverse: bool = False,
        ) -> list[WalkType]: ...

        @overload
        def walk_children(
            self,
            *,
            with_self: bool = False,
            method: WalkMethod = "depth",
            reverse: bool = False,
        ) -> list[DOMNode]: ...

    def walk_children(
        self,
        filter_type: type[WalkType] | None = None,
        *,
        with_self: bool = False,
        method: WalkMethod = "depth",
        reverse: bool = False,
    ) -> list[DOMNode] | list[WalkType]:
        """Walk the subtree rooted at this node, and return every descendant encountered in a list.

        Args:
            filter_type: Filter only this type, or None for no filter.
            with_self: Also yield self in addition to descendants.
            method: One of "depth" or "breadth".
            reverse: Reverse the order (bottom up).

        Returns:
            A list of nodes.
        """
        check_type = filter_type or DOMNode

        node_generator = (
            walk_depth_first(self, check_type, with_root=with_self)
            if method == "depth"
            else walk_breadth_first(self, check_type, with_root=with_self)
        )

        # We want a snapshot of the DOM at this point So that it doesn't
        # change mid-walk
        nodes = list(node_generator)
        if reverse:
            nodes.reverse()
        return cast("list[DOMNode]", nodes)

    if TYPE_CHECKING:

        @overload
        def query(self, selector: str | None = None) -> DOMQuery[Widget]: ...

        @overload
        def query(self, selector: type[QueryType]) -> DOMQuery[QueryType]: ...

    def query(
        self, selector: str | type[QueryType] | None = None
    ) -> DOMQuery[Widget] | DOMQuery[QueryType]:
        """Query the DOM for children that match a selector or widget type.

        Args:
            selector: A CSS selector, widget type, or `None` for all nodes.

        Returns:
            A query object.
        """
        from textual.css.query import DOMQuery, QueryType
        from textual.widget import Widget

        node = self._get_dom_base()
        if isinstance(selector, str) or selector is None:
            return DOMQuery[Widget](node, filter=selector)
        else:
            return DOMQuery[QueryType](node, filter=selector.__name__)

    if TYPE_CHECKING:

        @overload
        def query_children(self, selector: str | None = None) -> DOMQuery[Widget]: ...

        @overload
        def query_children(self, selector: type[QueryType]) -> DOMQuery[QueryType]: ...

    def query_children(
        self, selector: str | type[QueryType] | None = None
    ) -> DOMQuery[Widget] | DOMQuery[QueryType]:
        """Query the DOM for the immediate children that match a selector or widget type.

        Note that this will not return child widgets more than a single level deep.
        If you want to a query to potentially match all children in the widget tree,
        see [query][textual.dom.DOMNode.query].

        Args:
            selector: A CSS selector, widget type, or `None` for all nodes.

        Returns:
            A query object.
        """
        from textual.css.query import DOMQuery, QueryType
        from textual.widget import Widget

        node = self._get_dom_base()
        if isinstance(selector, str) or selector is None:
            return DOMQuery[Widget](node, deep=False, filter=selector)
        else:
            return DOMQuery[QueryType](node, deep=False, filter=selector.__name__)

    if TYPE_CHECKING:

        @overload
        def query_one(self, selector: str) -> Widget: ...

        @overload
        def query_one(self, selector: type[QueryType]) -> QueryType: ...

        @overload
        def query_one(
            self, selector: str, expect_type: type[QueryType]
        ) -> QueryType: ...

    def query_one(
        self,
        selector: str | type[QueryType],
        expect_type: type[QueryType] | None = None,
    ) -> QueryType | Widget:
        """Get a widget from this widget's children that matches a selector or widget type.

        Args:
            selector: A selector or widget type.
            expect_type: Require the object be of the supplied type, or None for any type.

        Raises:
            WrongType: If the wrong type was found.
            NoMatches: If no node matches the query.

        Returns:
            A widget matching the selector.
        """
        _rich_traceback_omit = True

        base_node = self._get_dom_base()

        if isinstance(selector, str):
            query_selector = selector
        else:
            query_selector = selector.__name__

        if is_id_selector(query_selector):
            cache_key = (base_node._nodes._updates, query_selector, expect_type)
            cached_result = base_node._query_one_cache.get(cache_key)
            if cached_result is not None:
                return cached_result
            if (
                node := walk_breadth_search_id(
                    base_node, query_selector[1:], with_root=False
                )
            ) is not None:
                if expect_type is not None and not isinstance(node, expect_type):
                    raise WrongType(
                        f"Node matching {query_selector!r} is the wrong type; expected type {expect_type.__name__!r}, found {node}"
                    )
                base_node._query_one_cache[cache_key] = node
                return node
            raise NoMatches(f"No nodes match {query_selector!r} on {base_node!r}")

        try:
            selector_set = parse_selectors(query_selector)
        except TokenError:
            raise InvalidQueryFormat(
                f"Unable to parse {query_selector!r} as a query; check for syntax errors"
            ) from None

        if all(selectors.is_simple for selectors in selector_set):
            cache_key = (base_node._nodes._updates, query_selector, expect_type)
            cached_result = base_node._query_one_cache.get(cache_key)
            if cached_result is not None:
                return cached_result
        else:
            cache_key = None

        for node in walk_breadth_first(base_node, with_root=False):
            if not match(selector_set, node):
                continue
            if expect_type is not None and not isinstance(node, expect_type):
                raise WrongType(
                    f"Node matching {query_selector!r} is the wrong type; expected type {expect_type.__name__!r}, found {node}"
                )
            if cache_key is not None:
                base_node._query_one_cache[cache_key] = node
            return node

        raise NoMatches(f"No nodes match {query_selector!r} on {base_node!r}")

    if TYPE_CHECKING:

        @overload
        def query_one_optional(self, selector: str) -> Widget | None: ...

        @overload
        def query_one_optional(self, selector: type[QueryType]) -> QueryType | None: ...

        @overload
        def query_one_optional(
            self, selector: str, expect_type: type[QueryType]
        ) -> QueryType | None: ...

    def query_one_optional(
        self,
        selector: str | type[QueryType],
        expect_type: type[QueryType] | None = None,
    ) -> QueryType | Widget | None:
        """Get a widget from this widget's children that matches a selector or widget type,
        or `None` if there is no match.

        Args:
            selector: A selector or widget type.
            expect_type: Require the object be of the supplied type, or None for any type.

        Raises:
            WrongType: If the wrong type was found.

        Returns:
            A widget matching the selector, or `None`.
        """
        try:
            widget = self.query_one(selector, expect_type)
        except NoMatches:
            return None
        return widget

    if TYPE_CHECKING:

        @overload
        def query_exactly_one(self, selector: str) -> Widget: ...

        @overload
        def query_exactly_one(self, selector: type[QueryType]) -> QueryType: ...

        @overload
        def query_exactly_one(
            self, selector: str, expect_type: type[QueryType]
        ) -> QueryType: ...

    def query_exactly_one(
        self,
        selector: str | type[QueryType],
        expect_type: type[QueryType] | None = None,
    ) -> QueryType | Widget:
        """Get a widget from this widget's children that matches a selector or widget type.

        !!! Note
            This method is similar to [query_one][textual.dom.DOMNode.query_one].
            The only difference is that it will raise `TooManyMatches` if there is more than a single match.

        Args:
            selector: A selector or widget type.
            expect_type: Require the object be of the supplied type, or None for any type.

        Raises:
            WrongType: If the wrong type was found.
            NoMatches: If no node matches the query.
            TooManyMatches: If there is more than one matching node in the query (and `exactly_one==True`).

        Returns:
            A widget matching the selector.
        """
        _rich_traceback_omit = True

        base_node = self._get_dom_base()

        if isinstance(selector, str):
            query_selector = selector
        else:
            query_selector = selector.__name__

        try:
            selector_set = parse_selectors(query_selector)
        except TokenError:
            raise InvalidQueryFormat(
                f"Unable to parse {query_selector!r} as a query; check for syntax errors"
            ) from None

        if all(selectors.is_simple for selectors in selector_set):
            cache_key = (base_node._nodes._updates, query_selector, expect_type)
            cached_result = base_node._query_one_cache.get(cache_key)
            if cached_result is not None:
                return cached_result
        else:
            cache_key = None

        children = walk_breadth_first(base_node, with_root=False)
        iter_children = iter(children)
        for node in iter_children:
            if not match(selector_set, node):
                continue
            if expect_type is not None and not isinstance(node, expect_type):
                raise WrongType(
                    f"Node matching {query_selector!r} is the wrong type; expected type {expect_type.__name__!r}, found {node}"
                )
            for later_node in iter_children:
                if match(selector_set, later_node):
                    raise TooManyMatches(
                        "Call to query_one resulted in more than one matched node"
                    )
            if cache_key is not None:
                base_node._query_one_cache[cache_key] = node
            return node

        raise NoMatches(f"No nodes match {query_selector!r} on {base_node!r}")

    if TYPE_CHECKING:

        @overload
        def query_ancestor(self, selector: str) -> DOMNode: ...

        @overload
        def query_ancestor(self, selector: type[QueryType]) -> QueryType: ...

        @overload
        def query_ancestor(
            self, selector: str, expect_type: type[QueryType]
        ) -> QueryType: ...

    def query_ancestor(
        self,
        selector: str | type[QueryType],
        expect_type: type[QueryType] | None = None,
    ) -> DOMNode:
        """Get an ancestor which matches a query.

        Args:
            selector: A TCSS selector.
            expect_type: Expected type, or `None` for any DOMNode.

        Raises:
            InvalidQueryFormat: If the selector is invalid.
            NoMatches: If there are no matching ancestors.

        Returns:
            A DOMNode or subclass if `expect_type` is provided.
        """
        base_node = self._get_dom_base()
        if isinstance(selector, str):
            query_selector = selector
        else:
            query_selector = selector.__name__

        try:
            selector_set = parse_selectors(query_selector)
        except TokenError:
            raise InvalidQueryFormat(
                f"Unable to parse {query_selector!r} as a query; check for syntax errors"
            ) from None
        if base_node.parent is not None:
            for node in base_node.parent.ancestors_with_self:
                if not match(selector_set, node):
                    continue
                if expect_type is not None and not isinstance(node, expect_type):
                    continue
                return node
        raise NoMatches(f"No ancestor matches {selector!r} on {self!r}")

    def set_styles(self, css: str | None = None, **update_styles: Any) -> Self:
        """Set custom styles on this object.

        Args:
            css: Styles in CSS format.
            update_styles: Keyword arguments map style names onto style values.

        Returns:
            Self.
        """

        if css is not None:
            try:
                new_styles = parse_declarations(css, read_from=("set_styles", ""))
            except DeclarationError as error:
                raise DeclarationError(error.name, error.token, error.message) from None
            self._inline_styles.merge(new_styles)
            self.refresh(layout=True)

        styles = self.styles
        for key, value in update_styles.items():
            setattr(styles, key, value)
        return self

    def has_class(self, *class_names: str) -> bool:
        """Check if the Node has all the given class names.

        Args:
            *class_names: CSS class names to check.

        Returns:
            ``True`` if the node has all the given class names, otherwise ``False``.
        """
        return self._classes.issuperset(class_names)

    def set_class(self, add: bool, *class_names: str, update: bool = True) -> Self:
        """Add or remove class(es) based on a condition.

        This can condense the four lines required to implement the equivalent branch into a single line.

        Example:
            ```python
            #if foo:
            #    self.add_class("-foo")
            #else:
            #    self.remove_class("-foo")
            self.set_class(foo, "-foo")
            ```

        Args:
            add: Add the classes if True, otherwise remove them.
            update: Also update styles.

        Returns:
            Self.
        """
        if add:
            self.add_class(*class_names, update=update)
        else:
            self.remove_class(*class_names, update=update)
        return self

    def set_classes(self, classes: str | Iterable[str]) -> Self:
        """Replace all classes.

        Args:
            classes: A string containing space separated classes, or an
                iterable of class names.

        Returns:
            Self.
        """
        self.classes = classes
        return self

    def update_node_styles(self, animate: bool = True) -> None:
        """Request an update of this node's styles.

        Called by Textual whenever CSS classes / pseudo classes change.
        """
        try:
            self.app.update_styles(self, animate=animate)
        except NoActiveAppError:
            pass

    def add_class(self, *class_names: str, update: bool = True) -> Self:
        """Add class names to this Node.

        Args:
            *class_names: CSS class names to add.
            update: Also update styles.

        Returns:
            Self.
        """
        check_identifiers("class name", *class_names)
        old_classes = self._classes.copy()
        self._classes.update(class_names)
        if old_classes == self._classes:
            return self
        if update:
            self.update_node_styles()
        return self

    def remove_class(self, *class_names: str, update: bool = True) -> Self:
        """Remove class names from this Node.

        Args:
            *class_names: CSS class names to remove.
            update: Also update styles.

        Returns:
            Self.
        """
        check_identifiers("class name", *class_names)
        old_classes = self._classes.copy()
        self._classes.difference_update(class_names)
        if old_classes == self._classes:
            return self
        if update:
            self.update_node_styles()
        return self

    def toggle_class(self, *class_names: str) -> Self:
        """Toggle class names on this Node.

        Args:
            *class_names: CSS class names to toggle.

        Returns:
            Self.
        """
        check_identifiers("class name", *class_names)
        old_classes = self._classes.copy()
        self._classes.symmetric_difference_update(class_names)
        if old_classes == self._classes:
            return self
        self.update_node_styles()
        return self

    def has_pseudo_class(self, class_name: str) -> bool:
        """Check the node has the given pseudo class.

        Args:
            class_name: The pseudo class to check for.

        Returns:
            `True` if the DOM node has the pseudo class, `False` if not.
        """
        try:
            return self._PSEUDO_CLASSES[class_name](self)
        except KeyError:
            return False

    def has_pseudo_classes(self, class_names: set[str]) -> bool:
        """Check the node has all the given pseudo classes.

        Args:
            class_names: Set of class names to check for.

        Returns:
            `True` if all pseudo class names are present.
        """
        PSEUDO_CLASSES = self._PSEUDO_CLASSES
        try:
            return all(PSEUDO_CLASSES[name](self) for name in class_names)
        except KeyError:
            return False

    @property
    def _pseudo_classes_cache_key(self) -> tuple[int, ...]:
        """A cache key used when updating a number of nodes from the stylesheet."""
        return ()

    def refresh(
        self, *, repaint: bool = True, layout: bool = False, recompose: bool = False
    ) -> Self:
        return self

    def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
        """Check whether an action is enabled.

        Implement this method to add logic for [dynamic actions](/guide/actions#dynamic-actions) / bindings.

        Args:
            action: The name of an action.
            parameters: A tuple of any action parameters.

        Returns:
            `True` if the action is enabled+visible,
                `False` if the action is disabled+hidden,
                `None` if the action is disabled+visible (grayed out in footer)
        """
        return True

    def refresh_bindings(self) -> None:
        """Call to prompt widgets such as the [Footer][textual.widgets.Footer] to update
        the display of key bindings.

        See [actions](/guide/actions#dynamic-actions) for how to use this method.

        """
        if self._is_mounted:
            self.screen.refresh_bindings()

    async def action_toggle(self, attribute_name: str) -> None:
        """Toggle an attribute on the node.

        Assumes the attribute is a bool.

        Args:
            attribute_name: Name of the attribute.
        """
        value = getattr(self, attribute_name)
        setattr(self, attribute_name, not value)
