#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Location: ./tests/fuzz/scripts/run_restler_docker.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

Run RESTler API fuzzing via Docker when the server is ready.

This helper waits for the gateway at BASE_URL to expose /openapi.json,
downloads it into reports/restler/openapi.json, and then launches RESTler
Docker image to compile and test.

Environment variables:
  MCPFUZZ_BASE_URL        (default: http://localhost:4444)
  MCPFUZZ_AUTH_HEADER     (optional: e.g. "Authorization: Basic ...")
  MCPFUZZ_TIME_BUDGET     (default: 5 minutes)
  MCPFUZZ_NO_SSL          (default: 1 => pass --no_ssl)

CLI options mirror these and take precedence over env values.
"""

# Future
from __future__ import annotations

# Standard
import argparse
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


def project_root() -> Path:
    # tests/fuzz/scripts -> tests -> repo root (parents[2])
    return Path(__file__).resolve().parents[2]


def check_docker_available() -> None:
    if shutil.which("docker") is None:
        print("[RESTler] Docker not found in PATH. Please install Docker.", file=sys.stderr)
        sys.exit(2)


def wait_for_openapi(url: str, headers: dict[str, str], timeout: int = 60) -> bytes:
    """Wait until OpenAPI is available, return its bytes."""
    print(f"[RESTler] Waiting for OpenAPI at {url} (timeout {timeout}s)...")
    start = time.time()
    last_err: Exception | None = None
    while time.time() - start < timeout:
        try:
            req = Request(url, headers=headers)
            with urlopen(req, timeout=5) as resp:
                data = resp.read()
                # Quick sanity check it's JSON
                try:
                    json.loads(data.decode("utf-8", errors="ignore"))
                except Exception:
                    pass  # still accept; RESTler will validate
                print("[RESTler] OpenAPI document fetched.")
                return data
        except (HTTPError, URLError, OSError) as e:
            last_err = e
            time.sleep(1.0)
    print(f"[RESTler] Failed to fetch OpenAPI within timeout: {last_err}", file=sys.stderr)
    sys.exit(3)


def run_docker_restler(out_dir: Path, time_budget: int, no_ssl: bool) -> None:
    volume = f"{out_dir}:{'/workspace'}"
    image = "ghcr.io/microsoft/restler"

    compile_cmd = [
        "docker",
        "run",
        "--rm",
        "-v",
        volume,
        image,
        "restler",
        "compile",
        "--api_spec",
        "/workspace/openapi.json",
    ]

    test_cmd = [
        "docker",
        "run",
        "--rm",
        "-v",
        volume,
        image,
        "restler",
        "test",
        "--grammar_dir",
        "/workspace/Compile",
        "--time_budget",
        str(time_budget),
    ]
    if no_ssl:
        test_cmd.append("--no_ssl")

    print("[RESTler] Running compile:", " ".join(compile_cmd))
    res = subprocess.run(compile_cmd, check=False)
    if res.returncode != 0:
        print(f"[RESTler] Compile failed with exit code {res.returncode}", file=sys.stderr)
        sys.exit(res.returncode)

    print("[RESTler] Running test:", " ".join(test_cmd))
    res = subprocess.run(test_cmd, check=False)
    if res.returncode != 0:
        print(f"[RESTler] Test failed with exit code {res.returncode}", file=sys.stderr)
        sys.exit(res.returncode)

    print("[RESTler] Completed. Artifacts under:", out_dir)


def parse_header(header: str | None) -> dict[str, str]:
    if not header:
        return {}
    # Expect "Name: Value" format
    if ":" not in header:
        print("[RESTler] Invalid header format. Use 'Name: Value'", file=sys.stderr)
        sys.exit(4)
    name, value = header.split(":", 1)
    return {name.strip(): value.strip()}


def main() -> None:
    parser = argparse.ArgumentParser(description="Run RESTler via Docker against a running gateway")
    parser.add_argument("--base-url", default=os.getenv("MCPFUZZ_BASE_URL", "http://localhost:4444"), help="Gateway base URL")
    parser.add_argument("--openapi-path", default="/openapi.json", help="OpenAPI path, default /openapi.json")
    parser.add_argument("--out-dir", default=None, help="Output dir (default: reports/restler at repo root)")
    parser.add_argument("--auth-header", default=os.getenv("MCPFUZZ_AUTH_HEADER"), help="Optional single HTTP header 'Name: Value'")
    parser.add_argument("--time-budget", type=int, default=int(os.getenv("MCPFUZZ_TIME_BUDGET", "5")), help="RESTler time budget (minutes)")
    parser.add_argument("--no-ssl", action="store_true", default=os.getenv("MCPFUZZ_NO_SSL", "1") == "1", help="Pass --no_ssl to RESTler test")

    args = parser.parse_args()

    check_docker_available()

    root = project_root()
    out_dir = Path(args.out_dir) if args.out_dir else (root / "reports" / "restler")
    out_dir.mkdir(parents=True, exist_ok=True)

    headers = parse_header(args.auth_header)
    openapi_url = f"{args.base_url.rstrip('/')}{args.openapi_path}"

    data = wait_for_openapi(openapi_url, headers, timeout=60)
    openapi_file = out_dir / "openapi.json"
    openapi_file.write_bytes(data)
    print(f"[RESTler] Saved OpenAPI to {openapi_file}")

    run_docker_restler(out_dir, time_budget=args.time_budget, no_ssl=args.no_ssl)


if __name__ == "__main__":
    main()
