"""E2E: the revoke() success path (Tier-0 #4 / survivability §3.4).

The audit gap closed here: pre-2026-05-26 the broker shipped
`revoke` as a wire op but no test exercised the *success path*
end-to-end through the SDK — only error paths (NotPermitted,
NotFound, Unsupported on fake) and the wire-format contract.

Lease engine semantics this pin:

  1. A live lease can be revoked by a cap carrying the `steal` right.
  2. After revoke, calling `lease.raw()` raises RevokedError
     (NOT InvalidTokenError — the distinction is the whole point
     of Tier-0 #4: clients learn their access was preempted, not
     that the token never existed).
  3. After revoke, calling `lease.release()` returns cleanly —
     release is TOTAL + IDEMPOTENT (§6.3), and a revoked lease
     is already torn down so there's nothing for the backend
     to release.

Substrate: FakeBackend's `hw:/test/dev0` (commit 0f7f485 added
vRevoke). Real backends (USB/VFIO) need root + hardware to test
the success path; the fake covers the lease-engine semantics
that are identical across all backends.
"""

from __future__ import annotations

import pytest

from mgf.hrb import ClaimMode, Client
from mgf.hrb.exceptions import MgfHrbError, RevokedError

pytestmark = [pytest.mark.e2e, pytest.mark.timeout(30)]


def test_revoke_marks_lease_revoked_subsequent_raw_returns_revoked_error(
    broker: Client,
) -> None:
    """The full happy path:
    claim → revoke → raw() raises RevokedError (not Invalid)."""
    lease = broker.claim("hw:/test/dev0", ClaimMode.EXCLUSIVE)
    try:
        # Pre-revoke: raw() works (the lease is live).
        raw = lease.raw()
        assert "fd" in raw  # FakeBackend exports a real memfd

        # Revoke via the broker's cap (root cap carries `steal`).
        broker.revoke("hw:/test/dev0")

        # Post-revoke: raw() raises RevokedError.
        # This is the lease-engine signal a client uses to learn
        # "your access was preempted." If we got InvalidTokenError
        # instead, the holder couldn't distinguish "never had it"
        # from "lost it" — the Tier-0 #4 point.
        with pytest.raises(RevokedError):
            lease.raw()
    finally:
        # Release on a revoked lease is a clean no-op (idempotent + total).
        try:
            lease.release()
        except MgfHrbError:
            pass


def test_revoke_then_release_is_clean_noop(broker: Client) -> None:
    """Release on a revoked lease must NOT raise — the contract
    says release is TOTAL + IDEMPOTENT (§6.3)."""
    lease = broker.claim("hw:/test/dev1", ClaimMode.EXCLUSIVE)
    broker.revoke("hw:/test/dev1")
    # Release returns cleanly — there's nothing to tear down
    # (backend already did it during revoke), but the contract
    # holds: release never raises against a known lease id.
    lease.release()


def test_revoke_idempotent_when_nothing_held(broker: Client) -> None:
    """Revoking a URN with no live lease is a no-op success.
    Operators may call revoke prophylactically; doing so against
    an unclaimed URN must not be an error."""
    # No claim active on dev0 — should succeed cleanly.
    broker.revoke("hw:/test/dev0")
    # And again — still no-op.
    broker.revoke("hw:/test/dev0")


def test_revoke_unknown_urn_raises(broker: Client) -> None:
    """Revoking a URN with no route raises a typed error."""
    with pytest.raises(MgfHrbError):
        broker.revoke("hw:/nonexistent/path")
