"""SOAP WSDL ops probe for pulumi-tests hybrid suite.

Covers every operation advertised in /sdk/vimService.wsdl (same list as
app/vsphere/soap/router.py). Fail on HTTP 5xx. Create/Power/Clone/Reconfig/Destroy
ops additionally assert task return + inventory side-effects via FindByInventoryPath
or RetrieveProperties where applicable.
"""

from __future__ import annotations

import os
import re
import secrets
import ssl
import urllib.error
import urllib.request
from typing import Any
from xml.sax.saxutils import escape

# Keep in sync with app/vsphere/soap/router.py sdk_wsdl ops list.
WSDL_OPS: list[str] = [
    "RetrieveServiceContent",
    "Login",
    "Logout",
    "RetrieveProperties",
    "RetrievePropertiesEx",
    "ContinueRetrievePropertiesEx",
    "CreateFilter",
    "WaitForUpdatesEx",
    "CreateContainerView",
    "DestroyPropertyFilter",
    "FindByInventoryPath",
    "FindByUuid",
    "FindByDnsName",
    "FindByIp",
    "FindChild",
    "CreateVM_Task",
    "CreateChildVM_Task",
    "CreateFolder",
    "PowerOnVM_Task",
    "PowerOffVM_Task",
    "CloneVM_Task",
    "CreateSnapshot_Task",
    "Rename_Task",
    "ReconfigVM_Task",
    "RelocateVM_Task",
    "Destroy_Task",
    "CustomizeVM_Task",
    "CancelTask",
    "CurrentTime",
    "InitiateFileTransferToGuest",
    "InitiateFileTransferFromGuest",
    "ListFilesInGuest",
    "DeleteFileInGuest",
    "MakeDirectoryInGuest",
    "ImportVApp_Task",
    "CreateImportSpec",
    "HttpNfcLeaseComplete",
    "HttpNfcLeaseProgress",
    "HttpNfcLeaseAbort",
    "HttpNfcLeaseGetManifest",
    "QueryConfigOption",
    "QueryConfigOptionEx",
    "QueryConfigOptionDescriptor",
    "QueryConfigTarget",
]


def _base() -> str:
    explicit = os.environ.get("VSPHERE_BASE")
    if explicit:
        return explicit.rstrip("/")
    server = os.environ.get("VSPHERE_SERVER", "localhost")
    if server.startswith("http://") or server.startswith("https://"):
        return server.rstrip("/")
    return f"https://{server}"


def _creds() -> tuple[str, str]:
    return (
        os.environ.get("VSPHERE_USER", "administrator@vsphere.local"),
        os.environ.get("VSPHERE_PASSWORD", "VMware1!"),
    )


def _ctx() -> ssl.SSLContext | None:
    if not _base().startswith("https://"):
        return None
    return ssl._create_unverified_context()  # noqa: S323


def _envelope(inner: str) -> str:
    return (
        '<?xml version="1.0" encoding="UTF-8"?>'
        '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"'
        ' xmlns:urn="urn:vim25">'
        f"<soapenv:Body>{inner}</soapenv:Body>"
        "</soapenv:Envelope>"
    )


def _post(
    body: str, *, cookie: str | None = None, session_id: str | None = None
) -> tuple[int, str, dict[str, str]]:
    headers = {
        "Content-Type": 'text/xml; charset="utf-8"',
        "SOAPAction": '""',
    }
    if cookie:
        headers["Cookie"] = cookie
    if session_id:
        headers["vmware-api-session-id"] = session_id
    req = urllib.request.Request(
        f"{_base()}/sdk",
        data=body.encode(),
        method="POST",
        headers=headers,
    )
    try:
        with urllib.request.urlopen(req, context=_ctx(), timeout=120) as resp:  # noqa: S310
            raw = resp.read().decode("utf-8", errors="replace")
            return int(resp.status), raw, {k.lower(): v for k, v in resp.headers.items()}
    except urllib.error.HTTPError as error:
        raw = error.read().decode("utf-8", errors="replace")
        return int(error.code), raw, {k.lower(): v for k, v in error.headers.items()}


def _xml_text(body: str, tag: str) -> str | None:
    match = re.search(rf"<(?:\w+:)?{re.escape(tag)}[^>]*>([^<]*)</(?:\w+:)?{re.escape(tag)}>", body)
    return match.group(1) if match else None


