"""Tests for the three first-class Fake* backends (D-F3).

Verifies the contract behaviors the fakes promise: idempotency,
typed errors, cancellation honored, state changes observable. The
*conformance suite* (separately) runs an exhaustive scenario
catalog against these same fakes; this file is the per-class unit
suite.
"""

from __future__ import annotations

import pytest

from mgf.vm.core.cancellation import CancellationEvent
from mgf.vm.core.contracts import (
    ClaimMode,
    DeviceBackend,
    HostDiskBackend,
    HypervisorBackend,
    MigrationPlan,
    MigrationTarget,
    NetworkBackend,
    NetworkSpec,
    PoolSpec,
    StorageBackend,
    VmSpec,
    VmState,
    VolumeSpec,
)
from mgf.vm.core.exceptions import (
    BackupError,
    DeviceContended,
    DiskOperationError,
    DomainAlreadyExistsError,
    DomainNotFound,
    DomainStateInvalid,
    FormatUnsupported,
    GuestUnresponsive,
    InsufficientCapacity,
    LeaseExpired,
    LeaseNotFound,
    MigrationIncompatible,
    NetworkExists,
    NetworkNotFound,
    OperationCancelled,
    PoolExists,
    PoolNotEmpty,
    PoolNotFound,
    SnapshotExists,
    SnapshotNotFound,
    SpecConflict,
    VolumeExists,
    VolumeHasSnapshots,
    VolumeNotFound,
)
from mgf.vm.core.fakes import (
    FakeDeviceBackend,
    FakeHostDiskBackend,
    FakeHypervisorBackend,
    FakeNetworkBackend,
    FakeStorageBackend,
)
from mgf.vm.core.ids import DomainId, HostId, VolumeId

# ===========================================================================
# Fakes satisfy the Protocols structurally (runtime_checkable)
# ===========================================================================


def test_fake_hypervisor_implements_protocol() -> None:
    assert isinstance(FakeHypervisorBackend(), HypervisorBackend)


def test_fake_storage_implements_protocol() -> None:
    assert isinstance(FakeStorageBackend(), StorageBackend)


def test_fake_device_implements_protocol() -> None:
    assert isinstance(FakeDeviceBackend.with_default_devices(), DeviceBackend)


def test_fake_host_disk_implements_protocol() -> None:
    assert isinstance(FakeHostDiskBackend(), HostDiskBackend)


def test_fake_network_implements_protocol() -> None:
    assert isinstance(FakeNetworkBackend(FakeHypervisorBackend()), NetworkBackend)


# ===========================================================================
# FakeHostDiskBackend (Wave-R4, D-G18)
# ===========================================================================


