#!/usr/bin/env python3
"""Hybrid pulumi-tests suite: pulumi-vsphere + full REST matrix + CRUD + SOAP WSDL ops."""

from __future__ import annotations

import json
import os
import subprocess
import sys
import traceback
import xml.etree.ElementTree as ET
from datetime import UTC, datetime
from pathlib import Path

ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / "lib"))

from assert_nonempty import assert_nonempty  # noqa: E402
from report_html import write_report  # noqa: E402

SMOKE = os.environ.get("TEST_SMOKE", "").lower() in {"1", "true", "yes"}
REPORT_DIR = Path(os.environ.get("REPORT_DIR", "/reports"))
JUNIT_PATH = Path(os.environ.get("REPORT_PATH", str(REPORT_DIR / "pulumi-junit.xml")))
HTML_PATH = Path(os.environ.get("HTML_REPORT_PATH", str(REPORT_DIR / "pulumi-report.html")))
JSON_PATH = Path(os.environ.get("JSON_REPORT_PATH", str(REPORT_DIR / "pulumi-summary.json")))
PROGRAMS = ROOT / "programs"

# Ensure VSPHERE_BASE for HTTP probes when only VSPHERE_SERVER is set (compose).
if not os.environ.get("VSPHERE_BASE") and os.environ.get("VSPHERE_SERVER"):
    server = os.environ["VSPHERE_SERVER"]
    if not server.startswith("http://") and not server.startswith("https://"):
        os.environ["VSPHERE_BASE"] = f"https://{server}"

PULUMI_CASES = [
    {
        "id": "PU-INV",
        "title": "Inventory data sources (dc/cluster/ds/net/vm/host/folder/pool)",
        "dir": "inventory",
        "smoke": True,
        "required": [
            "datacenter_id",
            "datacenter_name",
            "datastore_id",
            "datastore_name",
            "cluster_id",
            "cluster_name",
            "resource_pool_id",
            "resource_pool_name",
            "cluster_resource_pool_id",
            "network_id",
            "network_name",
            "vm_id",
            "vm_name",
            "host_id",
            "host_name",
            "folder_id",
            "folder_path",
        ],
    },
    {
        "id": "PU-FOLDER",
        "title": "Folder create/destroy",
        "dir": "folders",
        "smoke": False,
        "required": ["folder_id", "folder_path", "datacenter_id"],
    },
    {
        "id": "PU-VM",
        "title": "VirtualMachine create/destroy",
        "dir": "vm_lifecycle",
        "smoke": False,
        "required": [
            "lab_vm_id",
            "lab_vm_name",
            "resource_pool_id",
            "datastore_id",
            "network_id",
        ],
    },
    {
        "id": "PU-TAG",
        "title": "TagCategory + Tag create/destroy",
        "dir": "tags",
        "smoke": False,
        "required": ["category_id", "category_name", "tag_id", "tag_name"],
    },
]


def _ensure_program_deps() -> None:
    """Install per-program requirements once (image usually already has them)."""

    seen: set[str] = set()
    for case in PULUMI_CASES:
        req = PROGRAMS / case["dir"] / "requirements.txt"
        key = str(req.resolve()) if req.exists() else ""
        if not key or key in seen:
            continue
        seen.add(key)
        subprocess.run(
            [sys.executable, "-m", "pip", "install", "-q", "-r", str(req)],
            check=False,
            capture_output=True,
        )


def _run_pulumi_case(case: dict) -> dict:
    work_dir = PROGRAMS / case["dir"]
    if not (work_dir / "__main__.py").is_file():
        return {
            "id": case["id"],
            "title": case["title"],
            "status": "failed",
            "error": f"missing program {work_dir}",
            "outputs": {},
        }

    try:
        from pulumi import automation as auto
    except Exception as exc:  # noqa: BLE001
        return {
            "id": case["id"],
            "title": case["title"],
            "status": "failed",
            "error": f"pulumi automation unavailable: {exc}",
            "outputs": {},
        }

    os.environ.setdefault("PULUMI_CONFIG_PASSPHRASE", "lab")
    os.environ.setdefault("PULUMI_BACKEND_URL", "file:///tmp/pulumi-state")
    Path("/tmp/pulumi-state").mkdir(parents=True, exist_ok=True)

    stack_name = f"{case['id'].lower()}-{os.environ.get('TEST_RUN_ID', 'lab')[:8]}"
    try:
        stack = auto.create_or_select_stack(stack_name=stack_name, work_dir=str(work_dir))
        try:
            result = stack.up(on_output=lambda *_: None)
            outputs = {k: v.value for k, v in (result.outputs or {}).items()}
            errors = assert_nonempty(outputs, required=case["required"])
            if errors:
                return {
                    "id": case["id"],
                    "title": case["title"],
                    "status": "failed",
                    "error": "; ".join(errors),
                    "outputs": outputs,
                }
            return {
                "id": case["id"],
                "title": case["title"],
                "status": "passed",
                "error": "",
                "outputs": outputs,
            }
        finally:
            try:
                stack.destroy(on_output=lambda *_: None)
            except Exception:
                pass
            try:
                stack.workspace.remove_stack(stack_name)
            except Exception:
                pass
    except Exception as exc:  # noqa: BLE001
        return {
            "id": case["id"],
            "title": case["title"],
            "status": "failed",
            "error": str(exc)[-3000:],
            "outputs": {},
        }


