from __future__ import annotations

import hashlib
from collections import deque
from types import SimpleNamespace
from typing import Any, cast

import paho.mqtt.client as mqtt
import pytest

from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
    DEVICE_INFO_RESPONSE_TOPIC,
    GET_RTK_ADVANCE_REQUEST_TOPIC,
    GET_RTK_ADVANCE_RESPONSE_TOPIC,
    MODELING_STATUS_RESPONSE_TOPIC,
    LiveDeviceControlBinding,
)
from k1link.device_plugins.xgrids_k1.protocol.application_mqtt import (
    CONTROL_SUBSCRIPTION_GROUPS,
    MODELING_RESPONSE_TOPIC,
    SYSTEM_ERROR_TOPIC,
    ApplicationCommandOutcomeUnknown,
    ApplicationControlDeviceFault,
    ReviewedApplicationMqttTransport,
)
from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
    OneShotPublishEnvelope,
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import (
    MODELING_STATE_BASE,
    SYSTEM_ERROR_STATE_BASE,
    ModelingAction,
    SessionState,
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_safety import (
    DEVICE_STATUS_TOPIC,
    MODELING_REQUEST_TOPIC,
)

VENDOR_DEVICE_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
DEVICE_SERIAL = "K1SERIAL01"
APPLICATION_KEY = "11111111-2222-3333-4444-555555555555"


class FakeReasonCode:
    def __init__(self, *, failure: bool = False) -> None:
        self.is_failure = failure


class FakeClock:
    def __init__(self) -> None:
        self.now = 100.0

    def __call__(self) -> float:
        return self.now


class FakeControlClient:
    def __init__(
        self,
        *,
        emit_exchange: bool = True,
        clock: FakeClock | None = None,
        response_topic: str = DEVICE_INFO_RESPONSE_TOPIC,
    ) -> None:
        self.on_connect: Any = None
        self.on_subscribe: Any = None
        self.on_publish: Any = None
        self.on_message: Any = None
        self.on_disconnect: Any = None
        self.emit_exchange = emit_exchange
        self.clock = clock
        self.response_topic = response_topic
        self.events: deque[tuple[str, object]] = deque()
        self.connect_calls: list[tuple[str, int, int]] = []
        self.subscribe_calls: list[list[tuple[str, int]]] = []
        self.publish_calls: list[tuple[str, bytes, int, bool]] = []
        self.unsubscribe_calls: list[list[str]] = []
        self.disconnect_calls = 0
        self.next_mid = 20
        self.connect_timeout = 5.0

    def connect(self, host: str, port: int, keepalive: int) -> mqtt.MQTTErrorCode:
        self.connect_calls.append((host, port, keepalive))
        self.events.append(("connect", FakeReasonCode()))
        return mqtt.MQTT_ERR_SUCCESS

    def subscribe(self, topics: list[tuple[str, int]]) -> tuple[mqtt.MQTTErrorCode, int]:
        self.subscribe_calls.append(topics)
        mid = 6 + len(self.subscribe_calls)
        self.events.append(("subscribe", mid))
        return mqtt.MQTT_ERR_SUCCESS, mid

    def publish(
        self,
        topic: str,
        payload: bytes,
        qos: int,
        retain: bool,
    ) -> SimpleNamespace:
        self.publish_calls.append((topic, payload, qos, retain))
        mid = self.next_mid
        self.next_mid += 1
        if self.emit_exchange:
            self.events.append(("publish", mid))
            self.events.append(("message", (self.response_topic, payload)))
        return SimpleNamespace(rc=mqtt.MQTT_ERR_SUCCESS, mid=mid)

    def loop(self, timeout: float) -> mqtt.MQTTErrorCode:
        del timeout
        if self.clock is not None:
            self.clock.now += 1.0
        if not self.events:
            return mqtt.MQTT_ERR_SUCCESS
        kind, value = self.events.popleft()
        if kind == "connect":
            self.on_connect(self, None, SimpleNamespace(), value, None)
        elif kind == "subscribe":
            group = CONTROL_SUBSCRIPTION_GROUPS[int(value) - 7]
            self.on_subscribe(
                self,
                None,
                value,
                [FakeReasonCode() for _item in group],
                None,
            )
        elif kind == "publish":
            self.on_publish(self, None, value, FakeReasonCode(), None)
        elif kind == "message":
            topic, payload = value
            self.on_message(self, None, SimpleNamespace(topic=topic, payload=payload))
        return mqtt.MQTT_ERR_SUCCESS

    def unsubscribe(self, topics: list[str]) -> tuple[mqtt.MQTTErrorCode, int]:
        self.unsubscribe_calls.append(topics)
        return mqtt.MQTT_ERR_SUCCESS, 50

    def disconnect(self) -> mqtt.MQTTErrorCode:
        self.disconnect_calls += 1
        return mqtt.MQTT_ERR_SUCCESS


def _application_message(
    session_id: str,
    *,
    device_id: str | None = None,
) -> bytes:
    header = b"".join(
        (
            _text(4, device_id) if device_id is not None else b"",
            _text(5, session_id),
            _text(6, APPLICATION_KEY),
        )
    )
    return _bytes(1, header)


def _envelope(
    operation_key: str = "bootstrap:1:DeviceInfoRequest",
    *,
    topic: str = "lixel/application/request/device_info",
    session_id: str = ":DeviceInfoRequest",
    device_id: str | None = None,
) -> OneShotPublishEnvelope:
    payload = _application_message(session_id, device_id=device_id)
    return OneShotPublishEnvelope(
        operation_key=operation_key,
        topic=topic,
        payload=payload,
        payload_sha256=hashlib.sha256(payload).hexdigest(),
        payload_bytes=len(payload),
        qos=2,
        retain=False,
    )


def _modeling_status_envelope(
    operation_key: str = "dialogue:12:ModelingStatusRequest",
) -> OneShotPublishEnvelope:
    payload = _application_message(
        f"{VENDOR_DEVICE_ID}:ModelingStatusRequest",
        device_id=VENDOR_DEVICE_ID,
    )
    return OneShotPublishEnvelope(
        operation_key=operation_key,
        topic="lixel/application/request/modeling_status",
        payload=payload,
        payload_sha256=hashlib.sha256(payload).hexdigest(),
        payload_bytes=len(payload),
        qos=2,
        retain=False,
    )


def _modeling_envelope(action: ModelingAction) -> OneShotPublishEnvelope:
    payload = _application_message(
        f"{VENDOR_DEVICE_ID}:ModelingRequest",
        device_id=VENDOR_DEVICE_ID,
    ) + _uint(2, action)
    return OneShotPublishEnvelope(
        operation_key=f"modeling:{action.name.casefold()}",
        topic=MODELING_REQUEST_TOPIC,
        payload=payload,
        payload_sha256=hashlib.sha256(payload).hexdigest(),
        payload_bytes=len(payload),
        qos=2,
        retain=False,
    )


def _modeling_response(action: ModelingAction) -> bytes:
    return (
        _application_message(
            f"{VENDOR_DEVICE_ID}:ModelingRequest",
            device_id=VENDOR_DEVICE_ID,
        )
        + _uint(2, action)
        + _bytes(15, _uint(1, 0))
    )


def _varint(value: int) -> bytes:
    encoded = bytearray()
    while value > 0x7F:
        encoded.append((value & 0x7F) | 0x80)
        value >>= 7
    encoded.append(value)
    return bytes(encoded)


def _uint(number: int, value: int) -> bytes:
    return _varint(number << 3) + _varint(value)


def _bytes(number: int, value: bytes) -> bytes:
    return _varint((number << 3) | 2) + _varint(len(value)) + value


def _text(number: int, value: str) -> bytes:
    return _bytes(number, value.encode())


def _device_status(state: SessionState, *, project_bound: bool, init_ready: bool) -> bytes:
    header = _text(4, VENDOR_DEVICE_ID)
    fields = [
        _bytes(1, header),
        _uint(2, MODELING_STATE_BASE + state),
        _text(3, DEVICE_SERIAL),
    ]
    if project_bound:
        fields.append(_text(4, "project-present"))
    if init_ready:
        fields.append(_uint(6, 1))
    return b"".join(fields)


def _system_error(state: SessionState) -> bytes:
    return _bytes(15, _uint(1, SYSTEM_ERROR_STATE_BASE + state))


def _binding() -> LiveDeviceControlBinding:
    return LiveDeviceControlBinding(
        vendor_device_id=VENDOR_DEVICE_ID,
        device_serial=DEVICE_SERIAL,
        software_version="V3.0.2-20260101-release",
        system_version="V3.0.2",
        device_model="LixelKity K1",
        device_type="A4",
        is_activated=True,
    )


def test_acceptance_transport_rejects_the_k1_access_point_fallback() -> None:
    with pytest.raises(ValueError, match="not a direct-LAN"):
        ReviewedApplicationMqttTransport("192.168.56.1")


def test_acceptance_transport_admits_the_k1_ap_only_with_an_explicit_gate() -> None:
    transport = ReviewedApplicationMqttTransport(
        "192.168.56.1",
        allow_device_ap=True,
    )

    assert transport.snapshot().state == "new"


def test_retained_control_subscription_batches_remain_exact_and_separate_from_points() -> None:
    assert [len(group) for group in CONTROL_SUBSCRIPTION_GROUPS] == [9, 5, 42]
    assert CONTROL_SUBSCRIPTION_GROUPS[0][-1] == (DEVICE_INFO_RESPONSE_TOPIC, 0)
    assert ("lixel/application/response/modeling", 2) in CONTROL_SUBSCRIPTION_GROUPS[2]
    assert all(
        topic not in {"RealtimePointcloud", "lixel/application/report/lio_pcl"}
        for group in CONTROL_SUBSCRIPTION_GROUPS
        for topic, _qos in group
    )


def test_acceptance_transport_connects_once_and_completes_one_qos2_exchange() -> None:
    fake = FakeControlClient()
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        client_factory=lambda: cast(mqtt.Client, fake),
    )

    opened = transport.open().as_dict()
    responses = transport.exchange_batch_once(
        [_envelope()],
        required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
    )
    transport.close()

    assert fake.connect_calls == [("192.168.1.20", 1883, 60)]
    assert fake.connect_timeout == 10.0
    assert fake.subscribe_calls == [list(group) for group in CONTROL_SUBSCRIPTION_GROUPS]
    assert fake.publish_calls == [
        (
            "lixel/application/request/device_info",
            _application_message(":DeviceInfoRequest"),
            2,
            False,
        )
    ]
    assert responses == {
        "bootstrap:1:DeviceInfoRequest": _application_message(":DeviceInfoRequest")
    }
    assert opened["clean_session"] is False
    assert opened["automatic_reconnect"] is False
    assert opened["automatic_retry"] is False
    snapshot = transport.snapshot().as_dict()
    assert snapshot["state"] == "closed"
    assert snapshot["publish_attempts"] == 1
    assert snapshot["subscribe_attempts"] == 3
    assert snapshot["qos2_completions"] == 1
    assert snapshot["correlated_responses"] == 1
    assert snapshot["operation_keys_consumed"] == 1