def _task_id(body: str) -> str | None:
    match = re.search(r'type="Task">([^<]+)<', body) or re.search(r">(task-[^<]+)<", body)
    return match.group(1) if match else None


def _login() -> tuple[str, str]:
    user, password = _creds()
    code, body, headers = _post(
        _envelope(
            "<urn:Login>"
            '<urn:_this type="SessionManager">SessionManager</urn:_this>'
            f"<urn:userName>{escape(user)}</urn:userName>"
            f"<urn:password>{escape(password)}</urn:password>"
            "</urn:Login>"
        )
    )
    if code >= 500:
        raise RuntimeError(f"SOAP Login 5xx: {code} {body[:200]}")
    if code >= 400:
        raise RuntimeError(f"SOAP Login failed: {code} {body[:200]}")
    set_cookie = headers.get("set-cookie") or ""
    cookie = set_cookie.split(";")[0] if set_cookie else ""
    session_id = headers.get("vmware-api-session-id") or ""
    if "vmware_soap_session" in set_cookie and not cookie.startswith("vmware_soap_session"):
        # normalize
        for part in set_cookie.split(","):
            part = part.strip()
            if part.startswith("vmware_soap_session"):
                cookie = part.split(";")[0]
                break
    if not cookie and session_id:
        cookie = f'vmware_soap_session="{session_id}"'
    if not cookie and not session_id:
        # body may still indicate success — use header-less cookie from LoginResponse key
        key = _xml_text(body, "key")
        if key:
            cookie = f'vmware_soap_session="{key}"'
            session_id = key
    if not cookie and not session_id:
        raise RuntimeError(f"SOAP Login missing session: {body[:200]}")
    return cookie, session_id


