"""Unit pins for the long-poll monitor stream (M-B chunk B3).

Two facts that need a peer but not a broker, so they live here rather
than in the contract tier: the deprecated `poll_interval_s` really is
inert, and each monitor shape names ITSELF when it warns.

The wire-level truths — latency, the reconnect, the B3-AM1 refusals
against a real broker — are in
`tests/contract/test_sdk_longpoll_monitor.py`; the failure-mode truths
(wedged, crashed, poisoned) are in `test_sdk10_deadlines.py`.
"""

from __future__ import annotations

import time

import pytest

# ---------------------------------------------------------------------------
# AP-04 — the deprecated parameter that changes nothing.
# ---------------------------------------------------------------------------


def test_poll_interval_s_warns_and_changes_nothing() -> None:
    """`poll_interval_s` named a wait strategy the SDK no longer has.
    AP-04 says announce → still works ≥1 MINOR → removed, so it is
    accepted for 0.1.11 and does exactly nothing.

    "Nothing" is the load-bearing word and it is what this pin measures:
    the request the monitor builds with a wildly different
    `poll_interval_s` is byte-identical to the one it builds without it.
    A shim that quietly still influenced the deadline — the old floor
    was `poll_interval_s * 1000 + 2000` — would be worse than removing
    the parameter outright, because the caller would be getting a bound
    they did not ask for from a parameter documented as inert."""
    from tests.unit.mgf.hrb._monitor_fake import FakeEventBroker

    from mgf.hrb import Client

    def drains_with(**kwargs: object) -> list[dict[str, object]]:
        broker = FakeEventBroker([[{"kind": "added", "urn": "hw:/test/dev0"}]])
        try:
            client = Client.connect(broker.path)
            try:
                it = client.monitor("hw:/test/**", wait_ms=50, **kwargs)  # type: ignore[arg-type]
                next(it)
                it.close()
            finally:
                client.close()
            return broker.requests_for("drain_events")
        finally:
            broker.stop()

    with pytest.warns(DeprecationWarning, match="no effect|NO EFFECT"):
        deprecated = drains_with(poll_interval_s=30.0)
    plain = drains_with()

    assert deprecated, "the monitor never drained"
    assert deprecated[0] == plain[0], (
        "poll_interval_s changed the wire request; it is documented as inert"
    )
    assert deprecated[0]["deadline_ms"] == 50 + 2000, deprecated[0]


def test_every_monitor_shape_warns_about_poll_interval_s() -> None:
    """All three, each naming ITSELF in the message — a consumer
    grepping their logs for the deprecation has to be able to tell which
    call site to fix, and "the monitor family" would not."""
    from tests.unit.mgf.hrb._monitor_fake import FakeEventBroker

    from mgf.hrb import Client

    broker = FakeEventBroker()
    try:
        client = Client.connect(broker.path)
        try:
            with pytest.warns(DeprecationWarning, match=r"^monitor\("):
                client.monitor("hw:/**", poll_interval_s=1.0).close()
            with pytest.warns(DeprecationWarning, match=r"^amonitor\("):
                client.amonitor("hw:/**", poll_interval_s=1.0)
            with pytest.warns(DeprecationWarning, match=r"^monitor_fd\("):
                client.monitor_fd("hw:/**", wait_ms=10, poll_interval_s=1.0)
        finally:
            client.close()
    finally:
        broker.stop()


# ---------------------------------------------------------------------------
# Back-pressure is not a broken stream.
# ---------------------------------------------------------------------------


def test_a_shed_drain_backs_off_instead_of_reconnecting() -> None:
    """`Overloaded` means "the broker is busy", not "your stream died".

    The distinction is not academic. Reconnecting on back-pressure burns
    a connection AND a subscribe — two more ops into the token bucket
    that just shed one — announces a `gap` that did not happen, and
    kills the subscription ring the stream was reading, which converts
    transient throttling into REAL event loss. All three of those make
    the overload worse rather than better.

    So: one subscribe for the life of the stream, no gap, and the events
    that were queued behind the shed drains still arrive.

    This pin exists because sabotage round S9 found nothing to fail:
    the stress suite tolerates shedding by design, so the wrong
    behaviour was invisible to every test in the repo."""
    from tests.unit.mgf.hrb._monitor_fake import FakeEventBroker

    from mgf.hrb import Client

    broker = FakeEventBroker(
        [[{"kind": "added", "urn": "hw:/test/dev0"}]],
        shed_first=2,
    )
    try:
        client = Client.connect(broker.path)
        try:
            it = client.monitor("hw:/test/**", wait_ms=50)
            first = next(it)
            it.close()
        finally:
            client.close()
    finally:
        broker.stop()

    assert first == {"kind": "added", "urn": "hw:/test/dev0"}, (
        f"the shed drains lost the event behind them: got {first}"
    )
    assert first.get("source") != "monitor_reconnect", (
        "back-pressure was announced as a gap — the consumer will rescan "
        "the whole world because the broker was briefly busy"
    )
    assert len(broker.requests_for("subscribe")) == 1, (
        "the stream re-subscribed after a shed drain: that is a reconnect, "
        "and it spends two more ops on a broker that just told us it is full"
    )


