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

from __future__ import annotations

import xml.etree.ElementTree as ET

import pytest

from mgf.vm.core.configurator import passthrough as pt
from mgf.vm.core.exceptions import SpecValidationError


def _dom() -> ET.Element:
    return ET.fromstring("<domain><name>web</name><uuid>u</uuid><devices/></domain>")


def _dom_with_pci() -> ET.Element:
    return ET.fromstring(
        "<domain><name>web</name><uuid>u</uuid><devices>"
        "<hostdev mode='subsystem' type='pci' managed='yes'>"
        "<source><address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/></source>"
        "</hostdev>"
        "</devices></domain>"
    )


def _dom_with_usb() -> ET.Element:
    return ET.fromstring(
        "<domain><name>web</name><uuid>u</uuid><devices>"
        "<hostdev mode='subsystem' type='usb'>"
        "<source><vendor id='0x046d'/><product id='0xc534'/></source>"
        "</hostdev>"
        "</devices></domain>"
    )


class TestPCI:
    def test_add_happy_path(self) -> None:
        change = pt.plan_add_pci(_dom(), domain_addr=0, bus=1, slot=0, function=0)
        assert change.requires_shutdown is True
        assert "pci 0000:01:00.0 attached" in change.summary

    def test_add_duplicate(self) -> None:
        with pytest.raises(SpecValidationError, match="already attached"):
            pt.plan_add_pci(_dom_with_pci(), domain_addr=0, bus=1, slot=0, function=0)

    def test_remove_happy_path(self) -> None:
        change = pt.plan_remove_pci(_dom_with_pci(), domain_addr=0, bus=1, slot=0, function=0)
        assert "detached" in change.summary
        assert "hostdev" not in change.new_xml

    def test_remove_missing(self) -> None:
        with pytest.raises(SpecValidationError, match="not attached"):
            pt.plan_remove_pci(_dom(), domain_addr=0, bus=1, slot=0, function=0)

    def test_remove_no_devices_element(self) -> None:
        dom = ET.fromstring("<domain><name>web</name></domain>")
        with pytest.raises(SpecValidationError, match="no <devices>"):
            pt.plan_remove_pci(dom, domain_addr=0, bus=1, slot=0, function=0)

    def test_slot_out_of_range(self) -> None:
        with pytest.raises(SpecValidationError, match="out of range"):
            pt.plan_add_pci(_dom(), domain_addr=0, bus=0, slot=64, function=0)

    def test_bus_out_of_range(self) -> None:
        with pytest.raises(SpecValidationError, match="out of range"):
            pt.plan_add_pci(_dom(), domain_addr=0, bus=256, slot=0, function=0)

    def test_domain_out_of_range(self) -> None:
        with pytest.raises(SpecValidationError, match="out of range"):
            pt.plan_add_pci(_dom(), domain_addr=0x10000, bus=0, slot=0, function=0)

    def test_function_out_of_range(self) -> None:
        with pytest.raises(SpecValidationError, match="out of range"):
            pt.plan_add_pci(_dom(), domain_addr=0, bus=0, slot=0, function=8)


class TestUSB:
    def test_add_hotpluggable(self) -> None:
        change = pt.plan_add_usb(_dom(), vendor=0x046D, product=0xC534)
        assert "usb 046d:c534 attached" in change.summary
        assert change.requires_shutdown is False

    def test_remove(self) -> None:
        change = pt.plan_remove_usb(_dom_with_usb(), vendor=0x046D, product=0xC534)
        assert "detached" in change.summary
        assert "hostdev" not in change.new_xml

    def test_remove_missing(self) -> None:
        with pytest.raises(SpecValidationError, match="not attached"):
            pt.plan_remove_usb(_dom(), vendor=0x046D, product=0xC534)

    def test_remove_no_devices_element(self) -> None:
        dom = ET.fromstring("<domain><name>web</name></domain>")
        with pytest.raises(SpecValidationError, match="no <devices>"):
            pt.plan_remove_usb(dom, vendor=0x046D, product=0xC534)

    def test_zero_vendor_rejected(self) -> None:
        with pytest.raises(SpecValidationError):
            pt.plan_add_usb(_dom(), vendor=0, product=0xC534)

    def test_bad_product(self) -> None:
        with pytest.raises(SpecValidationError):
            pt.plan_add_usb(_dom(), vendor=0x046D, product=0)


class TestMixedAndMalformed:
    def test_pci_add_skips_usb_and_addressless_hostdev(self) -> None:
        # A USB hostdev (wrong type) plus a PCI hostdev missing its
        # <source><address> must both be skipped by the dup-scan, so the
        # add still succeeds.
        dom = ET.fromstring(
            "<domain><name>web</name><uuid>u</uuid><devices>"
            "<hostdev mode='subsystem' type='usb'>"
            "<source><vendor id='0x046d'/><product id='0xc534'/></source></hostdev>"
            "<hostdev mode='subsystem' type='pci'><source/></hostdev>"
            "</devices></domain>"
        )
        change = pt.plan_add_pci(dom, domain_addr=0, bus=1, slot=0, function=0)
        assert "attached" in change.summary

    def test_pci_remove_skips_usb_and_addressless_hostdev(self) -> None:
        dom = ET.fromstring(
            "<domain><name>web</name><uuid>u</uuid><devices>"
            "<hostdev mode='subsystem' type='usb'>"
            "<source><vendor id='0x046d'/><product id='0xc534'/></source></hostdev>"
            "<hostdev mode='subsystem' type='pci'><source/></hostdev>"
            "</devices></domain>"
        )
        with pytest.raises(SpecValidationError, match="not attached"):
            pt.plan_remove_pci(dom, domain_addr=0, bus=1, slot=0, function=0)

    def test_usb_remove_skips_pci_and_sourceless_hostdev(self) -> None:
        dom = ET.fromstring(
            "<domain><name>web</name><uuid>u</uuid><devices>"
            "<hostdev mode='subsystem' type='pci'>"
            "<source><address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>"
            "</source></hostdev>"
            "<hostdev mode='subsystem' type='usb'><source/></hostdev>"
            "</devices></domain>"
        )
        with pytest.raises(SpecValidationError, match="not attached"):
            pt.plan_remove_usb(dom, vendor=0x046D, product=0xC534)

    def test_pci_address_decimal_and_garbage_values(self) -> None:
        # _hex must fall back: a decimal string parses as decimal, and a
        # non-numeric string parses as 0. Address here is all-zero, so a
        # remove of 0:0:0.0 matches it.
        dom = ET.fromstring(
            "<domain><name>web</name><uuid>u</uuid><devices>"
            "<hostdev mode='subsystem' type='pci'>"
            "<source><address domain='garbage' bus='0' slot='0' function='0'/>"
            "</source></hostdev>"
            "</devices></domain>"
        )
        change = pt.plan_remove_pci(dom, domain_addr=0, bus=0, slot=0, function=0)
        assert "detached" in change.summary

    def test_pci_address_missing_attr_treated_as_zero(self) -> None:
        # An <address> missing the domain attr: _hex(None) -> 0, so a
        # remove targeting domain 0 matches.
        dom = ET.fromstring(
            "<domain><name>web</name><uuid>u</uuid><devices>"
            "<hostdev mode='subsystem' type='pci'>"
            "<source><address bus='0x00' slot='0x00' function='0x0'/></source></hostdev>"
            "</devices></domain>"
        )
        change = pt.plan_remove_pci(dom, domain_addr=0, bus=0, slot=0, function=0)
        assert "detached" in change.summary