class TestHostDisk:
    def _backend(self) -> FakeHostDiskBackend:
        b = FakeHostDiskBackend()
        b._put_image("/img/vda.qcow2", content=b"root", virtual_size=2 << 30)
        return b

    def test_image_info_reports_neutral_shape(self) -> None:
        info = self._backend().image_info("/img/vda.qcow2")
        assert info.path == "/img/vda.qcow2"
        assert info.format == "qcow2"
        assert info.virtual_size == 2 << 30
        assert info.actual_size == len(b"root")
        assert info.backing_file is None

    def test_image_info_missing_raises(self) -> None:
        with pytest.raises(DiskOperationError):
            FakeHostDiskBackend().image_info("/nope.qcow2")

    def test_copy_image_is_independent_full_copy(self) -> None:
        b = self._backend()
        b.copy_image("/img/vda.qcow2", "/img/clone.qcow2")
        copy = b.image_info("/img/clone.qcow2")
        assert copy.virtual_size == 2 << 30
        assert copy.backing_file is None

    def test_copy_image_missing_source_raises(self) -> None:
        with pytest.raises(DiskOperationError):
            FakeHostDiskBackend().copy_image("/nope", "/dest")

    def test_copy_image_refuses_overwrite(self) -> None:
        b = self._backend()
        b._put_image("/img/dest.qcow2")
        with pytest.raises(DiskOperationError, match="refusing to overwrite"):
            b.copy_image("/img/vda.qcow2", "/img/dest.qcow2")

    def test_copy_image_honors_cancellation(self) -> None:
        b = self._backend()
        token = CancellationEvent()
        token.cancel()
        with pytest.raises(OperationCancelled):
            b.copy_image("/img/vda.qcow2", "/img/clone.qcow2", token=token)

    def test_create_overlay_points_at_backing(self) -> None:
        b = self._backend()
        b.create_overlay("/img/vda.qcow2", "/img/overlay.qcow2")
        ov = b.image_info("/img/overlay.qcow2")
        assert ov.backing_file == "/img/vda.qcow2"
        assert ov.format == "qcow2"
        assert ov.virtual_size == 2 << 30

    def test_create_overlay_missing_backing_raises(self) -> None:
        with pytest.raises(DiskOperationError):
            FakeHostDiskBackend().create_overlay("/nope", "/overlay")

    def test_create_overlay_refuses_overwrite(self) -> None:
        b = self._backend()
        b._put_image("/img/overlay.qcow2")
        with pytest.raises(DiskOperationError, match="refusing to overwrite"):
            b.create_overlay("/img/vda.qcow2", "/img/overlay.qcow2")

    def test_archive_extract_round_trip(self) -> None:
        b = self._backend()
        b._put_image("/img/vdb.raw", content=b"data", fmt="raw")
        manifest = b.archive(
            "web",
            {"vda": "/img/vda.qcow2", "vdb": "/img/vdb.raw"},
            "/backups/web.tar",
            metadata={"vm_spec": "{...}"},
        )
        assert manifest.vm_name == "web"
        assert manifest.archive_path == "/backups/web.tar"
        assert manifest.created_at.utcoffset() is not None
        assert {d.target for d in manifest.disks} == {"vda", "vdb"}
        assert all(d.sha256 for d in manifest.disks)
        assert dict(manifest.metadata) == {"vm_spec": "{...}"}

        restored = b.extract("/backups/web.tar", "/restore")
        assert {d.target for d in restored.disks} == {"vda", "vdb"}
        assert dict(restored.metadata) == {"vm_spec": "{...}"}
        # The restored disks are readable images in the store.
        for d in restored.disks:
            assert b._has_image(d.path)

    def test_archive_missing_disk_raises_backup_error(self) -> None:
        b = self._backend()
        with pytest.raises(BackupError):
            b.archive("web", {"vda": "/nope.qcow2"}, "/backups/web.tar")

    def test_archive_refuses_overwrite(self) -> None:
        b = self._backend()
        b.archive("web", {"vda": "/img/vda.qcow2"}, "/backups/web.tar")
        with pytest.raises(BackupError, match="refusing to overwrite"):
            b.archive("web", {"vda": "/img/vda.qcow2"}, "/backups/web.tar")

    def test_extract_unknown_archive_raises(self) -> None:
        with pytest.raises(BackupError, match="no archive"):
            FakeHostDiskBackend().extract("/nope.tar", "/restore")

    def test_extract_detects_checksum_corruption(self) -> None:
        b = self._backend()
        b.archive("web", {"vda": "/img/vda.qcow2"}, "/backups/web.tar")
        # Tamper with the captured disk content so its hash no longer matches.
        b._archives["/backups/web.tar"].disks[0].content = b"tampered"
        with pytest.raises(BackupError, match="checksum mismatch"):
            b.extract("/backups/web.tar", "/restore")

    def test_archive_honors_cancellation(self) -> None:
        b = self._backend()
        token = CancellationEvent()
        token.cancel()
        with pytest.raises(OperationCancelled):
            b.archive("web", {"vda": "/img/vda.qcow2"}, "/b.tar", token=token)

    def test_extract_honors_cancellation(self) -> None:
        b = self._backend()
        b.archive("web", {"vda": "/img/vda.qcow2"}, "/backups/web.tar")
        token = CancellationEvent()
        token.cancel()
        with pytest.raises(OperationCancelled):
            b.extract("/backups/web.tar", "/restore", token=token)

    def test_close_and_context_manager(self) -> None:
        b = FakeHostDiskBackend()
        assert b.closed is False
        with b as ctx:
            assert ctx is b
        assert b.closed is True


# ===========================================================================
# FakeNetworkBackend (Wave-R3, D-G15)
# ===========================================================================


