"""Full vSphere REST + SOAP + major-matrix surface tests."""

from __future__ import annotations

import os
import re

import pytest
from httpx import ASGITransport, AsyncClient

from app.config import Settings
from app.main import create_app
from app.vsphere.contracts.matrix import VERSIONS, catalog_entries_for_major
from app.vsphere.rest.coverage import IMPLEMENTED, catalog_entries
from app.vsphere.seed import seed_vsphere_inventory

pytestmark = pytest.mark.integration

_PATH_SUBS = {
    "{vm}": "vm-101",
    "{host}": "host-11",
    "{datastore}": "datastore-31",
    "{task}": "task-1",
    "{snapshot}": "snapshot-missing",
    "{category_id}": "cat-lab-1",
    "{tag_id}": "tag-lab-1",
    "{item_id}": "item-ubuntu",
    "{library_id}": "lib-local-1",
    "{session_id}": "session-lab-1",
    "{folder}": "group-v23",
    "{datacenter}": "datacenter-21",
    "{cluster}": "domain-c21",
    "{resource_pool}": "resgroup-22",
    "{permission_id}": "1",
    "{policy}": "policy-default",
    "{supervisor}": "supervisor-1",
    "{namespace}": "ns-lab-1",
    "{provider}": "vsphere.local",
    "{interface}": "nic0",
    "{network}": "network-41",
    "{cdrom}": "3000",
    "{disk}": "2000",
    "{nic}": "4000",
    "{adapter}": "1000",
    "{service}": "vsphere-ui",
    "{domain}": "lab.local",
}


def _concrete(path: str) -> str:
    out = path
    for key, value in _PATH_SUBS.items():
        out = out.replace(key, value)
    return out


@pytest.fixture
async def client() -> AsyncClient:
    database_url = os.getenv("TEST_DATABASE_URL") or os.getenv("DATABASE_URL")
    if not database_url:
        pytest.skip("TEST_DATABASE_URL / DATABASE_URL required")
    settings = Settings(
        database_url=database_url,  # type: ignore[arg-type]
        contract_snapshot=None,
        enable_pve_stub=False,
    )
    app = create_app(settings=settings, worker_factories=())
    async with app.router.lifespan_context(app):
        await seed_vsphere_inventory(app.state.database, force=True, profile="small")
        async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http:
            yield http


async def _session(
    client: AsyncClient, user: str = "administrator@vsphere.local"
) -> dict[str, str]:
    login = await client.post("/api/session", auth=(user, "VMware1!"))
    assert login.status_code == 201, login.text
    sid = login.json()
    assert isinstance(sid, str) and sid
    return {"vmware-api-session-id": sid}


@pytest.mark.parametrize("major", sorted(VERSIONS))
async def test_ui_catalog_and_method_fields_for_every_major(
    client: AsyncClient, major: int
) -> None:
    catalog = await client.get("/ui/api/catalog", params={"major": major})
    assert catalog.status_code == 200
    body = catalog.json()
    assert body["plane"] == "vsphere-rest"
    assert body["method_count"] == len(catalog_entries_for_major(major))
    # Spot-check a path with params on majors that include VM get.
    method = await client.get(
        "/ui/api/method",
        params={"major": major, "path": "/api/vcenter/vm/{vm}", "verb": "GET"},
    )
    assert method.status_code == 200
    payload = method.json()
    if payload.get("implemented"):
        assert any(f["name"] == "vm" for f in payload["path_fields"])


async def test_all_coverage_routes_no_server_error(client: AsyncClient) -> None:
    headers = await _session(client)
    failures: list[str] = []
    for entry in catalog_entries():
        verb = entry["verb"]
        path = entry["path"]
        if verb == "DELETE" and path == "/api/session":
            continue
        url = _concrete(path)
        kwargs: dict = {"headers": headers}
        if verb in {"POST", "PATCH", "PUT"}:
            kwargs["headers"] = {**headers, "Content-Type": "application/json"}
            if path.endswith("/power"):
                url = f"{url}?action=start"
                kwargs["json"] = {}
            elif "tag-association" in path:
                kwargs["json"] = {
                    "action": "list-attached-tags",
                    "tag_id": "x",
                    "object_id": {"type": "VirtualMachine", "id": "vm-101"},
                }
            else:
                kwargs["json"] = {}
        response = await client.request(verb, url, **kwargs)
        if response.status_code >= 500:
            failures.append(f"{verb} {path} -> {response.status_code} {response.text[:160]}")
    assert failures == [], "\n".join(failures)


