#!/usr/bin/env python3
"""Cross-platform no-auth CLI integration smoke checks.

This script runs a minimal but meaningful no-auth integration flow across all
CI operating systems:
1. Validate `arcade configure` writes client configs in a path with spaces.
2. Scaffold a new toolkit with `arcade new`.
3. Run protocol smoke checks (stdio + http) against the generated server.
"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any, cast


def _run(
    cmd: list[str],
    *,
    cwd: Path,
    capture_output: bool = False,
) -> subprocess.CompletedProcess[str]:
    proc = subprocess.run(
        cmd,
        cwd=str(cwd),
        text=True,
        capture_output=capture_output,
        check=False,
    )
    if proc.returncode != 0:
        raise RuntimeError(
            f"Command failed ({proc.returncode}): {' '.join(cmd)}\n"
            f"STDOUT:\n{proc.stdout or ''}\nSTDERR:\n{proc.stderr or ''}"
        )
    return proc


def _ensure_exists(path: Path) -> None:
    if not path.exists():
        raise RuntimeError(f"Expected path to exist: {path}")


def _load_json_object(path: Path) -> dict[str, Any]:
    parsed = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(parsed, dict):
        raise TypeError(f"Expected JSON object in {path}, got {type(parsed).__name__}")
    return cast(dict[str, Any], parsed)


def _expect_dict(value: Any, context: str) -> dict[str, Any]:
    if not isinstance(value, dict):
        raise TypeError(f"Expected object for {context}, got {type(value).__name__}")
    return cast(dict[str, Any], value)


def _assert_stdio_entry(entry: dict[str, Any], context: str) -> None:
    if "command" not in entry:
        raise RuntimeError(f"{context}: missing 'command'")

    args = entry.get("args")
    if not isinstance(args, list):
        raise TypeError(f"{context}: missing or invalid 'args' list")
    if not any(str(arg).endswith("server.py") for arg in args):
        raise RuntimeError(f"{context}: expected entrypoint in args ending with 'server.py'")


def _add_local_uv_sources(pyproject_path: Path, repo_root: Path) -> None:
    pyproject_text = pyproject_path.read_text(encoding="utf-8")
    if "[tool.uv.sources]" in pyproject_text:
        return

    repo = repo_root.resolve()
    block_lines = [
        "[tool.uv.sources]",
        f'arcade-mcp = {{ path = "{repo.as_posix()}", editable = true }}',
        f'arcade-mcp-server = {{ path = "{(repo / "libs/arcade-mcp-server").as_posix()}", editable = true }}',
        f'arcade-core = {{ path = "{(repo / "libs/arcade-core").as_posix()}", editable = true }}',
        f'arcade-serve = {{ path = "{(repo / "libs/arcade-serve").as_posix()}", editable = true }}',
        f'arcade-tdk = {{ path = "{(repo / "libs/arcade-tdk").as_posix()}", editable = true }}',
    ]
    pyproject_path.write_text(
        pyproject_text.rstrip() + "\n\n" + "\n".join(block_lines) + "\n",
        encoding="utf-8",
    )


def _run_configure_smoke(repo_root: Path) -> None:
    config_tmp = Path(tempfile.mkdtemp(prefix="arcade mcp config test "))
    try:
        (config_tmp / "server.py").write_text("print('ok')\n", encoding="utf-8")

        cursor_cfg = config_tmp / "cursor config.json"
        vscode_cfg = config_tmp / "vscode config.json"
        claude_cfg = config_tmp / "claude config.json"

        _run(
            [
                "uv",
                "run",
                "--project",
                str(repo_root),
                "arcade",
                "configure",
                "cursor",
                "--name",
                "demo",
                "--config",
                str(cursor_cfg),
            ],
            cwd=config_tmp,
        )
        cursor_data = _load_json_object(cursor_cfg)
        cursor_mcp_servers = _expect_dict(cursor_data.get("mcpServers"), "Cursor stdio mcpServers")
        _assert_stdio_entry(
            _expect_dict(cursor_mcp_servers.get("demo"), "Cursor stdio demo server"), "Cursor stdio"
        )

        overwrite = _run(
            [
                "uv",
                "run",
                "--project",
                str(repo_root),
                "arcade",
                "configure",
                "cursor",
                "--transport",
                "http",
                "--port",
                "8123",
                "--name",
                "demo",
                "--config",
                str(cursor_cfg),
            ],
            cwd=config_tmp,
            capture_output=True,
        )
        overwrite_output = (overwrite.stdout or "") + "\n" + (overwrite.stderr or "")
        if "overwrite" not in overwrite_output.lower():
            raise RuntimeError(
                "Expected overwrite warning when configuring cursor with same --name.\n"
                f"Output:\n{overwrite_output}"
            )
        cursor_data = _load_json_object(cursor_cfg)
        cursor_mcp_servers = _expect_dict(cursor_data.get("mcpServers"), "Cursor http mcpServers")
        cursor_http_demo = _expect_dict(cursor_mcp_servers.get("demo"), "Cursor http demo server")
        if cursor_http_demo.get("type") != "stream":
            raise RuntimeError("Cursor http config type mismatch")
        if cursor_http_demo.get("url") != "http://localhost:8123/mcp":
            raise RuntimeError("Cursor http config URL mismatch")

        _run(
            [
                "uv",
                "run",
                "--project",
                str(repo_root),
                "arcade",
                "configure",
                "vscode",
                "--name",
                "demo",
                "--config",
                str(vscode_cfg),
            ],
            cwd=config_tmp,
        )
        vscode_data = _load_json_object(vscode_cfg)
        vscode_servers = _expect_dict(vscode_data.get("servers"), "VS Code stdio servers")
        _assert_stdio_entry(
            _expect_dict(vscode_servers.get("demo"), "VS Code stdio demo server"), "VS Code stdio"
        )

        _run(
            [
                "uv",
                "run",
                "--project",
                str(repo_root),
                "arcade",
                "configure",
                "vscode",
                "--transport",
                "http",
                "--port",
                "8123",
                "--name",
                "demo",
                "--config",
                str(vscode_cfg),
            ],
            cwd=config_tmp,
        )
        vscode_data = _load_json_object(vscode_cfg)
        vscode_servers = _expect_dict(vscode_data.get("servers"), "VS Code http servers")
        vscode_http_demo = _expect_dict(vscode_servers.get("demo"), "VS Code http demo server")
        if vscode_http_demo.get("type") != "http":
            raise RuntimeError("VS Code http config type mismatch")
        if vscode_http_demo.get("url") != "http://localhost:8123/mcp":
            raise RuntimeError("VS Code http config URL mismatch")

        _run(
            [
                "uv",
                "run",
                "--project",
                str(repo_root),
                "arcade",
                "configure",
                "claude",
                "--name",
                "demo",
                "--config",
                str(claude_cfg),
            ],
            cwd=config_tmp,
        )
        claude_data = _load_json_object(claude_cfg)
        claude_mcp_servers = _expect_dict(claude_data.get("mcpServers"), "Claude stdio mcpServers")
        _assert_stdio_entry(
            _expect_dict(claude_mcp_servers.get("demo"), "Claude stdio demo server"), "Claude stdio"
        )
    finally:
        shutil.rmtree(config_tmp, ignore_errors=True)


def _run_scaffold_and_protocol_smoke(repo_root: Path) -> None:
    scaffold_dir = Path(tempfile.mkdtemp(prefix="arcade scaffold with spaces "))
    try:
        created = _run(
            [
                "uv",
                "run",
                "arcade",
                "new",
                "my_server",
                "--dir",
                str(scaffold_dir),
            ],
            cwd=repo_root,
            capture_output=True,
        )
        new_output = (created.stdout or "") + "\n" + (created.stderr or "")
        if "Next steps:" not in new_output:
            raise RuntimeError(
                "Expected 'Next steps:' output from 'arcade new'.\n" f"Output:\n{new_output}"
            )

        generated_root = scaffold_dir / "my_server"
        _ensure_exists(generated_root / "pyproject.toml")
        _ensure_exists(generated_root / "src" / "my_server" / "server.py")
        _ensure_exists(generated_root / ".env.example")

        generated_pyproject = generated_root / "pyproject.toml"
        _add_local_uv_sources(generated_pyproject, repo_root)

        generated_server_dir = generated_root / "src" / "my_server"
        _run(
            ["uv", "run", "python", "-c", "import server; print('generated server import ok')"],
            cwd=generated_server_dir,
        )

        _run(
            [
                "uv",
                "run",
                "python",
                "tests/integration/mcp_protocol_smoke.py",
                "--server-dir",
                str(generated_server_dir),
                "--transport",
                "both",
            ],
            cwd=repo_root,
        )
    finally:
        shutil.rmtree(scaffold_dir, ignore_errors=True)


def main() -> None:
    repo_root = Path.cwd().resolve()
    print(f"Repo root: {repo_root}")
    os.environ["ARCADE_USAGE_TRACKING"] = "0"
    _run(["uv", "--version"], cwd=repo_root)

    _run_configure_smoke(repo_root)
    _run_scaffold_and_protocol_smoke(repo_root)

    print("Cross-platform no-auth CLI smoke checks passed.")


if __name__ == "__main__":
    main()