class TestNetwork:
    def _pair(self) -> tuple[FakeNetworkBackend, FakeHypervisorBackend]:
        hv = FakeHypervisorBackend()
        return FakeNetworkBackend(hv), hv

    def _spec(self, name: str = "lab-net") -> NetworkSpec:
        return NetworkSpec(name=name, forward_mode="nat", cidr="192.168.70.0/24")

    def test_define_is_observable_and_inactive(self) -> None:
        net, hv = self._pair()
        profile = net.define_network(self._spec())
        assert profile.name == "lab-net"
        assert profile.active is False
        observed = {n.name: n for n in hv.list_networks()}
        assert "lab-net" in observed
        assert observed["lab-net"].active is False

    def test_start_then_stop_toggles_active(self) -> None:
        net, hv = self._pair()
        net.define_network(self._spec())
        net.start_network("lab-net")
        assert next(n for n in hv.list_networks() if n.name == "lab-net").active is True
        net.stop_network("lab-net")
        assert next(n for n in hv.list_networks() if n.name == "lab-net").active is False

    def test_delete_removes_and_is_idempotent(self) -> None:
        net, hv = self._pair()
        net.define_network(self._spec())
        net.delete_network("lab-net")
        assert all(n.name != "lab-net" for n in hv.list_networks())
        net.delete_network("lab-net")  # idempotent — no raise

    def test_define_idempotent_same_shape(self) -> None:
        net, _hv = self._pair()
        first = net.define_network(self._spec())
        net.start_network("lab-net")
        # A redefine with the same shape returns the existing (active) profile.
        again = net.define_network(self._spec())
        assert again.active is True
        assert again == first.__class__(
            name="lab-net", bridge=None, active=True, forward_mode="nat"
        )

    def test_define_conflict_different_shape_raises(self) -> None:
        net, _hv = self._pair()
        net.define_network(NetworkSpec(name="lab-net", forward_mode="nat"))
        with pytest.raises(NetworkExists):
            net.define_network(NetworkSpec(name="lab-net", forward_mode="bridge"))

    def test_start_unknown_raises(self) -> None:
        net, _hv = self._pair()
        with pytest.raises(NetworkNotFound):
            net.start_network("ghost")

    def test_stop_unknown_raises(self) -> None:
        net, _hv = self._pair()
        with pytest.raises(NetworkNotFound):
            net.stop_network("ghost")

    def test_close_and_context_manager_does_not_close_hypervisor(self) -> None:
        net, hv = self._pair()
        assert net.closed is False
        with net as ctx:
            assert ctx is net
        assert net.closed is True
        # The shared hypervisor is untouched.
        assert hv.list_networks()  # still serves observation


# ===========================================================================
# FakeHypervisorBackend
# ===========================================================================


def _spec(name: str = "vm-1", cpu: int = 2, mem_mb: int = 1024) -> VmSpec:
    return VmSpec(name=name, cpu=cpu, memory_bytes=mem_mb * 1024 * 1024)