async def test_rest_inventory_returns_seed_data(client: AsyncClient) -> None:
    headers = await _session(client)
    for path, min_count in (
        ("/api/vcenter/vm", 5),
        ("/api/vcenter/host", 3),
        ("/api/vcenter/datastore", 1),
        ("/api/vcenter/network", 1),
        ("/api/vcenter/datacenter", 1),
        ("/api/vcenter/cluster", 1),
        ("/api/vcenter/folder", 1),
    ):
        response = await client.get(path, headers=headers)
        assert response.status_code == 200, path
        assert len(response.json()) >= min_count, path


async def test_legacy_rest_wrappers(client: AsyncClient) -> None:
    headers = await _session(client)
    for path in (
        "/rest/vcenter/vm",
        "/rest/vcenter/host",
        "/rest/vcenter/datastore",
        "/rest/vcenter/network",
        "/rest/vcenter/datacenter",
        "/rest/vcenter/cluster",
        "/rest/appliance/system/version",
    ):
        response = await client.get(path, headers=headers)
        assert response.status_code == 200, path
        body = response.json()
        assert "value" in body


async def test_session_contracts(client: AsyncClient) -> None:
    headers = await _session(client)
    get_session = await client.get("/api/session", headers=headers)
    assert get_session.status_code == 200
    assert get_session.content in (b"", b"null") or not get_session.text.strip()
    assert "Administrator" in (get_session.headers.get("x-vmware-session-roles") or "")

    legacy = await client.post(
        "/rest/com/vmware/cis/session",
        auth=("administrator@vsphere.local", "VMware1!"),
    )
    assert legacy.status_code in {200, 201}
    assert legacy.json()["value"]
    legacy_get = await client.get(
        "/rest/com/vmware/cis/session",
        headers={"vmware-api-session-id": legacy.json()["value"]},
    )
    assert legacy_get.status_code == 200
    assert legacy_get.json()["value"]


async def test_authz_readonly_forbidden_on_power(client: AsyncClient) -> None:
    headers = await _session(client, "readonly@vsphere.local")
    vms = await client.get("/api/vcenter/vm", headers=headers)
    assert vms.status_code == 200
    vm = vms.json()[0]["vm"]
    power = await client.post(
        f"/api/vcenter/vm/{vm}/power",
        params={"action": "start"},
        headers=headers,
    )
    assert power.status_code == 403


async def test_appliance_version_public_and_health(client: AsyncClient) -> None:
    version = await client.get("/api/appliance/system/version")
    assert version.status_code == 200
    assert version.json()["version"]
    headers = await _session(client)
    health = await client.get("/api/appliance/health/system", headers=headers)
    assert health.status_code == 200
    assert health.json()["status"] == "green"
    networking = await client.get("/api/appliance/networking", headers=headers)
    assert networking.status_code == 200
    assert networking.json()["hostname"]


