"""B4 unit tests — storage volume + pool CRUD through the SDK.

Drives ``VmManagerClient`` over ``InProcessTransport`` pointed at
``FakeStorageBackend``. Covers volume create/resize/delete/convert, pool
create/delete, the reads + capabilities, idempotency, and the reference-counted
GC contract (``VolumeHasSnapshots`` / ``PoolNotEmpty``; ``contracts.md`` §4).
"""

from __future__ import annotations

import pytest
from mgf.vm.core.contracts import PoolSpec, VolumeSpec
from mgf.vm.core.exceptions import (
    PoolNotEmpty,
    PoolNotFound,
    TransportError,
    VolumeHasSnapshots,
    VolumeNotFound,
)
from mgf.vm.core.fakes.hypervisor import FakeHypervisorBackend
from mgf.vm.core.fakes.storage import FakeStorageBackend
from mgf.vm.core.ids import VolumeId

from mgf.vm.sdk.client import VmManagerClient
from mgf.vm.sdk.transport.inprocess import InProcessTransport


def _client() -> VmManagerClient:
    return VmManagerClient(
        InProcessTransport.from_adapters(FakeHypervisorBackend(), FakeStorageBackend())
    )


def _vol_spec(name: str = "v1", pool: str = "default", size: int = 1 << 30) -> VolumeSpec:
    return VolumeSpec(pool=pool, name=name, size_bytes=size)


# -- volumes ---------------------------------------------------------------


def test_create_get_list_delete_volume_round_trip() -> None:
    client = _client()
    vol = client.create_volume(_vol_spec("v1"))
    assert isinstance(vol, VolumeId)

    got = client.get_volume(vol)
    assert got.name == "v1"
    assert got.size_bytes == 1 << 30

    assert [v.name for v in client.list_volumes("default")] == ["v1"]

    client.delete_volume(vol)
    with pytest.raises(VolumeNotFound):
        client.get_volume(vol)


def test_create_volume_idempotent_same_spec() -> None:
    client = _client()
    first = client.create_volume(_vol_spec("v1"))
    again = client.create_volume(_vol_spec("v1"))
    assert first == again
    assert len(client.list_volumes("default")) == 1


def test_resize_volume() -> None:
    client = _client()
    vol = client.create_volume(_vol_spec("v1", size=1 << 30))
    client.resize_volume(vol, 2 << 30)
    assert client.get_volume(vol).size_bytes == 2 << 30


def test_delete_volume_idempotent() -> None:
    client = _client()
    # Already-gone delete is success (reconciler GC contract, §4).
    client.delete_volume(VolumeId("never-existed"))


def test_delete_volume_with_snapshots_refuses() -> None:
    client = _client()
    vol = client.create_volume(_vol_spec("v1"))
    client.snapshot_volume(vol, "s1")
    with pytest.raises(VolumeHasSnapshots):
        client.delete_volume(vol)


def test_convert_format_returns_new_volume() -> None:
    client = _client()
    src = client.create_volume(_vol_spec("src", size=1 << 30))
    dst_spec = VolumeSpec(pool="default", name="dst", size_bytes=1 << 30, format="raw")
    dst = client.convert_format(src, "raw", dst_spec)
    assert isinstance(dst, VolumeId)
    assert dst != src
    assert client.get_volume(dst).format == "raw"


# -- pools -----------------------------------------------------------------


def test_create_get_list_delete_pool_round_trip() -> None:
    client = _client()
    client.create_pool(PoolSpec(name="p1", pool_type="dir", target_path="/p1"))
    assert client.get_pool("p1").name == "p1"
    assert "p1" in {p.name for p in client.list_pools()}

    client.delete_pool("p1")
    with pytest.raises(PoolNotFound):
        client.get_pool("p1")


def test_pool_reports_available_and_active() -> None:
    pool = _client().get_pool("default")
    assert pool.available_bytes == pool.capacity_bytes - pool.allocation_bytes
    assert pool.active is True


def test_refresh_pool_returns_fresh_info() -> None:
    client = _client()
    assert client.refresh_pool("default") == client.get_pool("default")
    with pytest.raises(PoolNotFound):
        client.refresh_pool("ghost")


def test_volume_reports_path_and_allocation() -> None:
    client = _client()
    client.create_volume(_vol_spec("v1", pool="default"))
    vol = next(v for v in client.list_volumes("default") if v.name == "v1")
    assert vol.path.endswith("/v1")
    assert vol.allocation_bytes == vol.size_bytes


def test_delete_nonempty_pool_refuses_without_force() -> None:
    client = _client()
    client.create_pool(PoolSpec(name="p1", pool_type="dir", target_path="/p1"))
    client.create_volume(_vol_spec("v1", pool="p1"))
    with pytest.raises(PoolNotEmpty):
        client.delete_pool("p1")
    # force cascades.
    client.delete_pool("p1", force=True)
    with pytest.raises(PoolNotFound):
        client.get_pool("p1")


def test_create_volume_in_missing_pool_raises() -> None:
    client = _client()
    with pytest.raises(PoolNotFound):
        client.create_volume(_vol_spec("v1", pool="ghost"))


# -- capabilities + missing-backend ----------------------------------------


def test_storage_capabilities() -> None:
    client = _client()
    caps = client.storage_capabilities()
    assert caps.engine == "fake"
    assert "qcow2" in caps.supports_formats


def test_storage_op_without_backend_fails() -> None:
    # Hypervisor-only transport → storage ops surface a typed TransportError.
    client = VmManagerClient(InProcessTransport.from_adapters(FakeHypervisorBackend()))
    with pytest.raises(TransportError):
        client.create_volume(_vol_spec("v1"))
    with pytest.raises(TransportError):
        client.list_pools()
