"""Wave-R4 unit tests — backup / clone / restore through the SDK (D-G18).

Drives ``VmManagerClient`` over ``InProcessTransport`` pointed at the core
fakes (``FakeHypervisorBackend`` + ``FakeHostDiskBackend``). Covers the
engine-neutral orchestration: clone copies/overlays disk sources and defines a
new VM from the canonical spec (no native rewrite); backup carries the
serialized spec so restore reconstructs the VM self-contained; the typed-error
contract (no backend → ``TransportError``; absent source → ``DomainNotFound``).
"""

from __future__ import annotations

from dataclasses import replace

import pytest
from mgf.vm.core.contracts import BackupManifest, DiskSpec, DiskStatus, Vm, VmSpec
from mgf.vm.core.exceptions import DomainNotFound, TransportError
from mgf.vm.core.fakes.disk import FakeHostDiskBackend
from mgf.vm.core.fakes.hypervisor import FakeHypervisorBackend
from mgf.vm.core.ids import DomainId, VmId

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


class _ObservingHypervisor(FakeHypervisorBackend):
    """Fake whose ``get_domain`` surfaces observed disks from the spec.

    Mirrors the real adapter (and the app's test fake): ``DiskStatus`` carries
    the source path. Lets us exercise the orchestration's observed-disk
    fallback (a VM the transport never remembered the spec for).
    """

    def get_domain(self, d: DomainId) -> object:  # type: ignore[override]
        info = super().get_domain(d)
        record = self._require(d)  # type: ignore[attr-defined]
        disks = tuple(
            DiskStatus(
                target=f"vd{chr(ord('a') + i)}",
                source=ds.source,
                size_bytes=ds.size_bytes,
                readonly=ds.readonly,
            )
            for i, ds in enumerate(record.spec.disks)
        )
        return replace(info, disks=disks)


_VDA = "/img/web-vda.qcow2"
_VDB = "/img/web-vdb.raw"


def _disk_backend() -> FakeHostDiskBackend:
    backend = FakeHostDiskBackend()
    backend._put_image(_VDA, content=b"root", fmt="qcow2")
    backend._put_image(_VDB, content=b"data", fmt="raw")
    return backend


def _client(
    *, host_disk: FakeHostDiskBackend | None = None
) -> tuple[VmManagerClient, FakeHostDiskBackend | None]:
    hv = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(hv, host_disk=host_disk)
    return VmManagerClient(transport), host_disk


def _spec(name: str = "web") -> VmSpec:
    return VmSpec(
        name=name,
        cpu=2,
        memory_bytes=2 << 30,
        disks=(
            DiskSpec(size_bytes=10 << 30, format="qcow2", source=_VDA),
            DiskSpec(size_bytes=5 << 30, format="raw", source=_VDB),
        ),
    )


def _make_source_vm(client: VmManagerClient) -> VmId:
    return client.create_vm(_spec()).vm_id


# -- clone ------------------------------------------------------------------


def test_clone_full_copies_disks_and_defines_new_vm() -> None:
    backend = _disk_backend()
    client, _ = _client(host_disk=backend)
    src = _make_source_vm(client)

    clone = client.clone_vm(src, "web-copy", dest_dir="/clones")

    assert isinstance(clone, Vm)
    assert clone.spec.name == "web-copy"
    # Disk sources repointed to the copied images (engine-neutral; no rewrite).
    assert [d.source for d in clone.spec.disks] == [
        "/clones/web-copy-0.qcow2",
        "/clones/web-copy-1.raw",
    ]
    # The copies are real images in the backend store.
    assert backend._has_image("/clones/web-copy-0.qcow2")
    assert backend._has_image("/clones/web-copy-1.raw")


def test_clone_linked_creates_overlays_backed_by_source() -> None:
    backend = _disk_backend()
    client, _ = _client(host_disk=backend)
    src = _make_source_vm(client)

    clone = client.clone_vm(src, "web-linked", dest_dir="/clones", linked=True)

    overlay0 = clone.spec.disks[0].source
    assert overlay0 == "/clones/web-linked-0.qcow2"
    assert backend.image_info(overlay0).backing_file == _VDA


def test_clone_passes_through_disks_without_a_source() -> None:
    backend = FakeHostDiskBackend()
    client, _ = _client(host_disk=backend)
    # A spec whose single disk is sourceless (a blank volume).
    blank = VmSpec(
        name="blank",
        cpu=1,
        memory_bytes=1 << 30,
        disks=(DiskSpec(size_bytes=1 << 30, source=None),),
    )
    src = client.create_vm(blank).vm_id
    clone = client.clone_vm(src, "blank-copy", dest_dir="/clones")
    assert clone.spec.disks[0].source is None