async def test_soap_login_service_content_and_inventory(client: AsyncClient) -> None:
    login = await client.post(
        "/sdk",
        content="""<?xml version="1.0"?>
        <Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
          <Body>
            <Login xmlns="urn:vim25">
              <_this type="SessionManager">SessionManager</_this>
              <userName>administrator@vsphere.local</userName>
              <password>VMware1!</password>
            </Login>
          </Body>
        </Envelope>""",
        headers={"Content-Type": "text/xml"},
    )
    assert login.status_code == 200
    assert "LoginResponse" in login.text
    cookie = (login.headers.get("set-cookie") or "").split(";")[0]
    assert "vmware_soap_session" in cookie

    content = await client.post(
        "/sdk",
        content="""<?xml version="1.0"?>
        <Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
          <Body>
            <RetrieveServiceContent xmlns="urn:vim25">
              <_this type="ServiceInstance">ServiceInstance</_this>
            </RetrieveServiceContent>
          </Body>
        </Envelope>""",
        headers={"Content-Type": "text/xml", "Cookie": cookie},
    )
    assert content.status_code == 200
    assert "propertyCollector" in content.text
    assert "eventManager" in content.text

    props = await client.post(
        "/sdk",
        content="""<?xml version="1.0"?>
        <Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
          <Body>
            <RetrieveProperties xmlns="urn:vim25">
              <_this type="PropertyCollector">propertyCollector</_this>
              <specSet>
                <propSet><type>Folder</type><pathSet>childEntity</pathSet><pathSet>name</pathSet></propSet>
                <objectSet>
                  <obj type="Folder">group-d1</obj>
                  <selectSet xsi:type="TraversalSpec">
                    <type>Folder</type><path>childEntity</path>
                  </selectSet>
                </objectSet>
              </specSet>
            </RetrieveProperties>
          </Body>
        </Envelope>""",
        headers={"Content-Type": "text/xml", "Cookie": cookie},
    )
    assert props.status_code == 200
    assert "datacenter-21" in props.text or "Datacenter" in props.text

    events = await client.post(
        "/sdk",
        content="""<?xml version="1.0"?>
        <Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
          <Body>
            <QueryEvents xmlns="urn:vim25">
              <_this type="EventManager">EventManager</_this>
            </QueryEvents>
          </Body>
        </Envelope>""",
        headers={"Content-Type": "text/xml", "Cookie": cookie},
    )
    assert events.status_code == 200
    assert "QueryEventsResponse" in events.text


async def test_vm_lifecycle_and_power(client: AsyncClient) -> None:
    headers = await _session(client)
    created = await client.post(
        "/api/vcenter/vm",
        headers={**headers, "Content-Type": "application/json"},
        json={
            "name": "full-api-lifecycle",
            "placement": {"folder": "group-v23", "host": "host-11", "datastore": "datastore-31"},
            "cpu_count": 1,
            "memory_size_MiB": 512,
        },
    )
    assert created.status_code in {200, 201}, created.text
    vm = created.json()
    if isinstance(vm, dict):
        vm = vm.get("vm") or vm.get("value") or vm
    assert isinstance(vm, str)
    detail = await client.get(f"/api/vcenter/vm/{vm}", headers=headers)
    assert detail.status_code == 200
    power = await client.post(
        f"/api/vcenter/vm/{vm}/power",
        params={"action": "start"},
        headers=headers,
    )
    assert power.status_code in {200, 204}, power.text
    # powered-on VMs cannot be deleted — stop first (vSphere semantics)
    stop = await client.post(
        f"/api/vcenter/vm/{vm}/power",
        params={"action": "stop"},
        headers=headers,
    )
    assert stop.status_code in {200, 204}, stop.text
    deleted = await client.delete(f"/api/vcenter/vm/{vm}", headers=headers)
    assert deleted.status_code in {200, 204}, deleted.text


async def test_coverage_registry_matches_implemented_constant() -> None:
    assert len(catalog_entries()) == len(IMPLEMENTED)
    for verb, path in IMPLEMENTED:
        assert re.match(r"^/(api|rest)/", path), path
        assert verb in {"GET", "POST", "PUT", "PATCH", "DELETE"}


