"""Contract: the `control` op's optimistic-concurrency response
shape (Part 13 §5.1.2, sub-phase 4B) + the per-(urn, verb)
serialiser (sub-phase 4A).

These pins cover the WIRE shape:

  * Success response carries `ok`, `generation`, `applied_value`,
    `queue_depth`, `superseded_by`.
  * Error response is unchanged: `{ok:false, err: ...}`.
  * Request grew an optional `cap_id` field (integer; defaults 0).
    Non-integer + negative integer reject with BadRequest at the
    gate.

The SUCCESS-PATH live test requires root (the broker writes to
sysfs files like `/sys/bus/usb/devices/<dev>/authorized`).
Without root, this file pins only the error-path shape + the
gate validation. The state-machine semantics live in Zig unit
tests at `hrb-code/src/linux_usb_test.zig` (controlStateFor +
slot reuse + registry overflow).
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from mgf.hrb import Client, ControlOutcome, UnsupportedError
from tests.contract.conftest import rt

if TYPE_CHECKING:
    import socket

pytestmark = pytest.mark.contract


# ---------------------------------------------------------------------------
# Gate validation: cap_id grammar.
# ---------------------------------------------------------------------------


def test_cap_id_defaults_to_zero(raw_broker: socket.socket) -> None:
    """Omitting `cap_id` MUST parse cleanly (defaults to 0). The
    error here surfaces from the underlying control call, not the
    gate — proves the gate accepted the missing field."""
    r = rt(
        raw_broker,
        {"op": "control", "urn": "hw:/test/dev0", "verb": "set_authorized", "value": "1"},
    )
    # fake backend → Unsupported (passed the gate cleanly)
    assert r.get("ok") is False
    assert r.get("err") == "Unsupported"


def test_cap_id_negative_rejects(raw_broker: socket.socket) -> None:
    r = rt(
        raw_broker,
        {
            "op": "control",
            "urn": "hw:/test/dev0",
            "verb": "set_authorized",
            "value": "1",
            "cap_id": -1,
        },
    )
    assert r.get("ok") is False
    assert r.get("err") == "BadRequest"


def test_cap_id_non_integer_rejects(raw_broker: socket.socket) -> None:
    for bad in ("abc", 1.5, [1], {"x": 1}, None):
        r = rt(
            raw_broker,
            {
                "op": "control",
                "urn": "hw:/test/dev0",
                "verb": "set_authorized",
                "value": "1",
                "cap_id": bad,
            },
        )
        assert r.get("ok") is False, f"cap_id={bad!r}: {r}"
        assert r.get("err") == "BadRequest"


# ---------------------------------------------------------------------------
# Error-path response shape (unchanged + still error-shaped).
# ---------------------------------------------------------------------------


def test_unsupported_backend_unchanged_shape(raw_broker: socket.socket) -> None:
    """On the Unsupported path (fake backend has no control), the
    response stays the legacy `{ok:false, err:Unsupported}` —
    the disclosure fields are NEVER spliced into error responses
    (they only have meaning when a write landed)."""
    r = rt(
        raw_broker,
        {"op": "control", "urn": "hw:/test/dev0", "verb": "set_authorized", "value": "1"},
    )
    assert r == {"ok": False, "err": "Unsupported"}


def test_not_found_unchanged_shape(raw_broker: socket.socket) -> None:
    """Same on NotFound — the response is the legacy error shape
    without disclosure fields."""
    r = rt(
        raw_broker, {"op": "control", "urn": "hw:/never", "verb": "set_authorized", "value": "1"}
    )
    assert r == {"ok": False, "err": "NotFound"}


# ---------------------------------------------------------------------------
# SDK projection.
# ---------------------------------------------------------------------------


def test_sdk_propagates_unsupported(broker: Client) -> None:
    """The SDK's `Client.control()` MUST still raise
    `UnsupportedError` for backends without control. The new
    return type (`ControlOutcome`) doesn't change the error
    path."""
    with pytest.raises(UnsupportedError):
        broker.control("hw:/test/dev0", "set_authorized", "1")
    # Same with cap_id explicit — still raises.
    with pytest.raises(UnsupportedError):
        broker.control("hw:/test/dev0", "set_authorized", "1", cap_id=42)


def test_sdk_returns_control_outcome_dataclass() -> None:
    """The `ControlOutcome` type is importable + the right shape.
    A live-success projection test would need root + a writable
    sysfs file; the dataclass-shape test stands in. Combined with
    the wire pins above + the Zig state-machine tests, the
    coverage is end-to-end without needing privilege."""
    # Pure type-shape pin — no broker needed.
    sample = ControlOutcome(
        generation=1,
        applied_value="1",
        queue_depth=0,
        superseded_by=None,
    )
    assert sample.generation == 1
    assert sample.applied_value == "1"
    assert sample.queue_depth == 0
    assert sample.superseded_by is None