def _run_rest_matrix() -> dict:
    from rest_matrix import run_rest_matrix

    majors = [9] if SMOKE else [6, 7, 8, 9]
    try:
        summary = run_rest_matrix(majors=majors)
    except Exception as exc:  # noqa: BLE001
        return {
            "id": "PU-REST",
            "title": f"REST full matrix majors={majors}",
            "status": "failed",
            "error": f"{exc}\n{traceback.format_exc()[-1500:]}",
            "outputs": {},
            "rest": {"total": 0, "failed": 1, "failures": [{"error": str(exc)}]},
        }
    status = "passed" if summary.get("ok") else "failed"
    err = ""
    coverage = summary.get("coverage_line") or "?"
    if not summary.get("ok"):
        sample = summary.get("failures") or []
        err = (
            f"REST matrix coverage={coverage} critical={summary.get('critical')} "
            f"probed={summary.get('probed')} declared={summary.get('declared')}; "
        )
        err += "; ".join(f"{f.get('verb')} {f.get('path')} → {f.get('status')}" for f in sample[:8])
    return {
        "id": "PU-REST",
        "title": f"REST HTTP matrix majors={majors} (IMPLEMENTED + HEAD; Layer A)",
        "status": status,
        "error": err,
        "outputs": {
            "coverage": coverage,
            "critical": summary.get("critical"),
            "probed": summary.get("probed"),
            "declared": summary.get("declared"),
            "by_verb": summary.get("by_verb"),
            "majors": [m.get("major") for m in summary.get("majors") or []],
        },
        "rest": summary,
    }


def _run_rest_crud() -> dict:
    from rest_crud import run_rest_crud

    try:
        summary = run_rest_crud()
    except Exception as exc:  # noqa: BLE001
        return {
            "id": "PU-CRUD",
            "title": "Deep REST CRUD (session/folder/tag/library/vm)",
            "status": "failed",
            "error": f"{exc}\n{traceback.format_exc()[-1500:]}",
            "outputs": {},
            "crud": {"total": 0, "failed": 1, "failures": [{"error": str(exc)}]},
        }
    status = "passed" if summary.get("ok") else "failed"
    err = ""
    if not summary.get("ok"):
        err = "; ".join(
            f"{f.get('flow')}/{f.get('step')}: {f.get('error')}"
            for f in (summary.get("failures") or [])
        )
    return {
        "id": "PU-CRUD",
        "title": "Deep REST CRUD (session/folder/tag/library/vm)",
        "status": status,
        "error": err,
        "outputs": {
            "total": summary.get("total"),
            "failed": summary.get("failed"),
            "flows": [f.get("flow") for f in summary.get("flows") or []],
        },
        "crud": summary,
    }


def _run_soap_ops() -> dict:
    from soap_ops import run_soap_ops

    try:
        summary = run_soap_ops()
    except Exception as exc:  # noqa: BLE001
        return {
            "id": "PU-SOAP",
            "title": "SOAP WSDL ops (all /sdk operations)",
            "status": "failed",
            "error": f"{exc}\n{traceback.format_exc()[-1500:]}",
            "outputs": {},
            "soap": {"total": 0, "failed": 1, "failures": [{"error": str(exc)}]},
        }
    status = "passed" if summary.get("ok") else "failed"
    err = ""
    if not summary.get("ok"):
        err = "; ".join(
            f"{f.get('op')}: {f.get('error')}" for f in (summary.get("failures") or [])[:12]
        )
    return {
        "id": "PU-SOAP",
        "title": f"SOAP WSDL ops ({summary.get('wsdl_ops', '?')} operations)",
        "status": status,
        "error": err,
        "outputs": {
            "total": summary.get("total"),
            "failed": summary.get("failed"),
            "wsdl_ops": summary.get("wsdl_ops"),
        },
        "soap": summary,
    }