@pytest.mark.parametrize("major", sorted(VERSIONS))
async def test_major_matrix_all_verbs_no_server_error(client: AsyncClient, major: int) -> None:
    """Apply each catalog major and exercise every registered GET/POST/PATCH/DELETE."""

    headers = await _session(client)
    apply = await client.post("/ui/api/contract/apply", params={"major": major})
    assert apply.status_code == 200, apply.text
    failures: list[str] = []
    order = {"GET": 0, "PUT": 1, "PATCH": 2, "POST": 3, "DELETE": 4}
    entries = sorted(
        catalog_entries_for_major(major),
        key=lambda item: (order.get(item["verb"], 9), item["path"]),
    )
    for entry in entries:
        verb = entry["verb"]
        path = entry["path"]
        if verb == "DELETE" and path in {"/api/session", "/rest/com/vmware/cis/session"}:
            continue
        url = _concrete(path)
        kwargs: dict = {"headers": {**headers}}
        if verb in {"POST", "PATCH", "PUT"}:
            kwargs["headers"] = {**headers, "Content-Type": "application/json"}
            if path.endswith("/power") and "/guest/" not in path:
                url = f"{url}?action=start"
                kwargs["json"] = {}
            elif path.endswith("/guest/power"):
                url = f"{url}?action=reboot"
                kwargs["json"] = {}
            elif path.endswith("/maintenance"):
                url = f"{url}?action=enter"
                kwargs["json"] = {}
            elif path == "/api/vcenter/folder/{folder}":
                url = f"{url}?action=rename"
                kwargs["json"] = {"name": "renamed-by-matrix"}
            elif path == "/api/content/local-library":
                kwargs["json"] = {"create_spec": {"name": f"lib-m{major}-{os.urandom(3).hex()}"}}
            elif path == "/api/cis/tagging/category":
                kwargs["json"] = {
                    "create_spec": {
                        "name": f"cat-m{major}-{os.urandom(3).hex()}",
                        "cardinality": "MULTIPLE",
                        "associable_types": [],
                    }
                }
            elif path == "/api/cis/tagging/tag":
                kwargs["json"] = {
                    "create_spec": {
                        "name": f"tag-m{major}-{os.urandom(3).hex()}",
                        "category_id": "missing-category",
                    }
                }
            elif "tag-association" in path:
                kwargs["json"] = {
                    "action": "list-attached-tags",
                    "tag_id": "x",
                    "object_id": {"type": "VirtualMachine", "id": "vm-101"},
                }
            elif path == "/api/vcenter/network/dvpg":
                kwargs["json"] = {
                    "name": f"dvpg-m{major}-{os.urandom(2).hex()}",
                    "dvs": "dvs-51",
                    "vlan_id": 20,
                }
            elif path == "/api/content/library/item":
                kwargs["json"] = {
                    "create_spec": {
                        "library_id": "lib-missing",
                        "name": f"item-m{major}-{os.urandom(2).hex()}",
                        "type": "ovf",
                    }
                }
            elif path == "/api/vcenter/authorization/permissions":
                kwargs["json"] = {
                    "principal": "readonly@vsphere.local",
                    "role": "ReadOnly",
                    "entity": "datacenter-21",
                }
            elif path == "/api/vcenter/datastore/{datastore}/files":
                kwargs["json"] = {
                    "path": f"/probe-m{major}-{os.urandom(2).hex()}.txt",
                    "size": 1,
                    "type": "FILE",
                }
            elif path.endswith("/hardware/cpu"):
                kwargs["json"] = {"count": 2}
            elif path.endswith("/hardware/memory"):
                kwargs["json"] = {"size_MiB": 1024}
            elif path.endswith("/hardware/disk"):
                kwargs["json"] = {"type": "SCSI", "new_vmdk": {"capacity": 1024}}
            elif path.endswith("/hardware/ethernet"):
                kwargs["json"] = {
                    "type": "VMXNET3",
                    "backing": {"type": "STANDARD_PORTGROUP", "network": "network-41"},
                }
            elif path.endswith("/snapshots") and verb == "POST":
                kwargs["json"] = {"name": f"snap-m{major}-{os.urandom(2).hex()}"}
            elif "/snapshots/" in path and verb == "POST":
                kwargs["json"] = {"action": "revert"}
            elif path.endswith("/clone"):
                kwargs["json"] = {
                    "name": f"clone-m{major}-{os.urandom(2).hex()}",
                    "placement": {"folder": "group-v23", "host": "host-11"},
                }
            elif path.endswith("/relocate"):
                kwargs["json"] = {"placement": {"host": "host-12"}}
            elif path.endswith("/console/tickets"):
                kwargs["json"] = {"type": "WEBMKS"}
            elif path.endswith("/guest/customization"):
                kwargs["json"] = {"name": {"name": f"guest-m{major}"}}
            elif path == "/api/vcenter/vm/{vm}" and verb == "POST":
                kwargs["json"] = {"action": "unregister"}
            else:
                kwargs["json"] = {"name": f"probe-{major}-{os.urandom(2).hex()}"}
        if verb == "DELETE" and path.endswith("{vm}"):
            url = "/api/vcenter/vm/vm-missing-matrix"
        response = await client.request(verb, url, **kwargs)
        if response.status_code >= 500:
            failures.append(f"{verb} {path} -> {response.status_code} {response.text[:160]}")
    assert failures == [], "\n".join(failures)