def test_consumed_operation_key_can_never_be_published_again() -> None:
    fake = FakeControlClient()
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        client_factory=lambda: cast(mqtt.Client, fake),
    )
    transport.open()
    transport.exchange_batch_once(
        [_envelope()],
        required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
    )

    with pytest.raises(ApplicationCommandOutcomeUnknown, match="already consumed"):
        transport.exchange_batch_once(
            [_envelope()],
            required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
        )

    assert len(fake.publish_calls) == 1


def test_same_topic_inflight_responses_are_routed_by_exact_session_without_retry() -> None:
    fake = FakeControlClient(response_topic=GET_RTK_ADVANCE_RESPONSE_TOPIC)
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        client_factory=lambda: cast(mqtt.Client, fake),
    )
    transport.open()
    unbound = _envelope(
        "bootstrap:3:GetRtkAdvanceRequest",
        topic=GET_RTK_ADVANCE_REQUEST_TOPIC,
        session_id=":GetRtkAdvanceRequest",
    )
    bound_session = f"{VENDOR_DEVICE_ID}:GetRtkAdvanceRequest"
    bound = _envelope(
        "bootstrap:6:GetRtkAdvanceRequest",
        topic=GET_RTK_ADVANCE_REQUEST_TOPIC,
        session_id=bound_session,
        device_id=VENDOR_DEVICE_ID,
    )

    responses = transport.exchange_batch_once(
        [unbound, bound],
        required_response_operation_keys={unbound.operation_key, bound.operation_key},
    )

    assert responses[unbound.operation_key] == _application_message(
        ":GetRtkAdvanceRequest"
    )
    assert responses[bound.operation_key] == _application_message(
        bound_session,
        device_id=VENDOR_DEVICE_ID,
    )
    snapshot = transport.snapshot()
    assert snapshot.publish_attempts == 2
    assert snapshot.qos2_completions == 2
    assert snapshot.correlated_responses == 2
    assert snapshot.late_known_responses == 0
    assert snapshot.automatic_retry is False
    assert len(fake.publish_calls) == 2


