"""
Nexus 负载压测脚本
测试: /health, /health/services, /api/servers/stats, /api/agent/heartbeat, /api/servers/exec

用法:
  NEXUS_API_KEY=your-key python tests/load_test.py [--concurrency 50] [--duration 30]
  NEXUS_TEST_BASE=http://127.0.0.1:8600  (可选)
"""
import asyncio
import httpx
import time
import sys
import os
import argparse

BASE = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600")
API_KEY = os.environ.get("NEXUS_API_KEY", "")

# 取一台在线服务器做 exec 测试
SERVER_ID = 1  # 默认，会动态查找

stats = {"total": 0, "ok": 0, "fail": 0, "times": [], "errors": []}


async def hit(client: httpx.AsyncClient, name: str, method: str, path: str, body=None, need_key=True):
    headers = {}
    if need_key and API_KEY:
        headers["X-API-Key"] = API_KEY
    t0 = time.perf_counter()
    try:
        if method == "GET":
            r = await client.get(f"{BASE}{path}", headers=headers, timeout=30)
        elif method == "POST":
            r = await client.post(f"{BASE}{path}", json=body, headers=headers, timeout=30)
        elapsed = time.perf_counter() - t0
        if r.status_code == 200:
            stats["ok"] += 1
        else:
            stats["fail"] += 1
            stats["errors"].append(f"[{r.status_code}] {name} {path}")
    except Exception as e:
        elapsed = time.perf_counter() - t0
        stats["fail"] += 1
        stats["errors"].append(f"[EXCEPTION] {name}: {str(e)[:80]}")
    stats["total"] += 1
    stats["times"].append(elapsed)


async def health_worker(client: httpx.AsyncClient, duration: float):
    """轻量级 health check 持续请求"""
    end = time.time() + duration
    while time.time() < end:
        await hit(client, "health", "GET", "/health", need_key=False)
        await asyncio.sleep(0.01)


async def health_services_worker(client: httpx.AsyncClient, duration: float):
    """health/services 持续请求"""
    end = time.time() + duration
    while time.time() < end:
        await hit(client, "health/services", "GET", "/health/services", need_key=False)
        await asyncio.sleep(0.05)


async def stats_worker(client: httpx.AsyncClient, duration: float):
    """仪表盘 stats (Redis 缓存)"""
    end = time.time() + duration
    while time.time() < end:
        await hit(client, "stats", "GET", "/api/servers/stats")
        await asyncio.sleep(0.1)


async def heartbeat_worker(client: httpx.AsyncClient, duration: float):
    """Agent 心跳压测"""
    end = time.time() + duration
    while time.time() < end:
        await hit(client, "heartbeat", "POST", "/api/agent/heartbeat", body={
            "cpu": 25.0, "memory": 60.5, "disk": 45.2,
            "load_1": 0.5, "load_5": 0.3, "load_15": 0.2,
            "uptime": 86400, "os": "Ubuntu 24.04"
        }, need_key=False)
        await asyncio.sleep(0.2)


async def exec_worker(client: httpx.AsyncClient, duration: float, server_id: int):
    """群发命令压测 (走 ThreadPoolExecutor SSH)"""
    end = time.time() + duration
    while time.time() < end:
        await hit(client, "exec", "POST", f"/api/servers/{server_id}/exec", body={
            "command": "echo 'load-test' && date"
        })
        await asyncio.sleep(0.5)


