"""Tests for :mod:`mgf.vm.core.configurator.storage` (pure planners)."""

from __future__ import annotations

import xml.etree.ElementTree as ET

import pytest

from mgf.vm.core.configurator import storage
from mgf.vm.core.exceptions import SpecValidationError


def _dom(targets: tuple[str, ...] = ("vda",)) -> ET.Element:
    disks = "".join(
        f"<disk type='file' device='disk'>"
        f"<driver name='qemu' type='qcow2'/>"
        f"<source file='/var/lib/libvirt/images/{t}.qcow2'/>"
        f"<target dev='{t}' bus='virtio'/>"
        f"</disk>"
        for t in targets
    )
    return ET.fromstring(
        f"<domain><name>web</name><uuid>u</uuid><devices>{disks}</devices></domain>"
    )


class TestPlanAdd:
    def test_happy_path(self) -> None:
        change = storage.plan_add(
            _dom(),
            target="vdb",
            source="/var/lib/libvirt/images/data.qcow2",
        )
        assert "disk vdb added" in change.summary
        assert "data.qcow2" in change.new_xml
        assert change.requires_shutdown is False

    def test_duplicate_target_rejected(self) -> None:
        with pytest.raises(SpecValidationError, match="already exists"):
            storage.plan_add(_dom(), target="vda", source="/x.qcow2")

    def test_invalid_target_rejected(self) -> None:
        with pytest.raises(SpecValidationError, match="invalid disk target"):
            storage.plan_add(_dom(), target="", source="/x.qcow2")

    def test_invalid_target_with_space_rejected(self) -> None:
        with pytest.raises(SpecValidationError, match="invalid disk target"):
            storage.plan_add(_dom(), target="vd a", source="/x.qcow2")

    def test_readonly_disk(self) -> None:
        change = storage.plan_add(_dom(), target="vdb", source="/x.qcow2", readonly=True)
        assert "<readonly" in change.new_xml

    def test_add_without_devices_element_creates_it(self) -> None:
        dom = ET.fromstring("<domain><name>web</name></domain>")
        change = storage.plan_add(dom, target="vda", source="/x.qcow2")
        assert "<devices>" in change.new_xml


class TestPlanRemove:
    def test_happy_path(self) -> None:
        change = storage.plan_remove(_dom(("vda", "vdb")), target="vdb")
        assert "vdb removed" in change.summary
        assert "vda" in change.new_xml
        assert "vdb" not in change.new_xml

    def test_missing_target(self) -> None:
        with pytest.raises(SpecValidationError, match="no disk with target"):
            storage.plan_remove(_dom(), target="vdz")

    def test_no_devices_element(self) -> None:
        dom = ET.fromstring("<domain><name>web</name></domain>")
        with pytest.raises(SpecValidationError, match="no <devices>"):
            storage.plan_remove(dom, target="vda")


class TestPlanResize:
    def test_happy_path(self) -> None:
        change = storage.plan_resize(_dom(), target="vda")
        assert "vda resized" in change.summary
        assert change.requires_shutdown is False

    def test_missing_target(self) -> None:
        with pytest.raises(SpecValidationError):
            storage.plan_resize(_dom(), target="vdz")

    def test_no_devices_element(self) -> None:
        dom = ET.fromstring("<domain><name>web</name></domain>")
        with pytest.raises(SpecValidationError, match="no <devices>"):
            storage.plan_resize(dom, target="vda")


class TestTargetless:
    def test_disk_without_target_is_skipped(self) -> None:
        # A <disk> with no <target> child has a None target; it must be
        # ignored by the target-match scan rather than crashing.
        dom = ET.fromstring(
            "<domain><name>web</name><uuid>u</uuid><devices>"
            "<disk type='file'><source file='/x.qcow2'/></disk>"
            "</devices></domain>"
        )
        with pytest.raises(SpecValidationError, match="no disk with target"):
            storage.plan_remove(dom, target="vda")
