"""S1-S5 — unit tests for ``LibvirtStorageBackend`` against a fake virConnect.

No real libvirtd; no real qemu-img. The fake models pools + volumes as
in-memory objects exposing the subset of the libvirt-python API the backend
touches, and ``qemu-img`` is monkeypatched so snapshot/convert paths are
driven deterministically. Conformance against the live engine lives in
``tests/conformance/``.
"""

from __future__ import annotations

import threading
from typing import TYPE_CHECKING, Any

import pytest
from mgf.vm.core.capabilities import StorageCapabilities
from mgf.vm.core.contracts import PoolSpec, VolumeSpec
from mgf.vm.core.exceptions import (
    FormatUnsupported,
    HypervisorConnectionError,
    PoolExists,
    PoolNotEmpty,
    PoolNotFound,
    ResizeUnsupported,
    SnapshotHasChildren,
    SnapshotNotFound,
    StorageError,
    VolumeExists,
    VolumeHasSnapshots,
    VolumeInUse,
    VolumeNotFound,
)

# Use the project's tier-1 libvirtError double.
from tests.conftest import FakeLibvirtError

from mgf.vm.libvirt._connection import LibvirtConnection
from mgf.vm.libvirt._storage import LibvirtStorageBackend
from mgf.vm.libvirt._volume_ids import encode_snapshot_id, encode_volume_id

if TYPE_CHECKING:
    from collections.abc import Iterator

_GIB = 1 << 30


# ---------------------------------------------------------------------------
# Fake libvirt object model
# ---------------------------------------------------------------------------


class FakeVolume:
    def __init__(self, name: str, capacity: int, fmt: str, path: str) -> None:
        self._name = name
        self._capacity = capacity
        self._format = fmt
        self._path = path
        self.deleted = False

    def name(self) -> str:
        return self._name

    def path(self) -> str:
        return self._path

    def info(self) -> list[int]:
        # [type, capacity, allocation]
        return [0, self._capacity, self._capacity]

    def resize(self, capacity: int, flags: int) -> int:
        self._capacity = capacity
        return 0

    def delete(self, flags: int) -> int:
        self.deleted = True
        return 0

    def XMLDesc(self, flags: int = 0) -> str:  # noqa: N802 - libvirt naming
        return (
            f"<volume><name>{self._name}</name>"
            f"<target><format type='{self._format}'/></target></volume>"
        )


class FakePool:
    def __init__(self, name: str, pool_type: str, path: str, *, active: bool = True) -> None:
        self._name = name
        self._type = pool_type
        self._path = path
        self._active = active
        self._defined = True
        self._volumes: dict[str, FakeVolume] = {}
        self.refreshed = 0

    def name(self) -> str:
        return self._name

    def isActive(self) -> bool:  # noqa: N802
        return self._active

    def info(self) -> list[int]:
        # [state, capacity, allocation, available]
        return [1, 100 * _GIB, 10 * _GIB, 90 * _GIB]

    def XMLDesc(self, flags: int = 0) -> str:  # noqa: N802
        return (
            f"<pool type='{self._type}'><name>{self._name}</name>"
            f"<target><path>{self._path}</path></target></pool>"
        )

    def refresh(self, flags: int = 0) -> int:
        self.refreshed += 1
        return 0

    def build(self, flags: int) -> int:
        return 0

    def create(self, flags: int) -> int:
        self._active = True
        return 0

    def destroy(self) -> int:
        self._active = False
        return 0

    def undefine(self) -> int:
        self._defined = False
        return 0

    def listVolumes(self) -> list[str]:  # noqa: N802
        return [v.name() for v in self._volumes.values() if not v.deleted]

    def storageVolLookupByName(self, name: str) -> FakeVolume:  # noqa: N802
        vol = self._volumes.get(name)
        if vol is None or vol.deleted:
            raise FakeLibvirtError(50, f"no volume {name}")  # VIR_ERR_NO_STORAGE_VOL
        return vol

    def createXML(self, xml: str, flags: int) -> FakeVolume:  # noqa: N802
        from defusedxml import ElementTree as DefusedET

        root = DefusedET.fromstring(xml)
        name = root.findtext("name") or "unnamed"
        cap = int(root.findtext("capacity") or "0")
        fmt_el = root.find("target/format")
        fmt = fmt_el.get("type") if fmt_el is not None else "raw"
        vol = FakeVolume(name, cap, fmt or "raw", f"{self._path}/{name}")
        self._volumes[name] = vol
        return vol