def _op_body(op: str, *, suffix: str, lab_vm: str = "vm-101") -> str:
    """Minimal SOAP body for each WSDL op."""

    if op == "RetrieveServiceContent":
        return (
            "<urn:RetrieveServiceContent>"
            '<urn:_this type="ServiceInstance">ServiceInstance</urn:_this>'
            "</urn:RetrieveServiceContent>"
        )
    if op == "Login":
        user, password = _creds()
        return (
            "<urn:Login>"
            '<urn:_this type="SessionManager">SessionManager</urn:_this>'
            f"<urn:userName>{escape(user)}</urn:userName>"
            f"<urn:password>{escape(password)}</urn:password>"
            "</urn:Login>"
        )
    if op == "Logout":
        return (
            '<urn:Logout><urn:_this type="SessionManager">SessionManager</urn:_this></urn:Logout>'
        )
    if op in {"RetrieveProperties", "RetrievePropertiesEx"}:
        return (
            f"<urn:{op}>"
            '<urn:_this type="PropertyCollector">propertyCollector</urn:_this>'
            "<urn:specSet>"
            "<urn:propSet><urn:type>VirtualMachine</urn:type><urn:pathSet>name</urn:pathSet></urn:propSet>"
            '<urn:objectSet><urn:obj type="VirtualMachine">vm-101</urn:obj></urn:objectSet>'
            "</urn:specSet>"
            f"</urn:{op}>"
        )
    if op == "ContinueRetrievePropertiesEx":
        return (
            "<urn:ContinueRetrievePropertiesEx>"
            '<urn:_this type="PropertyCollector">propertyCollector</urn:_this>'
            "<urn:token>token-none</urn:token>"
            "</urn:ContinueRetrievePropertiesEx>"
        )
    if op == "CreateFilter":
        return (
            "<urn:CreateFilter>"
            '<urn:_this type="PropertyCollector">propertyCollector</urn:_this>'
            "<urn:spec>"
            "<urn:propSet><urn:type>Folder</urn:type><urn:all>true</urn:all></urn:propSet>"
            '<urn:objectSet><urn:obj type="Folder">group-d1</urn:obj></urn:objectSet>'
            "</urn:spec>"
            "<urn:partialUpdates>false</urn:partialUpdates>"
            "</urn:CreateFilter>"
        )
    if op == "WaitForUpdatesEx":
        return (
            "<urn:WaitForUpdatesEx>"
            '<urn:_this type="PropertyCollector">propertyCollector</urn:_this>'
            "<urn:version></urn:version>"
            "</urn:WaitForUpdatesEx>"
        )
    if op == "CreateContainerView":
        return (
            "<urn:CreateContainerView>"
            '<urn:_this type="ViewManager">ViewManager</urn:_this>'
            '<urn:container type="Folder">group-d1</urn:container>'
            "<urn:type>VirtualMachine</urn:type>"
            "<urn:recursive>true</urn:recursive>"
            "</urn:CreateContainerView>"
        )
    if op == "DestroyPropertyFilter":
        return (
            "<urn:DestroyPropertyFilter>"
            '<urn:_this type="PropertyFilter">filter-1</urn:_this>'
            "</urn:DestroyPropertyFilter>"
        )
    if op == "FindByInventoryPath":
        return (
            "<urn:FindByInventoryPath>"
            '<urn:_this type="SearchIndex">SearchIndex</urn:_this>'
            "<urn:inventoryPath>/Datacenter/vm/web-01</urn:inventoryPath>"
            "</urn:FindByInventoryPath>"
        )
    if op == "FindByUuid":
        return (
            "<urn:FindByUuid>"
            '<urn:_this type="SearchIndex">SearchIndex</urn:_this>'
            "<urn:uuid>00000000-0000-0000-0000-000000000000</urn:uuid>"
            "<urn:vmSearch>true</urn:vmSearch>"
            "</urn:FindByUuid>"
        )
    if op == "FindByDnsName":
        return (
            "<urn:FindByDnsName>"
            '<urn:_this type="SearchIndex">SearchIndex</urn:_this>'
            "<urn:dnsName>web-01.lab.local</urn:dnsName>"
            "<urn:vmSearch>true</urn:vmSearch>"
            "</urn:FindByDnsName>"
        )
    if op == "FindByIp":
        return (
            "<urn:FindByIp>"
            '<urn:_this type="SearchIndex">SearchIndex</urn:_this>'
            "<urn:ip>10.0.0.10</urn:ip>"
            "<urn:vmSearch>true</urn:vmSearch>"
            "</urn:FindByIp>"
        )
    if op == "FindChild":
        return (
            "<urn:FindChild>"
            '<urn:_this type="SearchIndex">SearchIndex</urn:_this>'
            '<urn:entity type="Folder">group-v23</urn:entity>'
            "<urn:name>web-01</urn:name>"
            "</urn:FindChild>"
        )
    if op == "CreateVM_Task":
        return (
            "<urn:CreateVM_Task>"
            '<urn:_this type="Folder">group-v23</urn:_this>'
            "<urn:config>"
            f"<urn:name>soap-create-{suffix}</urn:name>"
            "<urn:guestId>otherGuest64</urn:guestId>"
            "<urn:numCPUs>1</urn:numCPUs>"
            "<urn:memoryMB>512</urn:memoryMB>"
            "<urn:files><urn:vmPathName>[datastore1]</urn:vmPathName></urn:files>"
            "</urn:config>"
            '<urn:pool type="ResourcePool">resgroup-22</urn:pool>'
            '<urn:host type="HostSystem">host-11</urn:host>'
            "</urn:CreateVM_Task>"
        )
    if op == "CreateChildVM_Task":
        return (
            "<urn:CreateChildVM_Task>"
            '<urn:_this type="ResourcePool">resgroup-22</urn:_this>'
            "<urn:config>"
            f"<urn:name>soap-child-{suffix}</urn:name>"
            "<urn:guestId>otherGuest64</urn:guestId>"
            "<urn:numCPUs>1</urn:numCPUs>"
            "<urn:memoryMB>512</urn:memoryMB>"
            "<urn:files><urn:vmPathName>[datastore1]</urn:vmPathName></urn:files>"
            "</urn:config>"
            '<urn:host type="HostSystem">host-11</urn:host>'
            "</urn:CreateChildVM_Task>"
        )
    if op == "CreateFolder":
        return (
            "<urn:CreateFolder>"
            '<urn:_this type="Folder">group-v23</urn:_this>'
            f"<urn:name>soap-folder-{suffix}</urn:name>"
            "</urn:CreateFolder>"
        )
    if op == "PowerOnVM_Task":
        return (
            "<urn:PowerOnVM_Task>"
            f'<urn:_this type="VirtualMachine">{lab_vm}</urn:_this>'
            "</urn:PowerOnVM_Task>"
        )
    if op == "PowerOffVM_Task":
        return (
            "<urn:PowerOffVM_Task>"
            f'<urn:_this type="VirtualMachine">{lab_vm}</urn:_this>'
            "</urn:PowerOffVM_Task>"
        )
    if op == "CloneVM_Task":
        return (
            "<urn:CloneVM_Task>"
            f'<urn:_this type="VirtualMachine">{lab_vm}</urn:_this>'
            '<urn:folder type="Folder">group-v23</urn:folder>'
            f"<urn:name>soap-clone-{suffix}</urn:name>"
            "<urn:spec><urn:powerOn>false</urn:powerOn><urn:template>false</urn:template></urn:spec>"
            "</urn:CloneVM_Task>"
        )
    if op == "CreateSnapshot_Task":
        return (
            "<urn:CreateSnapshot_Task>"
            f'<urn:_this type="VirtualMachine">{lab_vm}</urn:_this>'
            f"<urn:name>soap-snap-{suffix}</urn:name>"
            "<urn:description>probe</urn:description>"
            "<urn:memory>false</urn:memory>"
            "<urn:quiesce>false</urn:quiesce>"
            "</urn:CreateSnapshot_Task>"
        )
    if op == "Rename_Task":
        return (
            "<urn:Rename_Task>"
            f'<urn:_this type="VirtualMachine">{lab_vm}</urn:_this>'
            f"<urn:newName>web-01-renamed-{suffix}</urn:newName>"
            "</urn:Rename_Task>"
        )
    if op == "ReconfigVM_Task":
        return (
            "<urn:ReconfigVM_Task>"
            f'<urn:_this type="VirtualMachine">{lab_vm}</urn:_this>'
            "<urn:spec><urn:numCPUs>2</urn:numCPUs></urn:spec>"
            "</urn:ReconfigVM_Task>"
        )
    if op == "RelocateVM_Task":
        return (
            "<urn:RelocateVM_Task>"
            f'<urn:_this type="VirtualMachine">{lab_vm}</urn:_this>'
            "<urn:spec>"
            '<urn:host type="HostSystem">host-11</urn:host>'
            '<urn:datastore type="Datastore">datastore-31</urn:datastore>'
            "</urn:spec>"
            "</urn:RelocateVM_Task>"
        )
    if op == "Destroy_Task":
        return (
            "<urn:Destroy_Task>"
            f'<urn:_this type="VirtualMachine">{lab_vm}</urn:_this>'
            "</urn:Destroy_Task>"
        )
    if op == "CustomizeVM_Task":
        return (
            "<urn:CustomizeVM_Task>"
            f'<urn:_this type="VirtualMachine">{lab_vm}</urn:_this>'
            "<urn:spec><urn:identity/></urn:spec>"
            "</urn:CustomizeVM_Task>"
        )
    if op == "CancelTask":
        return '<urn:CancelTask><urn:_this type="Task">task-1</urn:_this></urn:CancelTask>'
    if op == "CurrentTime":
        return (
            "<urn:CurrentTime>"
            '<urn:_this type="ServiceInstance">ServiceInstance</urn:_this>'
            "</urn:CurrentTime>"
        )
    if op in {
        "InitiateFileTransferToGuest",
        "InitiateFileTransferFromGuest",
        "ListFilesInGuest",
        "DeleteFileInGuest",
        "MakeDirectoryInGuest",
    }:
        return (
            f"<urn:{op}>"
            f'<urn:_this type="GuestFileManager">guestFileManager-{lab_vm}</urn:_this>'
            f'<urn:vm type="VirtualMachine">{lab_vm}</urn:vm>'
            "<urn:auth><urn:username>root</urn:username><urn:password>lab</urn:password></urn:auth>"
            f"<urn:filePath>/tmp/soap-{suffix}</urn:filePath>"
            f"</urn:{op}>"
        )
    if op == "ImportVApp_Task":
        return (
            "<urn:ImportVApp_Task>"
            '<urn:_this type="ResourcePool">resgroup-22</urn:_this>'
            "<urn:spec/>"
            '<urn:folder type="Folder">group-v23</urn:folder>'
            '<urn:host type="HostSystem">host-11</urn:host>'
            "</urn:ImportVApp_Task>"
        )
    if op == "CreateImportSpec":
        return (
            "<urn:CreateImportSpec>"
            '<urn:_this type="OvfManager">OvfManager</urn:_this>'
            "<urn:ovfDescriptor>unused</urn:ovfDescriptor>"
            '<urn:resourcePool type="ResourcePool">resgroup-22</urn:resourcePool>'
            '<urn:datastore type="Datastore">datastore-31</urn:datastore>'
            "</urn:CreateImportSpec>"
        )
    if op in {
        "HttpNfcLeaseComplete",
        "HttpNfcLeaseProgress",
        "HttpNfcLeaseAbort",
        "HttpNfcLeaseGetManifest",
    }:
        return (
            f"<urn:{op}>"
            '<urn:_this type="HttpNfcLease">lease-1</urn:_this>'
            "<urn:percent>100</urn:percent>"
            f"</urn:{op}>"
        )
    if op in {
        "QueryConfigOption",
        "QueryConfigOptionEx",
        "QueryConfigOptionDescriptor",
        "QueryConfigTarget",
    }:
        return f'<urn:{op}><urn:_this type="EnvironmentBrowser">envbrowser-1</urn:_this></urn:{op}>'
    return f'<urn:{op}><urn:_this type="ServiceInstance">ServiceInstance</urn:_this></urn:{op}>'