def print_report(concurrency: int, duration: float):
    times = sorted(stats["times"]) if stats["times"] else [0]
    n = len(times)
    p50 = times[int(n * 0.50)] if n > 0 else 0
    p95 = times[min(int(n * 0.95), n - 1)] if n > 0 else 0
    p99 = times[min(int(n * 0.99), n - 1)] if n > 0 else 0
    rps = stats["total"] / duration if duration > 0 else 0
    err_rate = stats["fail"] / stats["total"] * 100 if stats["total"] > 0 else 0

    print(f"""
{'='*60}
  Nexus 压测报告
{'='*60}
  并发:        {concurrency}
  持续时间:    {duration}s
  总请求:      {stats['total']}
  成功:        {stats['ok']}
  失败:        {stats['fail']}
  错误率:      {err_rate:.1f}%
  RPS:         {rps:.1f}
  延迟 P50:    {p50*1000:.1f}ms
  延迟 P95:    {p95*1000:.1f}ms
  延迟 P99:    {p99*1000:.1f}ms
  最慢:        {max(times)*1000:.1f}ms
  最快:        {min(times)*1000:.1f}ms
{'='*60}
""")
    if stats["errors"]:
        print("错误采样 (前10):")
        for e in stats["errors"][:10]:
            print(f"  {e}")


async def main():
    global BASE, API_KEY, SERVER_ID, stats
    parser = argparse.ArgumentParser()
    parser.add_argument("--concurrency", type=int, default=50)
    parser.add_argument("--duration", type=int, default=30)
    parser.add_argument("--no-exec", action="store_true", help="跳过 SSH exec 测试")
    args = parser.parse_args()

    global BASE, API_KEY
    BASE = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600")
    API_KEY = os.environ.get("NEXUS_API_KEY", "")
    if not API_KEY:
        print("WARN: NEXUS_API_KEY unset — stats/exec workers may be skipped", file=sys.stderr)

    concurrency = args.concurrency
    duration = args.duration
    print(f"压测配置: BASE={BASE}, 并发={concurrency}, 时长={duration}s")

    # 动态获取一台在线服务器
    async with httpx.AsyncClient(timeout=10) as client:
        if not args.no_exec and API_KEY:
            try:
                r = await client.get(f"{BASE}/api/servers/", headers={"X-API-Key": API_KEY})
                servers = r.json() if r.status_code == 200 else []
                if isinstance(servers, dict):
                    servers = servers.get("data", [])
                online = [s for s in servers if s.get("is_online")]
                if online:
                    SERVER_ID = online[0]["id"]
                    print(f"exec 目标服务器: id={SERVER_ID} name={online[0].get('name', '?')}")
                else:
                    print("无在线服务器，跳过 exec 测试")
                    args.no_exec = True
            except Exception as e:
                print(f"获取服务器列表失败: {e}，跳过 exec 测试")
                args.no_exec = True
        else:
            args.no_exec = True

    stats = {"total": 0, "ok": 0, "fail": 0, "times": [], "errors": []}

    async with httpx.AsyncClient(limits=httpx.Limits(max_connections=concurrency + 100, max_keepalive_connections=concurrency)) as client:
        tasks = []

        # 分配权重: health 40%, stats 20%, health-services 10%, heartbeat 20%, exec 10%
        n_health = max(1, int(concurrency * 0.4))
        n_stats = max(1, int(concurrency * 0.2))
        n_hs = max(1, int(concurrency * 0.1))
        n_hb = max(1, int(concurrency * 0.2))
        n_exec = max(1, int(concurrency * 0.1))

        print(f"Worker: health={n_health} stats={n_stats} health-svc={n_hs} heartbeat={n_hb} exec={n_exec}")

        for _ in range(n_health):
            tasks.append(health_worker(client, duration))
        for _ in range(n_stats):
            tasks.append(stats_worker(client, duration))
        for _ in range(n_hs):
            tasks.append(health_services_worker(client, duration))
        for _ in range(n_hb):
            tasks.append(heartbeat_worker(client, duration))
        if not args.no_exec:
            for _ in range(n_exec):
                tasks.append(exec_worker(client, duration, SERVER_ID))

        print(f"启动 {len(tasks)} 个 worker...")
        t0 = time.time()
        await asyncio.gather(*tasks, return_exceptions=True)
        elapsed = time.time() - t0
        print_report(concurrency, elapsed)


if __name__ == "__main__":
    asyncio.run(main())
