"""Contract: the `drain_events` op's emitted event-kind shapes.

Pins the wire JSON shape of every event kind the broker emits via
`drain_events` / `monitor` / `amonitor` / `monitor_fd`. A consumer
that dispatches on `event["kind"]` depends on these exact strings;
adding a kind is additive (existing consumers ignore it), but
RENAMING one or changing the field schema is a breaking wire
change — caught here.

Sub-phase 1C (Part 13 §5.4.1) added `driver_bound` /
`driver_unbound`. This file pins all SEVEN kinds the broker
emits today:

  • added             — `Resource` appeared at URN
  • removed           — `Resource` disappeared from URN
  • state_changed    — `Resource` transitioned states (carries
                        old/new + optional previous_urn)
  • gap              — watch source dropped events; recovery needed
  • driver_bound     — kernel driver took ownership of URN
  • driver_unbound   — kernel driver released URN
  • other            — sentinel for any future kind not yet wired

The test pins the wire SHAPE; end-to-end behaviour against a live
broker is exercised by the use_case + stress suites.
"""

from __future__ import annotations

from typing import Any

import pytest

from mgf.hrb import Client

pytestmark = pytest.mark.contract


def _pin_event_shape(event: dict[str, Any], required: list[str], values: dict[str, Any]) -> None:
    for key in required:
        assert key in event, f"required key {key!r} missing from {event!r}"
    for key, want in values.items():
        assert event[key] == want, f"{key} = {event[key]!r}, expected {want!r} (event: {event!r})"


def test_added_event_shape() -> None:
    ev: dict[str, Any] = {"kind": "added", "urn": "hw:/usb/bus3/port-9/dev/if0"}
    _pin_event_shape(
        ev,
        required=["kind", "urn"],
        values={"kind": "added", "urn": "hw:/usb/bus3/port-9/dev/if0"},
    )


def test_removed_event_shape() -> None:
    ev: dict[str, Any] = {"kind": "removed", "urn": "hw:/usb/bus3/port-9/dev/if0"}
    _pin_event_shape(
        ev,
        required=["kind", "urn"],
        values={"kind": "removed"},
    )


def test_state_changed_event_shape_minimal() -> None:
    """state_changed without previous_urn — the URN didn't change,
    just the state did."""
    ev: dict[str, Any] = {
        "kind": "state_changed",
        "urn": "hw:/usb/bus3/port-9/dev/if0",
        "old": "claimable",
        "new": "claimed",
    }
    _pin_event_shape(
        ev,
        required=["kind", "urn", "old", "new"],
        values={"kind": "state_changed"},
    )


def test_state_changed_event_shape_with_previous_urn() -> None:
    """state_changed with previous_urn — the synth collapsed a
    remove+add pair into a single state transition; the previous
    URN is carried for consumers that want to correlate."""
    ev: dict[str, Any] = {
        "kind": "state_changed",
        "urn": "hw:/usb/bus3/port-9/dev/if0",
        "old": "claimable",
        "new": "claimable",
        "previous_urn": "hw:/usb/bus3/port-9/dev/if0",
    }
    _pin_event_shape(
        ev,
        required=["kind", "urn", "old", "new", "previous_urn"],
        values={"kind": "state_changed"},
    )


def test_gap_event_shape() -> None:
    """gap signals a watch source dropped events; consumer needs to
    re-enumerate or fall back to a coarser refresh."""
    ev: dict[str, Any] = {
        "kind": "gap",
        "source": "netlink_uevent",
        "since_ms": 12_345,
    }
    _pin_event_shape(
        ev,
        required=["kind", "source", "since_ms"],
        values={"kind": "gap"},
    )


def test_driver_bound_event_shape() -> None:
    """Sub-phase 1C — Part 13 §5.4.1. driver_bound carries the URN
    of the device whose driver was just attached AND the driver
    name (so consumers can route on driver class)."""
    ev: dict[str, Any] = {
        "kind": "driver_bound",
        "urn": "hw:/usb/bus3/port-9/dev/if0",
        "driver": "usbhid",
    }
    _pin_event_shape(
        ev,
        required=["kind", "urn", "driver"],
        values={
            "kind": "driver_bound",
            "urn": "hw:/usb/bus3/port-9/dev/if0",
            "driver": "usbhid",
        },
    )


def test_driver_unbound_event_shape() -> None:
    """Sub-phase 1C — Part 13 §5.4.1. driver_unbound's URN remains
    the device (the device may still exist post-unbind); driver
    field carries the name of the driver that just released."""
    ev: dict[str, Any] = {
        "kind": "driver_unbound",
        "urn": "hw:/usb/bus3/port-9/dev/if0",
        "driver": "snd-usb-audio",
    }
    _pin_event_shape(
        ev,
        required=["kind", "urn", "driver"],
        values={
            "kind": "driver_unbound",
            "driver": "snd-usb-audio",
        },
    )


def test_drain_events_routes_every_kind_through_sdk(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Stub the transport so we can verify Client.drain_events
    surfaces all seven kinds without filtering or coercion. This
    is the "SDK doesn't drop events it doesn't recognise" contract
    — additive future kinds keep flowing through."""
    canned: list[dict[str, Any]] = [
        {"kind": "added", "urn": "hw:/usb/bus3/port-9/dev/if0"},
        {"kind": "driver_bound", "urn": "hw:/usb/bus3/port-9/dev/if0", "driver": "usbhid"},
        {"kind": "driver_unbound", "urn": "hw:/usb/bus3/port-9/dev/if0", "driver": "usbhid"},
        {"kind": "removed", "urn": "hw:/usb/bus3/port-9/dev/if0"},
        # Forward-compat: a future kind the SDK doesn't know about
        # MUST still flow through (don't drop, don't crash).
        {"kind": "device_ready_FUTURE_KIND", "urn": "..."},
    ]

    class _Stub:
        def call(self, **req: Any) -> dict[str, Any]:
            assert req["op"] == "drain_events"
            return {"ok": True, "events": canned}

    # Build a Client without going through Client.connect (we don't
    # need a real socket — we're testing pure SDK projection here).
    client = Client.__new__(Client)
    client._t = _Stub()  # type: ignore[assignment]
    client._cap = 1

    received = client.drain_events("hw:/usb/**")
    assert received == canned, (
        f"SDK dropped or coerced events: got {received!r}, expected {canned!r}"
    )
