"""Form-rendering accessibility mixins.

`AriaInvalidMixin` walks bound forms after binding and tags invalid widgets
with `aria-invalid="true"` plus an `aria-describedby` token pointing at the
field error span rendered by `templates/components/form_field.html`. The
template gives every error span a stable id of `{field.id_for_label}_error`,
which this mixin matches.
"""

from __future__ import annotations


def _merge_describedby(existing: str | None, token: str) -> str:
    if not existing:
        return token
    parts = existing.split()
    if token in parts:
        return existing
    parts.append(token)
    return " ".join(parts)


class AriaInvalidMixin:
    """Adds `aria-invalid` and `aria-describedby` to widgets with errors.

    Mix into Django forms (or ModelForms) to render screen-reader-friendly
    validation feedback. Idempotent on rebinds — re-running does not stack
    duplicate tokens. Only does work when the form is bound, so the empty
    render path stays cheap.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not self.is_bound:
            return
        self._apply_aria_invalid()

    def _apply_aria_invalid(self) -> None:
        for name, field in self.fields.items():
            bound = self[name]
            if not bound.errors:
                continue
            attrs = field.widget.attrs
            attrs["aria-invalid"] = "true"
            error_id = f"{bound.id_for_label}_error"
            attrs["aria-describedby"] = _merge_describedby(
                attrs.get("aria-describedby"), error_id
            )