def test_duplicate_start_response_blocks_stop_before_stop_is_published() -> None:
    class ModelingControlClient(FakeControlClient):
        def __init__(self) -> None:
            super().__init__(
                emit_exchange=False,
                response_topic=MODELING_RESPONSE_TOPIC,
            )

        def publish(
            self,
            topic: str,
            payload: bytes,
            qos: int,
            retain: bool,
        ) -> SimpleNamespace:
            info = super().publish(topic, payload, qos, retain)
            action = (
                ModelingAction.START
                if len(self.publish_calls) == 1
                else ModelingAction.STOP
            )
            self.events.append(("publish", info.mid))
            self.events.append(
                (
                    "message",
                    (MODELING_RESPONSE_TOPIC, _modeling_response(action)),
                )
            )
            return info

    fake = ModelingControlClient()
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        client_factory=lambda: cast(mqtt.Client, fake),
    )
    transport.open()

    transport.exchange_batch_once(
        [_modeling_envelope(ModelingAction.START)],
        required_response_operation_keys={"modeling:start"},
    )
    fake.events.append(
        (
            "message",
            (
                MODELING_RESPONSE_TOPIC,
                _modeling_response(ModelingAction.START),
            ),
        )
    )
    with pytest.raises(ApplicationCommandOutcomeUnknown, match="duplicate application response"):
        transport.exchange_batch_once(
            [_modeling_envelope(ModelingAction.STOP)],
            required_response_operation_keys={"modeling:stop"},
        )

    snapshot = transport.snapshot()
    assert snapshot.publish_attempts == 1
    assert snapshot.correlated_responses == 1
    assert snapshot.late_known_responses == 0
    assert snapshot.state == "poisoned"
    assert len(fake.publish_calls) == 1