def main() -> int:
    REPORT_DIR.mkdir(parents=True, exist_ok=True)
    _ensure_program_deps()
    pulumi_cases = [c for c in PULUMI_CASES if c["smoke"]] if SMOKE else PULUMI_CASES
    print(
        f"pulumi hybrid suite pulumi_cases={len(pulumi_cases)} smoke={SMOKE} "
        f"server={os.environ.get('VSPHERE_SERVER', 'api-gateway')} "
        f"base={os.environ.get('VSPHERE_BASE', '')}",
        flush=True,
    )

    results: list[dict] = []
    failed = 0

    for case in pulumi_cases:
        print(f"== {case['id']}: {case['title']} ==", flush=True)
        result = _run_pulumi_case(case)
        results.append(result)
        print(f"{case['id']}: {result['status']}", flush=True)
        if result["status"] == "failed":
            failed += 1
            print(result["error"][:800], flush=True)

    # REST matrix (smoke = major 9 only; full = 6–9)
    print("== PU-REST: REST matrix ==", flush=True)
    rest_result = _run_rest_matrix()
    results.append(rest_result)
    print(f"PU-REST: {rest_result['status']}", flush=True)
    if rest_result["status"] == "failed":
        failed += 1
        print(rest_result["error"][:800], flush=True)

    rest_summary = rest_result.get("rest") or {}
    crud_summary: dict = {}
    soap_summary: dict = {}

    if not SMOKE:
        print("== PU-CRUD: deep REST CRUD ==", flush=True)
        crud_result = _run_rest_crud()
        results.append(crud_result)
        crud_summary = crud_result.get("crud") or {}
        print(f"PU-CRUD: {crud_result['status']}", flush=True)
        if crud_result["status"] == "failed":
            failed += 1
            print(crud_result["error"][:800], flush=True)

        print("== PU-SOAP: WSDL ops ==", flush=True)
        soap_result = _run_soap_ops()
        results.append(soap_result)
        soap_summary = soap_result.get("soap") or {}
        print(f"PU-SOAP: {soap_result['status']}", flush=True)
        if soap_result["status"] == "failed":
            failed += 1
            print(soap_result["error"][:800], flush=True)

    summary = {
        "generated_at": datetime.now(UTC).isoformat(),
        "vsphere_server": os.environ.get("VSPHERE_SERVER", "api-gateway"),
        "vsphere_base": os.environ.get("VSPHERE_BASE", ""),
        "smoke": SMOKE,
        "provider": "pulumi-vsphere+rest+soap",
        "cases": results,
        "total_failed": failed,
        "rest": {
            "total": rest_summary.get("total", 0),
            "probed": rest_summary.get("probed", rest_summary.get("total", 0)),
            "declared": rest_summary.get("declared", 0),
            "critical": rest_summary.get("critical", rest_summary.get("failed", 0)),
            "failed": rest_summary.get("failed", 0),
            "coverage_line": rest_summary.get("coverage_line"),
            "probed_eq_declared": rest_summary.get("probed_eq_declared"),
            "by_verb": rest_summary.get("by_verb"),
            "majors": rest_summary.get("majors"),
            "failures": rest_summary.get("failures"),
        },
        "crud": {
            "total": crud_summary.get("total", 0),
            "failed": crud_summary.get("failed", 0),
            "flows": crud_summary.get("flows"),
            "failures": crud_summary.get("failures"),
        },
        "soap": {
            "total": soap_summary.get("total", 0),
            "failed": soap_summary.get("failed", 0),
            "ops": soap_summary.get("ops"),
            "failures": soap_summary.get("failures"),
        },
    }
    JSON_PATH.write_text(json.dumps(summary, indent=2, default=str), encoding="utf-8")
    write_report(summary, HTML_PATH)

    suite = ET.Element(
        "testsuite",
        name="pulumi-hybrid",
        tests=str(len(results)),
        failures=str(failed),
    )
    for result in results:
        node = ET.SubElement(
            suite,
            "testcase",
            classname="pulumi-hybrid",
            name=f"{result['id']} {result['title']}",
        )
        if result["status"] == "failed":
            ET.SubElement(node, "failure", message=result["error"][:500])
        elif result["status"] == "skipped":
            ET.SubElement(node, "skipped", message=result.get("error") or "skipped")
    ET.ElementTree(suite).write(JUNIT_PATH, encoding="utf-8", xml_declaration=True)

    print(f"Wrote {HTML_PATH}", flush=True)
    print(f"Wrote {JSON_PATH}", flush=True)
    print(f"Wrote {JUNIT_PATH}", flush=True)
    print(
        f"SUMMARY failed={failed} total={len(results)} "
        f"rest.coverage={summary['rest'].get('coverage_line')} "
        f"rest.critical={summary['rest']['critical']} "
        f"crud.failed={summary['crud']['failed']} "
        f"soap.failed={summary['soap']['failed']}",
        flush=True,
    )
    return 1 if failed else 0


if __name__ == "__main__":
    raise SystemExit(main())
