"""Assertion helpers for ``MockRelay`` — observable NIP-01 frames.

Importing this module binds the helpers as methods on the ``MockRelay``
class, so tests read ``mock_relay.expect_req({...})`` without any
import-side ceremony. Each helper looks ONLY at ``relay.received``; no
hooks into the system under test.
"""

from __future__ import annotations

import time
from typing import Any

from mock_relay import MockRelay


def _filter_matches(actual: dict, expected: dict) -> bool:
    """``expected`` is a filter spec — every key in it must be present in
    ``actual`` with a matching value. List-valued expected fields
    (``authors``, ``#p``, ``kinds``) are compared as SETS because NIP-01
    filters semantically union over those lists. Keys present in
    ``actual`` but not in ``expected`` are ignored — tests assert only
    on what they care about."""
    for key, exp_val in expected.items():
        if key not in actual:
            return False
        act_val = actual[key]
        if isinstance(exp_val, list):
            if not isinstance(act_val, list) or set(act_val) != set(exp_val):
                return False
        else:
            if act_val != exp_val:
                return False
    return True


def expect_req(
    self: "MockRelay",
    expected_filter: dict[str, Any],
    *,
    timeout: float = 5.0,
) -> None:
    """Wait until this relay has received a REQ frame whose filter matches
    ``expected_filter``. Matcher rules in :func:`_filter_matches`. The
    sub may have been closed afterwards — we only care that the relay
    *was asked*. Raises ``AssertionError`` on timeout with all REQ
    filters received so far for diagnostics.
    """
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        for frame in self.received:
            if len(frame) >= 3 and frame[0] == "REQ":
                if _filter_matches(frame[2], expected_filter):
                    return
        time.sleep(0.05)

    seen = [f[2] for f in self.received if len(f) >= 3 and f[0] == "REQ"]
    raise AssertionError(
        f"timed out after {timeout}s waiting for REQ matching "
        f"{expected_filter!r} on {self.url}\n"
        f"  REQ filters seen: {seen!r}"
    )


MockRelay.expect_req = expect_req  # type: ignore[attr-defined]