def test_unanswered_optional_status_does_not_steal_same_identity_required_refresh() -> None:
    class ReusedStatusClient(FakeControlClient):
        def __init__(self) -> None:
            super().__init__(
                emit_exchange=False,
                response_topic=MODELING_STATUS_RESPONSE_TOPIC,
            )

        def publish(
            self,
            topic: str,
            payload: bytes,
            qos: int,
            retain: bool,
        ) -> SimpleNamespace:
            info = super().publish(topic, payload, qos, retain)
            self.events.append(("publish", info.mid))
            if len(self.publish_calls) == 2:
                # Ordinals 12 and 14 intentionally reuse the same protocol
                # identity.  The capture has only one response after ordinal
                # 14; unanswered optional ordinal 12 must not swallow it.
                self.events.append(
                    ("message", (MODELING_STATUS_RESPONSE_TOPIC, payload))
                )
            return info

    fake = ReusedStatusClient()
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        client_factory=lambda: cast(mqtt.Client, fake),
    )
    transport.open()
    immediate = _modeling_status_envelope()
    refresh = _modeling_status_envelope("dialogue:14:ModelingStatusRequest")

    transport.exchange_batch_once(
        [immediate],
        required_response_operation_keys=(),
    )
    responses = transport.exchange_batch_once(
        [refresh],
        required_response_operation_keys={refresh.operation_key},
    )

    assert responses[refresh.operation_key] == refresh.payload
    snapshot = transport.snapshot()
    assert snapshot.publish_attempts == 2
    assert snapshot.correlated_responses == 1
    assert snapshot.ignored_known_responses == 0
    assert snapshot.late_known_responses == 0