def test_clone_without_backend_raises_transport_error() -> None:
    client, _ = _client(host_disk=None)
    src = _make_source_vm(client)
    with pytest.raises(TransportError, match="HostDiskBackend"):
        client.clone_vm(src, "x", dest_dir="/clones")


def test_clone_absent_source_raises_domain_not_found() -> None:
    backend = _disk_backend()
    client, _ = _client(host_disk=backend)
    with pytest.raises(DomainNotFound):
        client.clone_vm(VmId("ghost"), "x", dest_dir="/clones")


# -- backup + restore -------------------------------------------------------


def test_backup_returns_manifest_with_checksummed_disks() -> None:
    backend = _disk_backend()
    client, _ = _client(host_disk=backend)
    src = _make_source_vm(client)

    manifest = client.backup_vm(src, "/backups/web.tar")

    assert isinstance(manifest, BackupManifest)
    assert manifest.vm_name == "web"
    assert manifest.archive_path == "/backups/web.tar"
    assert {d.target for d in manifest.disks} == {"disk0", "disk1"}
    assert all(d.sha256 for d in manifest.disks)


def test_backup_restore_round_trip_reconstructs_vm() -> None:
    backend = _disk_backend()
    client, _ = _client(host_disk=backend)
    src = _make_source_vm(client)

    client.backup_vm(src, "/backups/web.tar")
    restored = client.restore_vm("/backups/web.tar", dest_dir="/restore", new_name="web-restored")

    assert restored.spec.name == "web-restored"
    # Spec round-trips (cpu/memory + disk shapes), sources repointed to extracts.
    assert restored.spec.cpu == 2
    assert restored.spec.memory_bytes == 2 << 30
    assert [d.source for d in restored.spec.disks] == [
        "/restore/disk0.qcow2",
        "/restore/disk1.raw",
    ]
    assert backend._has_image("/restore/disk0.qcow2")


def test_restore_defaults_to_backed_up_name() -> None:
    backend = _disk_backend()
    client, _ = _client(host_disk=backend)
    src = _make_source_vm(client)
    client.backup_vm(src, "/backups/web.tar")
    # Delete the source so the name is free, then restore without new_name.
    client.delete_vm(src)
    restored = client.restore_vm("/backups/web.tar", dest_dir="/restore")
    assert restored.spec.name == "web"


def test_restore_without_backend_raises_transport_error() -> None:
    client, _ = _client(host_disk=None)
    with pytest.raises(TransportError, match="HostDiskBackend"):
        client.restore_vm("/backups/web.tar", dest_dir="/restore")


def test_restore_archive_without_spec_metadata_raises() -> None:
    # An archive written WITHOUT the spec metadata (e.g. by another tool):
    # the transport refuses to restore rather than guess a spec.
    backend = FakeHostDiskBackend()
    backend._put_image("/img/x.qcow2", content=b"x")
    backend.archive("vm", {"disk0": "/img/x.qcow2"}, "/backups/bare.tar")
    client, _ = _client(host_disk=backend)
    with pytest.raises(TransportError, match="no VmSpec metadata"):
        client.restore_vm("/backups/bare.tar", dest_dir="/restore")


def test_backup_without_backend_raises_transport_error() -> None:
    client, _ = _client(host_disk=None)
    src = _make_source_vm(client)
    with pytest.raises(TransportError, match="HostDiskBackend"):
        client.backup_vm(src, "/backups/web.tar")


# -- observed-disk fallback (spec not remembered by this transport) ---------


def test_clone_falls_back_to_observed_disks() -> None:
    # A VM defined directly on the backend (the transport never remembered its
    # spec) — as happens for any pre-existing VM a fresh process operates on.
    # The orchestration must resolve disk sources from the OBSERVED disks.
    backend = _disk_backend()
    hv = _ObservingHypervisor()
    domain_id = hv.define(_spec())  # define directly: transport doesn't remember
    client = VmManagerClient(InProcessTransport.from_adapters(hv, host_disk=backend))

    clone = client.clone_vm(VmId(domain_id.value), "web-copy", dest_dir="/clones")

    assert [d.source for d in clone.spec.disks] == [
        "/clones/web-copy-0.qcow2",
        "/clones/web-copy-1.raw",
    ]
    assert backend._has_image("/clones/web-copy-0.qcow2")


def test_backup_restore_round_trip_with_observed_disks() -> None:
    backend = _disk_backend()
    hv = _ObservingHypervisor()
    domain_id = hv.define(_spec())
    client = VmManagerClient(InProcessTransport.from_adapters(hv, host_disk=backend))
    vm_id = VmId(domain_id.value)

    client.backup_vm(vm_id, "/backups/web.tar")
    restored = client.restore_vm("/backups/web.tar", dest_dir="/restore", new_name="web-restored")
    assert restored.spec.name == "web-restored"
    assert [d.source for d in restored.spec.disks] == [
        "/restore/disk0.qcow2",
        "/restore/disk1.raw",
    ]
