#!/usr/bin/env python3
"""
Comprehensive pipeline: all protocol/security modes with project client.

Flow per scenario:
  1. Start server (with config for this mode and ws/queue variant).
  2. Check it is listening (GET /health).
  3. Check commands (echo via JsonRpcClient); if ws enabled, check /ws.
  4. Stop server, move to next scenario.
  5. On completion (success or failure), kill all running servers and proxy.

Ports (15000-16000): proxy 15000, app 15001 (one app at a time).

WS is always on (no config). Per mode we run 2 variants: queue enabled, queue disabled.
Total: 8 modes × 2 = 16 scenarios. Each scenario has a timeout (PIPELINE_SCENARIO_TIMEOUT s).

Run:
  python tests/test_all_modes_comprehensive_pipeline.py
  pytest tests/test_all_modes_comprehensive_pipeline.py -v

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

import asyncio
import json
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

# Project root
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

# Ports in 15000-16000
PIPELINE_PROXY_PORT = 15000
PIPELINE_APP_PORT = 15001

# Timeout per scenario (start server, health, echo, ws, stop). Prevents pipeline from hanging.
PIPELINE_SCENARIO_TIMEOUT = 90


def kill_ports_force(ports: List[int]) -> None:
    """
    Kill -9 any process listening on the given ports.
    Uses fuser -k -9 port/tcp when available, else lsof + kill -9.
    """
    for port in ports:
        try:
            # Prefer fuser -k -9 (SIGKILL)
            subprocess.run(
                ["fuser", "-k", "-9", f"{port}/tcp"],
                capture_output=True,
                timeout=3,
            )
        except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
            try:
                out = subprocess.run(
                    ["lsof", "-ti", f":{port}"],
                    capture_output=True,
                    text=True,
                    timeout=2,
                )
                if out.returncode == 0 and out.stdout.strip():
                    pids = out.stdout.strip().split()
                    for pid in pids:
                        subprocess.run(
                            ["kill", "-9", pid],
                            capture_output=True,
                            timeout=2,
                        )
            except Exception:
                pass
        time.sleep(0.2)


async def wait_health(
    protocol: str,
    port: int,
    token: Optional[str] = None,
    cert: Optional[str] = None,
    key: Optional[str] = None,
    timeout: float = 30.0,
) -> bool:
    """Wait until GET /health returns 200."""
    import httpx

    url = f"{protocol}://localhost:{port}/health"
    verify = protocol == "http"
    headers: Dict[str, str] = {}
    if token:
        headers["X-API-Key"] = token
    kwargs: Dict[str, Any] = {"verify": verify, "headers": headers, "timeout": 2.0}
    if cert and key:
        kwargs["cert"] = (cert, key)

    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        try:
            async with httpx.AsyncClient(**kwargs) as client:
                r = await client.get(url)
                if r.status_code == 200:
                    return True
        except Exception:
            pass
        await asyncio.sleep(1.0)
    return False


async def test_health_via_client(
    client: Any,
    base_url: str,
    use_ssl: bool,
) -> bool:
    """GET /health (client may not expose it; use httpx)."""
    import httpx

    try:
        async with httpx.AsyncClient(verify=not use_ssl) as http_client:
            r = await http_client.get(f"{base_url}/health", timeout=10.0)
            return r.status_code == 200
    except Exception:
        return False


async def test_echo_via_client(client: Any, config_name: str) -> bool:
    """Execute echo command via JsonRpcClient."""
    try:
        result = await client.execute_command(
            command="echo",
            params={"message": f"Pipeline {config_name}"},
        )
        return bool(result.get("success"))
    except Exception:
        return False


async def test_ws_via_client(client: Any) -> bool:
    """Connect to /ws, subscribe, unsubscribe, close."""
    try:
        from mcp_proxy_adapter.client.jsonrpc_client import (
            open_bidirectional_ws_channel,
        )
    except ImportError:
        return True  # skip if ws client not available
    try:
        channel = open_bidirectional_ws_channel(client, receive_timeout=10.0)
        async with channel:
            await channel.send_json({"action": "subscribe", "job_id": "pipeline-test"})
            try:
                await asyncio.wait_for(channel.receive_iter().__anext__(), 2.0)
            except asyncio.TimeoutError:
                pass
            await channel.send_json(
                {"action": "unsubscribe", "job_id": "pipeline-test"}
            )
        return True
    except Exception:
        return False


def _config_path_for_variant(
    configs_dir: Path,
    config_file: str,
    with_queue: bool,
    proxy_port: int,
) -> Tuple[Path, Optional[Path]]:
    """
    Return (path_to_use, temp_path_or_none).
    Always builds a temp config so registration URLs use proxy_port (pipeline proxy).
    If with_queue False, also sets queue_manager.enabled=false.
    """
    original = configs_dir / config_file
    with open(original, "r", encoding="utf-8") as f:
        data = json.load(f)
    reg = data.setdefault("registration", {})
    orig_url = reg.get("register_url", "http://localhost:3005/register")
    scheme = orig_url.split("://")[0] if "://" in orig_url else "http"
    base = f"{scheme}://localhost:{proxy_port}"
    reg["register_url"] = f"{base}/register"
    reg["unregister_url"] = f"{base}/unregister"
    if "heartbeat" in reg and isinstance(reg["heartbeat"], dict):
        reg["heartbeat"]["url"] = f"{base}/proxy/heartbeat"
    if not with_queue:
        data.setdefault("queue_manager", {})["enabled"] = False
    fd, path = tempfile.mkstemp(suffix=".json", prefix="pipeline_")
    with open(fd, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2)
    return Path(path), Path(path)


# Mode: (config_file, protocol, token or None, cert path or None, key path or None)
MODES: List[Tuple[str, str, Optional[str], Optional[str], Optional[str]]] = [
    ("http_basic.json", "http", None, None, None),
    ("http_token.json", "http", "admin-secret-key", None, None),
    ("http_token_roles.json", "http", "admin-secret-key", None, None),
    ("https_basic.json", "https", None, None, None),
    ("https_token.json", "https", "admin-secret-key-https", None, None),
    ("https_token_roles.json", "https", "admin-secret-key-https", None, None),
    (
        "mtls_no_roles_correct.json",
        "https",
        "admin-secret-key-mtls",
        str(PROJECT_ROOT / "mtls_certificates" / "client" / "test-client.crt"),
        str(PROJECT_ROOT / "mtls_certificates" / "client" / "test-client.key"),
    ),
    (
        "mtls_with_roles_correct.json",
        "https",
        "admin-secret-key-mtls",
        str(PROJECT_ROOT / "mtls_certificates" / "client" / "test-client.crt"),
        str(PROJECT_ROOT / "mtls_certificates" / "client" / "test-client.key"),
    ),
]


async def run_mode(
    config_file: str,
    protocol: str,
    token: Optional[str],
    cert: Optional[str],
    key: Optional[str],
    configs_dir: Path,
    server_script: Path,
    proxy_port: int,
    app_port: int,
    with_queue: bool = True,
) -> Tuple[str, bool, str]:
    """
    Start server, check listening (health), check commands (echo), check /ws (always on), stop server.
    Returns (config_name, success, error_message).
    """
    config_path_orig = configs_dir / config_file
    if not config_path_orig.exists():
        return (
            config_file.replace(".json", ""),
            False,
            f"Config not found: {config_path_orig}",
        )

    config_path, temp_config_path = _config_path_for_variant(
        configs_dir, config_file, with_queue, proxy_port
    )
    proc: Optional[subprocess.Popen[Any]] = None
    try:
        # Start app server
        proc = subprocess.Popen(
            [
                sys.executable,
                str(server_script),
                "--config",
                str(config_path),
                "--port",
                str(app_port),
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            cwd=str(PROJECT_ROOT),
            text=True,
        )
        if not await wait_health(protocol, app_port, token, cert, key):
            out, _ = proc.communicate(timeout=2)
            return (
                config_file.replace(".json", ""),
                False,
                f"Server did not become ready. Output: {(out or '')[:300]}",
            )

        # Build client
        from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient

        client_kwargs: Dict[str, Any] = {
            "protocol": protocol,
            "host": "localhost",
            "port": app_port,
            "check_hostname": False,
        }
        if token:
            client_kwargs["token"] = token
            client_kwargs["token_header"] = "X-API-Key"
        if cert and key:
            client_kwargs["cert"] = cert
            client_kwargs["key"] = key

        client = JsonRpcClient(**client_kwargs)
        base_url = f"{protocol}://localhost:{app_port}"

        health_ok = await test_health_via_client(client, base_url, protocol == "https")
        echo_ok = await test_echo_via_client(client, config_file.replace(".json", ""))
        ws_ok = await test_ws_via_client(client)
        success = health_ok and echo_ok and ws_ok
        err = ""
        if not health_ok:
            err = "health failed"
        elif not echo_ok:
            err = "echo failed"
        elif not ws_ok:
            err = "ws failed"
        return config_file.replace(".json", ""), success, err
    finally:
        if proc is not None:
            proc.terminate()
            try:
                proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                proc.kill()
                proc.wait()
        if temp_config_path is not None and temp_config_path.exists():
            try:
                temp_config_path.unlink()
            except OSError:
                pass
        await asyncio.sleep(1)


async def run_pipeline() -> int:
    """Kill ports, start proxy, run each mode with client tests, stop proxy. Return 0 if all pass."""
    configs_dir = (
        PROJECT_ROOT / "mcp_proxy_adapter" / "examples" / "full_application" / "configs"
    )
    server_script = (
        PROJECT_ROOT / "mcp_proxy_adapter" / "examples" / "full_application" / "main.py"
    )
    proxy_script = (
        PROJECT_ROOT / "mcp_proxy_adapter" / "examples" / "run_proxy_server.py"
    )

    if not server_script.exists():
        print(f"❌ Server script not found: {server_script}")
        return 1
    if not proxy_script.exists():
        print(f"❌ Proxy script not found: {proxy_script}")
        return 1

    used_ports = [PIPELINE_PROXY_PORT, PIPELINE_APP_PORT]
    print("🔪 Killing processes on pipeline ports: %s" % used_ports)
    kill_ports_force(used_ports)
    await asyncio.sleep(2)

    # Start proxy
    print("🚀 Starting proxy on port %s" % PIPELINE_PROXY_PORT)
    proxy_proc = subprocess.Popen(
        [
            sys.executable,
            str(proxy_script),
            "--host",
            "0.0.0.0",
            "--port",
            str(PIPELINE_PROXY_PORT),
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        cwd=str(PROJECT_ROOT),
    )
    try:
        if not await wait_health("http", PIPELINE_PROXY_PORT, timeout=15.0):
            stderr = (
                proxy_proc.stderr.read().decode("utf-8", errors="ignore")
                if proxy_proc.stderr
                else ""
            )
            print(f"❌ Proxy did not become ready: {stderr[:400]}")
            return 1
        print("✅ Proxy ready")
    except Exception as e:
        print(f"❌ Proxy start failed: {e}")
        proxy_proc.terminate()
        proxy_proc.wait(timeout=3)
        return 1

    results: List[Tuple[str, bool, str]] = []
    try:
        for config_file, protocol, token, cert, key in MODES:
            name = config_file.replace(".json", "")
            for with_queue in (True, False):
                variant = "queue" if with_queue else "no_queue"
                print(f"\n📋 Mode: {name} ({variant}, port {PIPELINE_APP_PORT})")
                try:
                    name_ret, ok, err = await asyncio.wait_for(
                        run_mode(
                            config_file,
                            protocol,
                            token,
                            cert,
                            key,
                            configs_dir,
                            server_script,
                            PIPELINE_PROXY_PORT,
                            PIPELINE_APP_PORT,
                            with_queue=with_queue,
                        ),
                        timeout=PIPELINE_SCENARIO_TIMEOUT,
                    )
                except asyncio.TimeoutError:
                    name_ret, ok, err = name, False, "timeout"
                    results.append((f"{name_ret} ({variant})", ok, err))
                    print(f"   ❌ FAIL (timeout after {PIPELINE_SCENARIO_TIMEOUT}s)")
                    kill_ports_force(used_ports)
                    continue
                results.append((f"{name_ret} ({variant})", ok, err))
                status = "✅ PASS" if ok else "❌ FAIL"
                print(f"   {status}" + (f" ({err})" if err else ""))
    finally:
        # 5. In any case (success, failure, exception): kill all servers and proxy
        print("\n🛑 Killing all servers and proxy...")
        proxy_proc.terminate()
        try:
            proxy_proc.wait(timeout=5)
        except subprocess.TimeoutExpired:
            proxy_proc.kill()
            proxy_proc.wait()
        kill_ports_force(used_ports)

    passed = sum(1 for _, ok, _ in results if ok)
    total = len(results)
    print("\n" + "=" * 60)
    print("📊 PIPELINE SUMMARY")
    print("=" * 60)
    for name_ret, ok, err in results:
        status = "✅" if ok else "❌"
        print(f"  {status} {name_ret}" + (f" — {err}" if err else ""))
    print(f"\n🎯 {passed}/{total} passed")
    return 0 if passed == total else 1


def test_pipeline_standalone() -> None:
    """Pytest entry: run pipeline (can be slow)."""
    exit_code = asyncio.run(run_pipeline())
    assert exit_code == 0, "Pipeline had failures"


if __name__ == "__main__":
    sys.exit(asyncio.run(run_pipeline()))