def test_already_arrived_optional_status_is_consumed_before_required_refresh() -> None:
    class ReusedStatusClient(FakeControlClient):
        def __init__(self) -> None:
            super().__init__(
                emit_exchange=False,
                response_topic=MODELING_STATUS_RESPONSE_TOPIC,
            )

        def publish(
            self,
            topic: str,
            payload: bytes,
            qos: int,
            retain: bool,
        ) -> SimpleNamespace:
            info = super().publish(topic, payload, qos, retain)
            self.events.append(("publish", info.mid))
            if len(self.publish_calls) == 2:
                self.events.append(
                    ("message", (MODELING_STATUS_RESPONSE_TOPIC, payload))
                )
            return info

    fake = ReusedStatusClient()
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        client_factory=lambda: cast(mqtt.Client, fake),
    )
    transport.open()
    immediate = _modeling_status_envelope()
    refresh = _modeling_status_envelope("dialogue:14:ModelingStatusRequest")

    transport.exchange_batch_once(
        [immediate],
        required_response_operation_keys=(),
    )
    # This callback is already available before ordinal 14 is admitted.  The
    # retained socket's zero-wait service turn must consume it as ordinal 12.
    fake.events.append(
        ("message", (MODELING_STATUS_RESPONSE_TOPIC, immediate.payload))
    )
    responses = transport.exchange_batch_once(
        [refresh],
        required_response_operation_keys={refresh.operation_key},
    )

    assert responses[refresh.operation_key] == refresh.payload
    snapshot = transport.snapshot()
    assert snapshot.publish_attempts == 2
    assert snapshot.correlated_responses == 1
    assert snapshot.ignored_known_responses == 1
    assert snapshot.late_known_responses == 1


def test_unbound_optional_status_remains_distinct_from_bound_required_refresh() -> None:
    class DistinctStatusClient(FakeControlClient):
        def __init__(self) -> None:
            super().__init__(
                emit_exchange=False,
                response_topic=MODELING_STATUS_RESPONSE_TOPIC,
            )

        def publish(
            self,
            topic: str,
            payload: bytes,
            qos: int,
            retain: bool,
        ) -> SimpleNamespace:
            info = super().publish(topic, payload, qos, retain)
            self.events.append(("publish", info.mid))
            if len(self.publish_calls) == 2:
                self.events.append(
                    (
                        "message",
                        (
                            MODELING_STATUS_RESPONSE_TOPIC,
                            _application_message(":ModelingStatusRequest"),
                        ),
                    )
                )
                self.events.append(
                    ("message", (MODELING_STATUS_RESPONSE_TOPIC, payload))
                )
            return info

    fake = DistinctStatusClient()
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        client_factory=lambda: cast(mqtt.Client, fake),
    )
    transport.open()
    initial = _envelope(
        "bootstrap:2:ModelingStatusRequest",
        topic="lixel/application/request/modeling_status",
        session_id=":ModelingStatusRequest",
    )
    refresh = _modeling_status_envelope("dialogue:14:ModelingStatusRequest")

    transport.exchange_batch_once(
        [initial],
        required_response_operation_keys=(),
    )
    responses = transport.exchange_batch_once(
        [refresh],
        required_response_operation_keys={refresh.operation_key},
    )

    assert responses[refresh.operation_key] == refresh.payload
    snapshot = transport.snapshot()
    assert snapshot.publish_attempts == 2
    assert snapshot.correlated_responses == 1
    assert snapshot.ignored_known_responses == 1
    assert snapshot.late_known_responses == 1