# ---------------------------------------------------------------------------
# Teardown — the interrupt must reach every shape, not just the fd one.
# ---------------------------------------------------------------------------


def test_a_cancelled_amonitor_stops_waiting_immediately() -> None:
    """Cancelling an `amonitor` consumer must not cost the rest of the
    poll (F-B3-01).

    The drain runs on an `asyncio.to_thread` worker. Cancellation
    unblocks the AWAIT instantly, which makes the task look fine — but
    the worker is still parked in the broker's wait, holding a
    default-executor thread, and `asyncio.run` joins the default
    executor on its way out. So the cost lands on the program's exit
    rather than on the cancel, which is exactly where nobody looks for
    it: the court measured 0.60 / 4.60 / 14.60 s for `wait_ms` of
    1000 / 5000 / 15000.

    The interrupt in `_EventStream.close()` is what collapses that, and
    it was DEAD CODE until this fold — `close()` nulled `self._client`
    and then called `self.interrupt()`, whose own None-guard ate the
    call. Only `_close_monitor_fd`'s direct `stream.interrupt()` kept
    the fd shape honest, and the fd shape was the only one pinned.

    15 s of `wait_ms` against a 1 s budget: a 15x margin that no
    scheduler hiccup can fake, and a pin that cannot pass by accident
    if the interrupt goes missing again."""
    import asyncio

    from tests.unit.mgf.hrb._monitor_fake import FakeEventBroker

    from mgf.hrb import Client

    wait_ms = 15_000
    broker = FakeEventBroker(idle_delay_s=wait_ms / 1000.0)
    settle_s = 0.3
    try:
        client = Client.connect(broker.path)

        async def consume(agen: object) -> None:
            async for _ in agen:  # type: ignore[attr-defined]
                pass

        async def main() -> None:
            agen = client.amonitor("hw:/**", wait_ms=wait_ms)
            task = asyncio.create_task(consume(agen))
            # Let the first drain reach the worker thread and park.
            await asyncio.sleep(settle_s)
            task.cancel()
            with pytest.raises(asyncio.CancelledError):
                await task

        started = time.monotonic()
        asyncio.run(main())
        elapsed = time.monotonic() - started - settle_s
        client.close()
    finally:
        broker.stop()

    assert elapsed < 1.0, (
        f"a cancelled amonitor took {elapsed:.2f}s to let go with wait_ms="
        f"{wait_ms}: the parked drain was not interrupted, so the program "
        "waits out the poll on its way to exit"
    )


def test_the_deprecation_warning_points_at_the_callers_line() -> None:
    """F-B3-06. A `DeprecationWarning` whose `filename` is `_client.py`
    tells the consumer that the SDK deprecated its own parameter — true,
    useless, and actively misleading under
    `-W error::DeprecationWarning`, where the traceback then points into
    a file the consumer cannot edit.

    `stacklevel` has to count the whole chain — caller → `monitor()` →
    `_open_event_stream()` → the warn — and the pin asserts the outcome
    (this file) rather than the number, so it survives a refactor that
    changes the depth and catches one that forgets to."""
    from tests.unit.mgf.hrb._monitor_fake import FakeEventBroker

    from mgf.hrb import Client

    broker = FakeEventBroker()
    try:
        client = Client.connect(broker.path)
        try:
            with pytest.warns(DeprecationWarning) as record:
                client.monitor("hw:/**", poll_interval_s=1.0).close()
        finally:
            client.close()
    finally:
        broker.stop()

    assert len(record) == 1, [str(w.message) for w in record]
    assert record[0].filename == __file__, (
        f"the warning blamed {record[0].filename}, not the caller "
        f"({__file__}) — a consumer reading it learns nothing they can act on"
    )
