"""
Synthetic Discord client backed by a JSON fixture + the ``_RoleStub``
stand-in it returns from ``guild_roles``.

``_FixtureClient`` is used by ``--from-fixture`` repro mode:
``build_plan`` accepts it in place of an ``AuditedDiscordClient`` so
an operator can replay classification logic against a snapshot
attached to a ticket. ``_RoleStub`` is the matching role-like
stand-in; ``audit_benchmark`` also constructs ``_RoleStub`` instances
directly when assembling a synthetic role catalogue. Both live here
(not in the main client module) because they are CLI-only concerns
and the protocol implemented is implicit (``guild_roles`` +
``guild_members`` are the only methods ``build_plan`` reaches for).
"""

from __future__ import annotations

from typing import TYPE_CHECKING, cast

if TYPE_CHECKING:  # pragma: no cover
    from collections.abc import Generator


class _RoleStub:
    """Role-like stand-in for the ``RoleLike`` protocol used by build_plan."""

    # ``managed`` is part of the ``RoleLike`` protocol that
    # ``role_filter.persistent_role_ids_for`` and ``is_managed_by_aa``
    # consult during ``apply_plan``. Without it on the slot list, any
    # attribute access on a stub instance raises ``AttributeError``;
    # adding it to ``__slots__`` and giving the constructor a default
    # of ``False`` (the synthetic decorative roles produced here are
    # never Discord-managed by definition) keeps the apply-side hot
    # path crash-free.
    __slots__ = ("id", "managed", "name")

    def __init__(
        self, role_id: int, name: str, *, managed: bool = False
    ) -> None:
        self.id = role_id
        self.name = name
        self.managed = managed


class _FixtureClient:
    """Synthetic AuditedDiscordClient backed by a JSON fixture."""

    def __init__(
        self,
        roles: list[dict[str, object]],
        members_pages: list[list[dict[str, object]]],
    ) -> None:
        # Roles arrive as JSON dicts; convert to the .id/.name shape
        # build_plan + role_filter consume.  Pages stay as raw dicts —
        # GuildMember.from_dict reads them via bracket access at the
        # classify call site, so re-shaping here would be wasted work.
        # JSON values land as ``object`` in the type system; ``int(...)``
        # rejects ``object`` directly. Cast each field at the parse
        # boundary — fixture authoring trusts caller-provided JSON to
        # be well-formed, and the runtime ``int()`` / ``str()`` calls
        # raise loudly on mistyped values.
        self._roles = [
            _RoleStub(
                role_id=int(cast("int | str", r["id"])),
                name=str(r["name"]),
            )
            for r in roles
        ]
        self._members_pages = members_pages

    def guild_roles(self, guild_id: object) -> list[_RoleStub]:
        del guild_id
        return list(self._roles)

    def guild_members(
        self, *, guild_id: object, limit: int
    ) -> Generator[list[dict[str, object]], None, None]:
        del guild_id, limit
        for page in self._members_pages:
            yield list(page)