def test_duplicate_current_application_response_fails_closed() -> None:
    class DuplicateResponseClient(FakeControlClient):
        def publish(
            self,
            topic: str,
            payload: bytes,
            qos: int,
            retain: bool,
        ) -> SimpleNamespace:
            info = super().publish(topic, payload, qos, retain)
            self.events.append(("message", (self.response_topic, payload)))
            return info

    fake = DuplicateResponseClient()
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        client_factory=lambda: cast(mqtt.Client, fake),
    )
    transport.open()

    with pytest.raises(
        ApplicationCommandOutcomeUnknown,
        match="duplicate application response",
    ) as raised:
        transport.exchange_batch_once(
            [_envelope()],
            required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
        )

    assert raised.value.reason_code == "duplicate_application_response"
    assert transport.snapshot().state == "poisoned"
    assert len(fake.publish_calls) == 1


def test_post_start_status_can_be_response_free_and_socket_is_continuously_serviced() -> None:
    clock = FakeClock()
    fake = FakeControlClient(
        clock=clock,
        response_topic=MODELING_STATUS_RESPONSE_TOPIC,
    )
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        client_factory=lambda: cast(mqtt.Client, fake),
        monotonic=clock,
    )
    transport.open()

    responses = transport.exchange_batch_once(
        [_modeling_status_envelope()],
        required_response_operation_keys=(),
    )
    transport.maintain_open_for(
        2.0,
        allowed_response_topics={MODELING_STATUS_RESPONSE_TOPIC},
    )

    assert responses == {}
    assert transport.snapshot().state == "ready"
    assert transport.snapshot().qos2_completions == 1
    assert transport.snapshot().ignored_known_responses == 1


def test_control_owner_uses_live_state_gates_and_surfaces_system_error() -> None:
    clock = FakeClock()
    fake = FakeControlClient(clock=clock)
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        client_factory=lambda: cast(mqtt.Client, fake),
        monotonic=clock,
    )
    transport.open()

    fake.events.append(
        (
            "message",
            (
                DEVICE_STATUS_TOPIC,
                _device_status(
                    SessionState.READY,
                    project_bound=False,
                    init_ready=False,
                ),
            ),
        )
    )
    transport.maintain_open_for(1.0)
    assert transport.pre_start_ready(_binding()) is True

    fake.events.append(
        (
            "message",
            (
                DEVICE_STATUS_TOPIC,
                _device_status(
                    SessionState.SCANNING,
                    project_bound=True,
                    init_ready=True,
                ),
            ),
        )
    )
    transport.maintain_open_for(1.0)

    assert transport.scan_initialization_complete(_binding()) is True
    assert transport.pre_start_ready(_binding()) is False
    assert transport.standby_complete(_binding()) is False

    fake.events.append(
        (
            "message",
            (SYSTEM_ERROR_TOPIC, _system_error(SessionState.ALGORITHM_ERROR)),
        )
    )
    transport.maintain_open_for(1.0)

    snapshot = transport.snapshot().as_dict()
    assert snapshot["device_status_reports"] == 2
    assert snapshot["system_error_reports"] == 1
    assert snapshot["latest_system_error_code"] == 0x32040133
    assert snapshot["latest_system_error_state"] == "algorithm_error"
    with pytest.raises(ApplicationControlDeviceFault, match="algorithm_error"):
        transport.scan_initialization_complete(_binding())


def test_post_publish_timeout_poisoned_transport_never_retries() -> None:
    clock = FakeClock()
    fake = FakeControlClient(emit_exchange=False, clock=clock)
    transport = ReviewedApplicationMqttTransport(
        "192.168.1.20",
        exchange_timeout_seconds=2.0,
        client_factory=lambda: cast(mqtt.Client, fake),
        monotonic=clock,
    )
    transport.open()

    with pytest.raises(ApplicationCommandOutcomeUnknown, match="automatic retry is forbidden"):
        transport.exchange_batch_once(
            [_envelope()],
            required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
        )

    assert transport.snapshot().state == "poisoned"
    assert len(fake.publish_calls) == 1
    with pytest.raises(ApplicationCommandOutcomeUnknown, match="already consumed"):
        transport.exchange_batch_once(
            [_envelope()],
            required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
        )
    assert len(fake.publish_calls) == 1