def _find_by_path(cookie: str, session_id: str, path: str) -> tuple[int, str]:
    code, body, _ = _post(
        _envelope(
            "<urn:FindByInventoryPath>"
            '<urn:_this type="SearchIndex">SearchIndex</urn:_this>'
            f"<urn:inventoryPath>{escape(path)}</urn:inventoryPath>"
            "</urn:FindByInventoryPath>"
        ),
        cookie=cookie,
        session_id=session_id,
    )
    return code, body


def _verify_side_effect(
    op: str,
    response: str,
    *,
    cookie: str,
    session_id: str,
    suffix: str,
) -> str | None:
    """Return error string if side-effect check fails; None if ok/not applicable."""

    if op in {
        "CreateVM_Task",
        "CreateChildVM_Task",
        "CloneVM_Task",
        "PowerOnVM_Task",
        "PowerOffVM_Task",
        "Destroy_Task",
        "ReconfigVM_Task",
        "CreateFolder",
    }:
        if op.endswith("_Task") and not _task_id(response) and "Task" not in response:
            return "missing Task returnval"
    if op in {"CreateVM_Task", "CreateChildVM_Task"}:
        name = f"soap-create-{suffix}" if op == "CreateVM_Task" else f"soap-child-{suffix}"
        code, body = _find_by_path(cookie, session_id, f"/Datacenter/vm/{name}")
        if code >= 500:
            return f"FindByInventoryPath 5xx after {op}"
        if "VirtualMachine" not in body and name not in body:
            # Some seeds place under production folder — also try that path.
            code2, body2 = _find_by_path(cookie, session_id, f"/Datacenter/vm/production/{name}")
            if "VirtualMachine" not in body2 and name not in body2:
                return f"created VM {name} not found in inventory"
    if op == "CreateFolder":
        folder_moid = _xml_text(response, "returnval")
        if not folder_moid:
            return "CreateFolder missing Folder returnval"
    if op == "CloneVM_Task":
        name = f"soap-clone-{suffix}"
        code, body = _find_by_path(cookie, session_id, f"/Datacenter/vm/{name}")
        if code >= 500:
            return f"FindByInventoryPath 5xx after clone"
        if "VirtualMachine" not in body and name not in body:
            code2, body2 = _find_by_path(cookie, session_id, f"/Datacenter/vm/production/{name}")
            if "VirtualMachine" not in body2 and name not in body2:
                return f"clone {name} not found"
    if op == "Destroy_Task":
        # Destroy uses a disposable VM created earlier in the suite — checked by caller via lab_vm.
        pass
    return None