class FakeDomain:
    def __init__(self, name: str, *, active: bool, disk_paths: tuple[str, ...]) -> None:
        self._name = name
        self._active = active
        self._disks = disk_paths

    def isActive(self) -> bool:  # noqa: N802
        return self._active

    def XMLDesc(self, flags: int = 0) -> str:  # noqa: N802
        disks = "".join(f"<disk type='file'><source file='{p}'/></disk>" for p in self._disks)
        return f"<domain><devices>{disks}</devices></domain>"


class FakeConnReal:
    """A virConnect double with a pool/volume/domain model."""

    def __init__(self) -> None:
        self._pools: dict[str, FakePool] = {}
        self._domains: list[FakeDomain] = []
        self.close_calls = 0

    # storage pools
    def listAllStoragePools(self, flags: int = 0) -> list[FakePool]:  # noqa: N802
        return [p for p in self._pools.values() if p._defined]

    def storagePoolLookupByName(self, name: str) -> FakePool:  # noqa: N802
        pool = self._pools.get(name)
        if pool is None or not pool._defined:
            raise FakeLibvirtError(49, f"no pool {name}")  # VIR_ERR_NO_STORAGE_POOL
        return pool

    def storagePoolDefineXML(self, xml: str, flags: int = 0) -> FakePool:  # noqa: N802
        from defusedxml import ElementTree as DefusedET

        root = DefusedET.fromstring(xml)
        name = root.findtext("name") or "unnamed"
        ptype = root.get("type") or "dir"
        path = root.findtext("target/path") or "/var/lib/libvirt/images"
        pool = FakePool(name, ptype, path, active=False)
        self._pools[name] = pool
        return pool

    # domains
    def listAllDomains(self, flags: int = 0) -> list[FakeDomain]:  # noqa: N802
        return list(self._domains)

    def getVersion(self) -> int:  # noqa: N802 - mirror libvirt naming
        return 8_002_000

    def close(self) -> int:
        self.close_calls += 1
        return 0

    # test helpers
    def add_pool(self, name: str, ptype: str = "dir", path: str = "/pools/p") -> FakePool:
        pool = FakePool(name, ptype, path, active=True)
        self._pools[name] = pool
        return pool

    def add_domain(self, name: str, *, active: bool, disk_paths: tuple[str, ...]) -> None:
        self._domains.append(FakeDomain(name, active=active, disk_paths=disk_paths))


# ---------------------------------------------------------------------------
# qemu-img fake
# ---------------------------------------------------------------------------


class FakeQemuImg:
    """Records qemu-img invocations and serves a per-path snapshot list."""

    def __init__(self) -> None:
        self.calls: list[list[str]] = []
        # path -> list of snapshot names (the qemu-img -l view)
        self.snapshots: dict[str, list[str]] = {}
        self.fail_on: set[str] = set()

    def run(self, args: list[str]) -> str:
        self.calls.append(args)
        sub = args[0]
        if sub in self.fail_on:
            msg = f"qemu-img {sub} forced failure"
            raise StorageError(msg)
        if sub == "snapshot":
            return self._snapshot(args)
        if sub == "convert":
            return ""
        return ""

    def _snapshot(self, args: list[str]) -> str:
        flag = args[1]
        if flag == "-l":
            path = args[2]
            names = self.snapshots.get(path, [])
            header = "Snapshot list:\nID        TAG\n"
            rows = "".join(f"{i + 1}         {n}\n" for i, n in enumerate(names))
            return header + rows
        name = args[2]
        path = args[3]
        lst = self.snapshots.setdefault(path, [])
        if flag == "-c":
            lst.append(name)
        elif flag == "-d" and name in lst:
            lst.remove(name)
        # -a (apply) is a no-op on the list
        return ""


@pytest.fixture
def conn() -> FakeConnReal:
    return FakeConnReal()


@pytest.fixture
def backend(conn: FakeConnReal, monkeypatch: pytest.MonkeyPatch) -> Iterator[LibvirtStorageBackend]:
    fake_qemu = FakeQemuImg()
    be = LibvirtStorageBackend.adopt(LibvirtConnection.adopt(conn, uri="test:///fake"))
    # Route qemu-img through the fake; pretend the binary exists.
    monkeypatch.setattr("mgf.vm.libvirt._storage.shutil.which", lambda _name: "/usr/bin/qemu-img")
    monkeypatch.setattr(be, "_qemu_img", fake_qemu.run)
    be._fake_qemu = fake_qemu  # type: ignore[attr-defined]
    yield be
    be.close()


