"""``VmSpec`` ↔ mapping serialization (D-G8 / D-G18).

The serde pairs with :func:`migrate_vmspec`: it produces the serialized
mapping that ``migrate_vmspec`` consumes, and re-reads it. These tests pin the
round-trip (including every neutral sub-type + enum), json-friendliness, and
the schema-version migration hook.
"""

from __future__ import annotations

import json

import mgf.vm.core as core
from mgf.vm.core.contracts import (
    ClaimMode,
    DeviceSpec,
    DiskSpec,
    NicSpec,
    VmSpec,
)
from mgf.vm.core.domain import IsolationLevel
from mgf.vm.core.provisioning.spec import ProvisioningSpec
from mgf.vm.core.serde import vmspec_from_mapping, vmspec_to_mapping


def _full_spec() -> VmSpec:
    return VmSpec(
        name="web",
        cpu=4,
        memory_bytes=2 << 30,
        disks=(
            DiskSpec(size_bytes=10 << 30, format="qcow2", source="/img/vda.qcow2"),
            DiskSpec(size_bytes=5 << 30, format="raw", source=None, readonly=True),
        ),
        nics=(NicSpec(network="default", model="e1000e", mac="52:54:00:aa:bb:cc"),),
        devices=(DeviceSpec(kind="pci", selector={"vendor": "10de"}, mode=ClaimMode.SHARED),),
        provisioning=ProvisioningSpec(
            backend="cloud-init",
            hostname="web-1",
            username="ops",
            ssh_authorized_keys=["ssh-ed25519 AAAA"],
            packages=["nginx"],
            run_commands=["systemctl enable nginx"],
        ),
        isolation=IsolationLevel.STRICT,
        metadata={"libvirt/firmware": "uefi"},
    )


def test_round_trip_preserves_full_spec() -> None:
    spec = _full_spec()
    restored = vmspec_from_mapping(vmspec_to_mapping(spec))
    assert restored == spec


def test_round_trip_minimal_spec() -> None:
    spec = VmSpec(name="m", cpu=1, memory_bytes=1 << 20)
    restored = vmspec_from_mapping(vmspec_to_mapping(spec))
    assert restored == spec
    assert restored.isolation is IsolationLevel.OFF
    assert restored.provisioning is None
    assert restored.disks == ()


def test_mapping_is_json_serializable() -> None:
    mapping = vmspec_to_mapping(_full_spec())
    # No exotic types leak (MappingProxyType / Enum instances) — pure json.
    text = json.dumps(mapping)
    assert vmspec_from_mapping(json.loads(text)) == _full_spec()


def test_enums_serialize_as_string_values() -> None:
    mapping = vmspec_to_mapping(_full_spec())
    assert mapping["isolation"] == IsolationLevel.STRICT.value
    assert mapping["devices"][0]["mode"] == ClaimMode.SHARED.value
    assert isinstance(mapping["metadata"], dict)


def test_restored_metadata_is_immutable() -> None:
    restored = vmspec_from_mapping(vmspec_to_mapping(_full_spec()))
    # The reconstructed metadata is a read-only mapping (frozen-spec hygiene).
    import pytest

    with pytest.raises(TypeError):
        restored.metadata["x"] = "y"  # type: ignore[index]


def test_from_mapping_fills_defaults_for_missing_optional_keys() -> None:
    # An additive-minimal mapping (D-D6): only the required fields present.
    spec = vmspec_from_mapping({"name": "x", "cpu": 2, "memory_bytes": 1024})
    assert spec == VmSpec(name="x", cpu=2, memory_bytes=1024)


def test_from_mapping_runs_through_migrate(monkeypatch) -> None:
    # Prove the migration hook is on the load path: a spec at an unknown old
    # version is routed through migrate_vmspec before reconstruction.
    seen: dict[str, int] = {}
    real = core.serde.migrate_vmspec

    def spy(raw, from_version):  # type: ignore[no-untyped-def]
        seen["from_version"] = from_version
        return real(raw, from_version)

    monkeypatch.setattr(core.serde, "migrate_vmspec", spy)
    vmspec_from_mapping({"name": "x", "cpu": 1, "memory_bytes": 1024, "schema_version": 1})
    assert seen["from_version"] == 1


def test_exported_from_package() -> None:
    assert core.vmspec_to_mapping is vmspec_to_mapping
    assert core.vmspec_from_mapping is vmspec_from_mapping