class TestHypervisor:
    def test_define_returns_domain_id(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        assert isinstance(d, DomainId)
        assert d.value.startswith("fake-")

    def test_define_idempotent_same_spec(self) -> None:
        """contracts.md §3 — same spec returns same DomainId."""
        backend = FakeHypervisorBackend()
        spec = _spec("alpha")
        d1 = backend.define(spec)
        d2 = backend.define(spec)
        assert d1 == d2

    def test_define_conflict_on_different_spec(self) -> None:
        backend = FakeHypervisorBackend()
        backend.define(_spec("alpha", cpu=2))
        with pytest.raises(SpecConflict):
            backend.define(_spec("alpha", cpu=4))

    def test_get_domain_missing_raises_domain_not_found(self) -> None:
        backend = FakeHypervisorBackend()
        with pytest.raises(DomainNotFound):
            backend.get_domain(DomainId("nope"))

    def test_rename_preserves_id_and_updates_name(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("before"))
        backend.rename(d, "after")
        info = backend.get_domain(d)
        assert info.domain_id == d
        assert info.name == "after"

    def test_rename_to_same_name_is_noop(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("same"))
        backend.rename(d, "same")  # no raise
        assert backend.get_domain(d).name == "same"

    def test_rename_to_taken_name_raises_already_exists(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("one"))
        backend.define(_spec("two"))
        with pytest.raises(DomainAlreadyExistsError):
            backend.rename(d, "two")

    def test_rename_running_domain_raises_state_invalid(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("run"))
        backend.start(d)
        with pytest.raises(DomainStateInvalid):
            backend.rename(d, "run-renamed")

    def test_rename_missing_domain_raises_domain_not_found(self) -> None:
        backend = FakeHypervisorBackend()
        with pytest.raises(DomainNotFound):
            backend.rename(DomainId("nope"), "x")

    def test_list_domains_empty_initially(self) -> None:
        assert FakeHypervisorBackend().list_domains() == []

    def test_lifecycle_start_stop_observable(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        assert backend.get_domain(d).is_running is False
        backend.start(d)
        assert backend.get_domain(d).is_running is True
        backend.stop(d)
        assert backend.get_domain(d).is_running is False

    def test_start_idempotent(self) -> None:
        """contracts.md §3 — already-running is success."""
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        backend.start(d)
        backend.start(d)  # idempotent

    def test_stop_idempotent(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        backend.stop(d)  # already stopped

    def test_graceful_stop_unresponsive(self) -> None:
        """When the guest is unresponsive, graceful stop raises."""
        backend = FakeHypervisorBackend(graceful_stop_succeeds=False)
        d = backend.define(_spec("alpha"))
        backend.start(d)
        with pytest.raises(GuestUnresponsive):
            backend.stop(d, force=False)
        # Force still works.
        backend.stop(d, force=True)
        assert backend.get_domain(d).is_running is False

    def test_undefine_running_raises(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        backend.start(d)
        with pytest.raises(DomainStateInvalid):
            backend.undefine(d)

    def test_undefine_idempotent_when_missing(self) -> None:
        FakeHypervisorBackend().undefine(DomainId("nope"))  # no raise

    def test_cancellation_raises_on_start(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        token = CancellationEvent()
        token.cancel()
        with pytest.raises(OperationCancelled):
            backend.start(d, token=token)

    def test_snapshot_idempotent_same_name(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        s1 = backend.snapshot(d, "before-upgrade")
        s2 = backend.snapshot(d, "before-upgrade")
        assert s1 == s2

    def test_snapshot_conflict_different_memory_flag(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        backend.snapshot(d, "named", include_memory=False)
        with pytest.raises(SnapshotExists):
            backend.snapshot(d, "named", include_memory=True)

    def test_snapshot_captures_vm_state(self) -> None:
        # A domain snapshot records the VM's run-state at capture time (D-G21).
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        backend.snapshot(d, "while-stopped")
        backend.start(d)
        backend.snapshot(d, "while-running")
        by_name = {s.name: s for s in backend.list_snapshots(d)}
        assert by_name["while-stopped"].state is VmState.STOPPED
        assert by_name["while-running"].state is VmState.RUNNING

    def test_revert_missing_snapshot_raises(self) -> None:
        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        with pytest.raises(SnapshotNotFound):
            from mgf.vm.core.ids import SnapshotId

            backend.revert_snapshot(d, SnapshotId("nope"))

    def test_capabilities_reports_fake_engine(self) -> None:
        caps = FakeHypervisorBackend().capabilities()
        assert caps.engine == "fake"
        assert caps.max_cpus >= 1

    def test_migrate_incompatible_raises(self) -> None:
        from mgf.vm.core.contracts import CompatibilityResult

        backend = FakeHypervisorBackend()
        d = backend.define(_spec("alpha"))
        backend.start(d)
        plan = MigrationPlan(
            compatibility=CompatibilityResult(
                compatible=False, reasons=("CPU feature missing: vmx",)
            )
        )
        with pytest.raises(MigrationIncompatible):
            backend.migrate(d, MigrationTarget(host=HostId("host-x")), plan=plan)
        # source should remain running per contracts.md §3
        assert backend.get_domain(d).is_running is True

    def test_context_manager_closes(self) -> None:
        with FakeHypervisorBackend() as backend:
            backend.define(_spec("alpha"))
        assert backend.closed is True


# ===========================================================================
# FakeStorageBackend
# ===========================================================================


class TestStorage:
    def test_default_pool_exists(self) -> None:
        assert FakeStorageBackend().get_pool("default").name == "default"

    def test_get_missing_pool_raises(self) -> None:
        with pytest.raises(PoolNotFound):
            FakeStorageBackend().get_pool("ghost")

    def test_pool_reports_available_and_active(self) -> None:
        pool = FakeStorageBackend().get_pool("default")
        # A fresh default pool is fully available and active (D-G20).
        assert pool.available_bytes == pool.capacity_bytes - pool.allocation_bytes
        assert pool.active is True

    def test_refresh_pool_mirrors_get_pool(self) -> None:
        backend = FakeStorageBackend()
        assert backend.refresh_pool("default") == backend.get_pool("default")

    def test_refresh_missing_pool_raises(self) -> None:
        with pytest.raises(PoolNotFound):
            FakeStorageBackend().refresh_pool("ghost")

    def test_volume_reports_path_and_allocation(self) -> None:
        backend = FakeStorageBackend()
        backend.create_volume(VolumeSpec(pool="default", name="disk0", size_bytes=2048))
        vol = next(v for v in backend.list_volumes("default") if v.name == "disk0")
        assert vol.path.endswith("/disk0")
        assert vol.size_bytes == 2048
        assert vol.allocation_bytes == 2048

    def test_create_pool_idempotent(self) -> None:
        backend = FakeStorageBackend()
        spec = PoolSpec(name="extra", pool_type="dir", target_path="/tmp/p")
        backend.create_pool(spec)
        backend.create_pool(spec)  # idempotent

    def test_create_pool_conflict_on_different_spec(self) -> None:
        backend = FakeStorageBackend()
        backend.create_pool(PoolSpec(name="x", pool_type="dir", target_path="/tmp/p"))
        with pytest.raises(PoolExists):
            backend.create_pool(PoolSpec(name="x", pool_type="lvm", target_path="/tmp/q"))

    def test_create_volume_returns_id(self) -> None:
        backend = FakeStorageBackend()
        v = backend.create_volume(VolumeSpec(pool="default", name="disk0", size_bytes=1024))
        assert isinstance(v, VolumeId)

    def test_create_volume_idempotent(self) -> None:
        backend = FakeStorageBackend()
        spec = VolumeSpec(pool="default", name="disk0", size_bytes=1024)
        v1 = backend.create_volume(spec)
        v2 = backend.create_volume(spec)
        assert v1 == v2

    def test_create_volume_conflict_different_size(self) -> None:
        backend = FakeStorageBackend()
        backend.create_volume(VolumeSpec(pool="default", name="disk0", size_bytes=1024))
        with pytest.raises(VolumeExists):
            backend.create_volume(VolumeSpec(pool="default", name="disk0", size_bytes=2048))

    def test_create_volume_unsupported_format(self) -> None:
        backend = FakeStorageBackend()
        with pytest.raises(FormatUnsupported):
            backend.create_volume(
                VolumeSpec(pool="default", name="x", size_bytes=1024, format="vhdx")
            )

    def test_create_volume_insufficient_capacity(self) -> None:
        backend = FakeStorageBackend(default_pool_capacity_bytes=1024)
        with pytest.raises(InsufficientCapacity):
            backend.create_volume(VolumeSpec(pool="default", name="big", size_bytes=2048))

    def test_volume_with_snapshots_refuses_delete(self) -> None:
        """contracts.md §4 — ref-counted GC."""
        backend = FakeStorageBackend()
        v = backend.create_volume(VolumeSpec(pool="default", name="disk0", size_bytes=1024))
        backend.snapshot_volume(v, "snap1")
        with pytest.raises(VolumeHasSnapshots):
            backend.delete_volume(v)

    def test_delete_volume_idempotent_when_missing(self) -> None:
        backend = FakeStorageBackend()
        backend.delete_volume(VolumeId("nope"))  # no raise

    def test_delete_volume_round_trip(self) -> None:
        backend = FakeStorageBackend()
        v = backend.create_volume(VolumeSpec(pool="default", name="disk0", size_bytes=1024))
        backend.delete_volume(v)
        with pytest.raises(VolumeNotFound):
            backend.get_volume(v)

    def test_pool_not_empty_refuses_delete(self) -> None:
        backend = FakeStorageBackend()
        backend.create_volume(VolumeSpec(pool="default", name="x", size_bytes=1024))
        with pytest.raises(PoolNotEmpty):
            backend.delete_pool("default")

    def test_pool_delete_force_cascades(self) -> None:
        backend = FakeStorageBackend()
        backend.create_volume(VolumeSpec(pool="default", name="x", size_bytes=1024))
        backend.delete_pool("default", force=True)
        with pytest.raises(PoolNotFound):
            backend.get_pool("default")

    def test_snapshot_volume_idempotent(self) -> None:
        backend = FakeStorageBackend()
        v = backend.create_volume(VolumeSpec(pool="default", name="x", size_bytes=1024))
        s1 = backend.snapshot_volume(v, "snap1")
        s2 = backend.snapshot_volume(v, "snap1")
        assert s1 == s2

    def test_capabilities_reports_fake(self) -> None:
        caps = FakeStorageBackend().capabilities()
        assert caps.engine == "fake"
        assert "qcow2" in caps.supports_formats


# ===========================================================================
# FakeDeviceBackend
# ===========================================================================


class TestDevice:
    def test_default_devices_assignable(self) -> None:
        backend = FakeDeviceBackend.with_default_devices()
        devs = backend.list_assignable()
        assert len(devs) == 3
        assert all(d.assignable for d in devs)

    def test_get_missing_device_raises(self) -> None:
        from mgf.vm.core.ids import DeviceId

        backend = FakeDeviceBackend.with_default_devices()
        with pytest.raises(Exception) as exc_info:
            backend.get_device(DeviceId("nope"))
        # DeviceNotFound is what should fire.
        from mgf.vm.core.exceptions import DeviceNotFound

        assert isinstance(exc_info.value, DeviceNotFound)

    def test_claim_returns_lease(self) -> None:
        backend = FakeDeviceBackend.with_default_devices()
        devs = backend.list_assignable()
        lease = backend.claim(devs[0].device_id)
        assert lease.value.startswith("fake-lease-")

    def test_claim_idempotent_same_client(self) -> None:
        """contracts.md §5 — same client, same device, same mode."""
        backend = FakeDeviceBackend.with_default_devices()
        devs = backend.list_assignable()
        l1 = backend.claim(devs[0].device_id)
        l2 = backend.claim(devs[0].device_id)
        assert l1 == l2

    def test_contention_between_clients(self) -> None:
        """Two clients race; one wins."""
        backend_a = FakeDeviceBackend.with_default_devices(claimant="client-a")
        FakeDeviceBackend.with_default_devices(claimant="client-b")
        # Same device ID exists in both fakes; in real broker, they'd
        # share state. To simulate contention, share the lease state.
        # Here we just verify the EXCEPTION path: claim from one client,
        # try to claim from another (we simulate by directly calling
        # the same backend with a different injected claimant).
        # The simpler path: same backend, different mode on same device.
        devs = backend_a.list_assignable()
        backend_a.claim(devs[0].device_id, mode=ClaimMode.EXCLUSIVE)
        with pytest.raises(DeviceContended):
            backend_a.claim(devs[0].device_id, mode=ClaimMode.SHARED)

    def test_assignment_for_returns_handle(self) -> None:
        backend = FakeDeviceBackend.with_default_devices()
        devs = backend.list_assignable()
        lease = backend.claim(devs[0].device_id)
        assignment = backend.assignment_for(lease)
        assert assignment.lease == lease
        assert assignment.host_handle != ""

    def test_release_round_trip(self) -> None:
        backend = FakeDeviceBackend.with_default_devices()
        devs = backend.list_assignable()
        lease = backend.claim(devs[0].device_id)
        backend.release(lease)
        # Device assignable again.
        assert any(
            d.device_id == devs[0].device_id and d.assignable for d in backend.list_assignable()
        )

    def test_release_idempotent(self) -> None:
        from mgf.vm.core.ids import LeaseId

        FakeDeviceBackend.with_default_devices().release(LeaseId("nope"))

    def test_fenced_lease_assignment_raises(self) -> None:
        """contracts.md §5 — liveness fence (CONF-D-04)."""
        backend = FakeDeviceBackend.with_default_devices()
        devs = backend.list_assignable()
        lease = backend.claim(devs[0].device_id)
        backend.fence_lease(lease)
        with pytest.raises(LeaseExpired):
            backend.assignment_for(lease)

    def test_transfer_lease_returns_new_id(self) -> None:
        backend = FakeDeviceBackend.with_default_devices()
        devs = backend.list_assignable()
        lease = backend.claim(devs[0].device_id)
        new_lease = backend.transfer_lease(lease, HostId("dest-host"))
        assert new_lease != lease
        # Old lease invalidated.
        with pytest.raises(LeaseNotFound):
            backend.assignment_for(lease)
        # New lease valid.
        backend.assignment_for(new_lease)

    def test_capabilities_reports_fake_hrb(self) -> None:
        caps = FakeDeviceBackend.with_default_devices().capabilities()
        assert caps.backend == "fake-hrb"
        assert caps.fenced_by_liveness is True
        assert caps.capability_scoped is True