def run_soap_ops() -> dict[str, Any]:
    """Exercise all WSDL SOAP ops. Mutating ops use disposable VMs where needed."""

    cookie, session_id = _login()
    results: list[dict[str, Any]] = []
    failures: list[dict[str, Any]] = []

    # Disposable VM for destroy / power cycles (do not destroy seeded web-01).
    suffix = secrets.token_hex(3)
    create_body = _envelope(_op_body("CreateVM_Task", suffix=f"lab-{suffix}"))
    code, body, _ = _post(create_body, cookie=cookie, session_id=session_id)
    disposable_vm = "vm-101"
    if code < 500 and _task_id(body):
        # Resolve created VM name via FindByInventoryPath
        name = f"soap-create-lab-{suffix}"
        _, found = _find_by_path(cookie, session_id, f"/Datacenter/vm/{name}")
        moid = re.search(r'type="VirtualMachine">([^<]+)<', found)
        if moid:
            disposable_vm = moid.group(1)
        else:
            _, found2 = _find_by_path(cookie, session_id, f"/Datacenter/vm/production/{name}")
            moid = re.search(r'type="VirtualMachine">([^<]+)<', found2)
            if moid:
                disposable_vm = moid.group(1)

    # Prefer a clone as destroy target so we never delete the only disposable if create failed.
    clone_suffix = secrets.token_hex(3)
    clone_body = _envelope(_op_body("CloneVM_Task", suffix=clone_suffix, lab_vm=disposable_vm))
    code, body, _ = _post(clone_body, cookie=cookie, session_id=session_id)
    destroy_target = disposable_vm
    if code < 500:
        cname = f"soap-clone-{clone_suffix}"
        _, found = _find_by_path(cookie, session_id, f"/Datacenter/vm/{cname}")
        moid = re.search(r'type="VirtualMachine">([^<]+)<', found)
        if not moid:
            _, found = _find_by_path(cookie, session_id, f"/Datacenter/vm/production/{cname}")
            moid = re.search(r'type="VirtualMachine">([^<]+)<', found)
        if moid:
            destroy_target = moid.group(1)

    for op in WSDL_OPS:
        op_suffix = secrets.token_hex(3)
        lab_vm = destroy_target if op == "Destroy_Task" else disposable_vm
        # Avoid Logout killing the suite session mid-run — probe with a fresh login at end.
        if op == "Logout":
            tmp_cookie, tmp_sid = _login()
            code, body, _ = _post(
                _envelope(_op_body(op, suffix=op_suffix, lab_vm=lab_vm)),
                cookie=tmp_cookie,
                session_id=tmp_sid,
            )
        elif op == "Login":
            code, body, _ = _post(_envelope(_op_body(op, suffix=op_suffix)))
        elif op == "Rename_Task":
            # Rename disposable VM then rename back via another call is heavy; use folder instead.
            folder_code, folder_body, _ = _post(
                _envelope(_op_body("CreateFolder", suffix=f"rn-{op_suffix}")),
                cookie=cookie,
                session_id=session_id,
            )
            folder_id = _xml_text(folder_body, "returnval") or "group-v23"
            if folder_code >= 500:
                code, body = folder_code, folder_body
            else:
                code, body, _ = _post(
                    _envelope(
                        "<urn:Rename_Task>"
                        f'<urn:_this type="Folder">{folder_id}</urn:_this>'
                        f"<urn:newName>soap-renamed-{op_suffix}</urn:newName>"
                        "</urn:Rename_Task>"
                    ),
                    cookie=cookie,
                    session_id=session_id,
                )
        else:
            code, body, _ = _post(
                _envelope(_op_body(op, suffix=op_suffix, lab_vm=lab_vm)),
                cookie=cookie,
                session_id=session_id,
            )

        entry: dict[str, Any] = {
            "op": op,
            "status": code,
            "ok": True,
            "error": "",
        }
        if code >= 500:
            entry["ok"] = False
            entry["error"] = f"HTTP {code}: {body[:200]}"
        else:
            side = _verify_side_effect(
                op,
                body,
                cookie=cookie,
                session_id=session_id,
                suffix=op_suffix if op != "CreateVM_Task" else op_suffix,
            )
            # CreateVM_Task in the loop creates yet another VM — verify with its suffix.
            if (
                op in {"CreateVM_Task", "CreateChildVM_Task", "CloneVM_Task", "CreateFolder"}
                and side
            ):
                entry["ok"] = False
                entry["error"] = side
            elif op in {"PowerOnVM_Task", "PowerOffVM_Task", "ReconfigVM_Task", "Destroy_Task"}:
                if not _task_id(body) and "Task" not in body and "Response" not in body:
                    entry["ok"] = False
                    entry["error"] = "missing task/response"
                elif op == "Destroy_Task":
                    # Confirm target is gone
                    _, found = _find_by_path(
                        cookie, session_id, f"/Datacenter/vm/soap-clone-{clone_suffix}"
                    )
                    if 'type="VirtualMachine"' in found and destroy_target in found:
                        entry["ok"] = False
                        entry["error"] = "VM still present after Destroy_Task"

        if not entry["ok"]:
            failures.append(entry)
        results.append(entry)

    return {
        "ops": results,
        "total": len(results),
        "failed": len(failures),
        "failures": failures,
        "ok": not failures,
        "wsdl_ops": len(WSDL_OPS),
    }