# ---------------------------------------------------------------------------
# S1 — pools
# ---------------------------------------------------------------------------


def test_list_pools(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    pools = backend.list_pools()
    assert [p.name for p in pools] == ["default"]
    assert pools[0].pool_type == "dir"
    assert pools[0].target_path == "/pools/default"


def test_get_pool_found(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("p1")
    info = backend.get_pool("p1")
    assert info.name == "p1"
    # available = info()[3], active = isActive() (D-G20).
    assert info.available_bytes == 90 * _GIB
    assert info.active is True


def test_get_pool_not_found(backend: LibvirtStorageBackend) -> None:
    with pytest.raises(PoolNotFound):
        backend.get_pool("ghost")


def test_refresh_pool_rescans_and_returns_info(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    pool = conn.add_pool("p1")
    info = backend.refresh_pool("p1")
    assert info.name == "p1"
    assert pool.refreshed == 1  # the pool's backing store was rescanned


def test_refresh_pool_not_found(backend: LibvirtStorageBackend) -> None:
    with pytest.raises(PoolNotFound):
        backend.refresh_pool("ghost")


def test_create_pool_new(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    backend.create_pool(PoolSpec(name="np", pool_type="dir", target_path="/pools/np"))
    assert conn.storagePoolLookupByName("np").isActive()


def test_create_pool_idempotent_same_spec(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("p", "dir", "/pools/p")
    # Same spec → no error, ensured active.
    backend.create_pool(PoolSpec(name="p", pool_type="dir", target_path="/pools/p"))


def test_create_pool_conflict_diff_spec(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("p", "dir", "/pools/p")
    with pytest.raises(PoolExists):
        backend.create_pool(PoolSpec(name="p", pool_type="dir", target_path="/other"))


def test_create_pool_reactivates_inactive(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    pool = conn.add_pool("p", "dir", "/pools/p")
    pool._active = False
    backend.create_pool(PoolSpec(name="p", pool_type="dir", target_path="/pools/p"))
    assert pool.isActive()


def test_delete_pool_empty(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("p", "dir", "/pools/p")
    backend.delete_pool("p")
    with pytest.raises(PoolNotFound):
        backend.get_pool("p")


def test_delete_pool_missing_is_idempotent(backend: LibvirtStorageBackend) -> None:
    backend.delete_pool("ghost")  # no raise


def test_delete_pool_not_empty_refuses(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    pool = conn.add_pool("p", "dir", "/pools/p")
    pool.createXML(_vol_xml("v1"), 0)
    with pytest.raises(PoolNotEmpty):
        backend.delete_pool("p")


def test_delete_pool_force_cascades(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    pool = conn.add_pool("p", "dir", "/pools/p")
    pool.createXML(_vol_xml("v1"), 0)
    backend.delete_pool("p", force=True)
    with pytest.raises(PoolNotFound):
        backend.get_pool("p")


# ---------------------------------------------------------------------------
# S2 — volume CRUD
# ---------------------------------------------------------------------------


def _vol_xml(name: str, cap: int = 16 * 1024 * 1024, fmt: str = "qcow2") -> str:
    return (
        f"<volume><name>{name}</name><capacity>{cap}</capacity>"
        f"<target><format type='{fmt}'/></target></volume>"
    )


def _vol_path(conn: FakeConnReal, vid: Any) -> str:
    """The host path the fake assigned a volume (for in-use disk wiring)."""
    from mgf.vm.libvirt._volume_ids import decode_volume_id

    ref = decode_volume_id(vid)
    return conn.storagePoolLookupByName(ref.pool).storageVolLookupByName(ref.name).path()


def test_create_volume_new(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=16 * 1024 * 1024))
    assert vid == encode_volume_id("default", "v1")
    listed = [info.volume_id for info in backend.list_volumes("default")]
    assert vid in listed


def test_create_volume_idempotent_same_spec(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    spec = VolumeSpec(pool="default", name="v1", size_bytes=16 * 1024 * 1024)
    a = backend.create_volume(spec)
    b = backend.create_volume(spec)
    assert a == b


def test_create_volume_conflict_diff_spec(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=16 * 1024 * 1024))
    with pytest.raises(VolumeExists):
        backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=32 * 1024 * 1024))


def test_create_volume_unknown_pool(backend: LibvirtStorageBackend) -> None:
    with pytest.raises(PoolNotFound):
        backend.create_volume(VolumeSpec(pool="ghost", name="v", size_bytes=_GIB))


def test_create_volume_unsupported_format(backend: LibvirtStorageBackend) -> None:
    with pytest.raises(FormatUnsupported):
        backend.create_volume(VolumeSpec(pool="default", name="v", size_bytes=_GIB, format="vmdk"))


def test_get_volume_found(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    info = backend.get_volume(vid)
    assert info.name == "v1"
    assert info.format == "qcow2"
    assert info.size_bytes == _GIB
    # path = vol.path(), allocation = info()[2] (D-G20).
    assert info.path
    assert info.allocation_bytes == _GIB


def test_get_volume_not_found(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    with pytest.raises(VolumeNotFound):
        backend.get_volume(encode_volume_id("default", "ghost"))


def test_resize_volume_grow(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    backend.resize_volume(vid, 2 * _GIB)
    assert backend.get_volume(vid).size_bytes == 2 * _GIB


def test_resize_volume_same_size_idempotent(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    backend.resize_volume(vid, _GIB)  # no raise, no change
    assert backend.get_volume(vid).size_bytes == _GIB


def test_resize_volume_shrink_refused(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=2 * _GIB))
    with pytest.raises(ResizeUnsupported):
        backend.resize_volume(vid, _GIB)


def test_resize_volume_not_found(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    with pytest.raises(VolumeNotFound):
        backend.resize_volume(encode_volume_id("default", "ghost"), _GIB)


def test_delete_volume(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    backend.delete_volume(vid)
    with pytest.raises(VolumeNotFound):
        backend.get_volume(vid)


def test_delete_volume_already_gone_idempotent(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    backend.delete_volume(encode_volume_id("default", "ghost"))  # no raise


def test_delete_volume_with_snapshots_refuses(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    backend.snapshot_volume(vid, "s1")
    with pytest.raises(VolumeHasSnapshots):
        backend.delete_volume(vid)


def test_delete_volume_in_use_refuses(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    path = _vol_path(conn, vid)
    conn.add_domain("running-vm", active=True, disk_paths=(path,))
    with pytest.raises(VolumeInUse):
        backend.delete_volume(vid)


def test_delete_volume_stopped_domain_attach_ok(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    path = _vol_path(conn, vid)
    # Inactive domain referencing the disk does NOT block delete.
    conn.add_domain("stopped-vm", active=False, disk_paths=(path,))
    backend.delete_volume(vid)
    with pytest.raises(VolumeNotFound):
        backend.get_volume(vid)


# ---------------------------------------------------------------------------
# S3 — volume snapshots
# ---------------------------------------------------------------------------


def test_snapshot_volume_and_list(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    sid = backend.snapshot_volume(vid, "s1")
    assert sid == encode_snapshot_id("default", "v1", "s1")
    snaps = backend.list_snapshots(vid)
    assert [s.name for s in snaps] == ["s1"]
    assert snaps[0].parent == vid


def test_snapshot_volume_idempotent_same_name(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    a = backend.snapshot_volume(vid, "s1")
    b = backend.snapshot_volume(vid, "s1")
    assert a == b
    assert len(backend.list_snapshots(vid)) == 1


def test_snapshot_volume_raw_format_refused(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(
        VolumeSpec(pool="default", name="v1", size_bytes=_GIB, format="raw")
    )
    with pytest.raises(FormatUnsupported):
        backend.snapshot_volume(vid, "s1")


def test_snapshot_volume_unknown_volume(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    with pytest.raises(VolumeNotFound):
        backend.snapshot_volume(encode_volume_id("default", "ghost"), "s1")


def test_list_snapshots_raw_returns_empty(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(
        VolumeSpec(pool="default", name="v1", size_bytes=_GIB, format="raw")
    )
    assert backend.list_snapshots(vid) == []


def test_delete_snapshot_leaf(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    sid = backend.snapshot_volume(vid, "s1")
    backend.delete_snapshot(sid)
    assert backend.list_snapshots(vid) == []


def test_delete_snapshot_with_children_refuses(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    sid_parent = backend.snapshot_volume(vid, "parent")
    backend.snapshot_volume(vid, "child")  # child parented at "parent"
    with pytest.raises(SnapshotHasChildren):
        backend.delete_snapshot(sid_parent)


def test_delete_snapshot_already_gone_idempotent(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    backend.snapshot_volume(vid, "s1")
    sid_ghost = encode_snapshot_id("default", "v1", "ghost")
    backend.delete_snapshot(sid_ghost)  # no raise


def test_delete_snapshot_volume_gone_idempotent(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    # Volume never existed → snapshot considered gone → success.
    backend.delete_snapshot(encode_snapshot_id("default", "ghost", "s1"))


def test_revert_volume_to_snapshot(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    backend.snapshot_volume(vid, "s1")
    sid2 = backend.snapshot_volume(vid, "s2")
    # Revert to s1 then confirm reverting to s1 again is a no-op (idempotent).
    sid1 = encode_snapshot_id("default", "v1", "s1")
    backend.revert_volume_to_snapshot(vid, sid1)
    backend.revert_volume_to_snapshot(vid, sid1)  # idempotent no-op
    # Reverting to s2 should apply.
    backend.revert_volume_to_snapshot(vid, sid2)


def test_revert_unknown_snapshot(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    with pytest.raises(SnapshotNotFound):
        backend.revert_volume_to_snapshot(vid, encode_snapshot_id("default", "v1", "ghost"))


def test_revert_snapshot_wrong_volume(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    other_sid = encode_snapshot_id("default", "other", "s1")
    with pytest.raises(SnapshotNotFound):
        backend.revert_volume_to_snapshot(vid, other_sid)


def test_revert_cancelled(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    from mgf.vm.core.cancellation import CancellationEvent
    from mgf.vm.core.exceptions import OperationCancelled

    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    sid = backend.snapshot_volume(vid, "s1")
    token = CancellationEvent()
    token.cancel()
    with pytest.raises(OperationCancelled):
        backend.revert_volume_to_snapshot(vid, sid, token=token)


def test_revert_in_use_refuses(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    vid = backend.create_volume(VolumeSpec(pool="default", name="v1", size_bytes=_GIB))
    backend.snapshot_volume(vid, "s1")
    sid2 = backend.snapshot_volume(vid, "s2")
    sid1 = encode_snapshot_id("default", "v1", "s1")
    _ = sid2
    path = _vol_path(conn, vid)
    conn.add_domain("running", active=True, disk_paths=(path,))
    with pytest.raises(VolumeInUse):
        backend.revert_volume_to_snapshot(vid, sid1)


# ---------------------------------------------------------------------------
# S4 — convert_format
# ---------------------------------------------------------------------------


def test_convert_format_new_volume(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    src = backend.create_volume(VolumeSpec(pool="default", name="src", size_bytes=_GIB))
    dst_spec = VolumeSpec(pool="default", name="dst", size_bytes=_GIB, format="raw")
    dst = backend.convert_format(src, "raw", dst_spec)
    assert dst == encode_volume_id("default", "dst")
    assert backend.get_volume(dst).format == "raw"


def test_convert_format_idempotent_on_dst(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    src = backend.create_volume(VolumeSpec(pool="default", name="src", size_bytes=_GIB))
    dst_spec = VolumeSpec(pool="default", name="dst", size_bytes=_GIB, format="raw")
    a = backend.convert_format(src, "raw", dst_spec)
    b = backend.convert_format(src, "raw", dst_spec)
    assert a == b


def test_convert_format_collision_diff_spec(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    src = backend.create_volume(VolumeSpec(pool="default", name="src", size_bytes=_GIB))
    backend.create_volume(VolumeSpec(pool="default", name="dst", size_bytes=2 * _GIB))
    dst_spec = VolumeSpec(pool="default", name="dst", size_bytes=_GIB, format="raw")
    with pytest.raises(VolumeExists):
        backend.convert_format(src, "raw", dst_spec)


def test_convert_format_unsupported(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    src = backend.create_volume(VolumeSpec(pool="default", name="src", size_bytes=_GIB))
    with pytest.raises(FormatUnsupported):
        backend.convert_format(
            src, "qed", VolumeSpec(pool="default", name="dst", size_bytes=_GIB, format="qed")
        )


def test_convert_format_spec_format_mismatch(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    src = backend.create_volume(VolumeSpec(pool="default", name="src", size_bytes=_GIB))
    with pytest.raises(FormatUnsupported):
        backend.convert_format(
            src,
            "raw",
            VolumeSpec(pool="default", name="dst", size_bytes=_GIB, format="qcow2"),
        )


def test_convert_format_rollback_on_qemu_failure(
    backend: LibvirtStorageBackend, conn: FakeConnReal
) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    src = backend.create_volume(VolumeSpec(pool="default", name="src", size_bytes=_GIB))
    backend._fake_qemu.fail_on.add("convert")  # type: ignore[attr-defined]
    dst_spec = VolumeSpec(pool="default", name="dst", size_bytes=_GIB, format="raw")
    with pytest.raises(StorageError):
        backend.convert_format(src, "raw", dst_spec)
    # The half-written destination must have been rolled back.
    with pytest.raises(VolumeNotFound):
        backend.get_volume(encode_volume_id("default", "dst"))


def test_convert_cancelled_before_start(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    from mgf.vm.core.cancellation import CancellationEvent
    from mgf.vm.core.exceptions import OperationCancelled

    conn.add_pool("default", "dir", "/pools/default")
    src = backend.create_volume(VolumeSpec(pool="default", name="src", size_bytes=_GIB))
    token = CancellationEvent()
    token.cancel()
    with pytest.raises(OperationCancelled):
        backend.convert_format(
            src,
            "raw",
            VolumeSpec(pool="default", name="dst", size_bytes=_GIB, format="raw"),
            token=token,
        )


# ---------------------------------------------------------------------------
# S5 — capabilities
# ---------------------------------------------------------------------------


def test_capabilities(backend: LibvirtStorageBackend) -> None:
    caps = backend.capabilities()
    assert isinstance(caps, StorageCapabilities)
    assert caps.engine == "libvirt-qemu"
    assert "qcow2" in caps.supports_formats
    assert "raw" in caps.supports_formats
    assert caps.supports_shrink is False
    assert "dir" in caps.supports_pool_types


def test_capabilities_version_parsed(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    caps = backend.capabilities()
    assert caps.engine_version == "8.2.0"


# ---------------------------------------------------------------------------
# Lifecycle + threading + error translation
# ---------------------------------------------------------------------------


def test_close_idempotent(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    backend.close()
    backend.close()
    assert conn.close_calls == 1
    assert backend.closed


def test_context_manager_closes(conn: FakeConnReal) -> None:
    with LibvirtStorageBackend.adopt(LibvirtConnection.adopt(conn)) as be:
        assert not be.closed
    assert conn.close_calls == 1


def test_call_after_close_raises_connection_error(
    backend: LibvirtStorageBackend,
) -> None:
    backend.close()
    with pytest.raises(HypervisorConnectionError):
        backend.list_pools()


def test_libvirt_error_translated_at_seam(
    backend: LibvirtStorageBackend, conn: FakeConnReal, monkeypatch: pytest.MonkeyPatch
) -> None:
    # listAllStoragePools raising a libvirtError must surface as a typed
    # StorageError-family / Hypervisor error, never the raw FakeLibvirtError.
    def boom(*_a: Any, **_k: Any) -> Any:
        raise FakeLibvirtError(49)  # VIR_ERR_NO_STORAGE_POOL → PoolNotFound

    monkeypatch.setattr(conn, "listAllStoragePools", boom)
    with pytest.raises(PoolNotFound):
        backend.list_pools()


def test_open_classmethod(monkeypatch: pytest.MonkeyPatch) -> None:
    fake = FakeConnReal()
    monkeypatch.setattr("mgf.vm.libvirt._connection._open_libvirt", lambda _uri: fake)
    be = LibvirtStorageBackend.open("test:///fake")
    assert not be.closed
    be.close()


def test_concurrent_calls_no_corruption(backend: LibvirtStorageBackend, conn: FakeConnReal) -> None:
    conn.add_pool("default", "dir", "/pools/default")
    errors: list[BaseException] = []

    def worker(i: int) -> None:
        try:
            spec = VolumeSpec(pool="default", name=f"v{i}", size_bytes=_GIB)
            vid = backend.create_volume(spec)
            backend.get_volume(vid)
        except BaseException as exc:
            errors.append(exc)

    threads = [threading.Thread(target=worker, args=(i,)) for i in range(12)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    assert not errors
    assert len(backend.list_volumes("default")) == 12
