"""
tests/test_mcp_registry.py — MCP server 注册中心 P1（docs/MCP_SKILL_DESIGN.md）。

覆盖：
- 注册中心 CRUD（文件锁 json 权威源）、校验、脱敏。
- stdio argv allowlist（评审#16）。
- 风险分级 medium 默认 + 破坏性动词升 high（评审#15）。
- inject_into_config 只注入启用的 http server（评审#14 禁用即不可用）。
- ToolGateway MCP 风险修复。
- API 端点 loopback 门 + CRUD + probe。
"""
from __future__ import annotations

import json
import os

import pytest

os.environ.setdefault("AGENTPAAS_DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AGENTPAAS_TESTING", "1")


@pytest.fixture()
def reg(tmp_path, monkeypatch):
    """隔离 MCP_FILE 到 tmp，避免动到真实 ~/.agentpaas/mcp_servers.json。"""
    from agentpaas.engine import mcp_registry as r
    monkeypatch.setattr(r, "MCP_FILE", str(tmp_path / "mcp_servers.json"))
    return r


# ── CRUD + 校验 ──

def test_upsert_list_get_remove(reg):
    reg.upsert_server({
        "id": "arxiv", "name": "arXiv", "transport": "http",
        "url": "http://localhost:9000/mcp",
        "auth": {"kind": "env_bearer", "env_key": "ARXIV_KEY"},
    })
    lst = reg.list_servers()
    assert len(lst) == 1 and lst[0]["id"] == "arxiv"
    got = reg.get_server("arxiv")
    assert got["url"] == "http://localhost:9000/mcp"
    assert got["auth"]["env_key"] == "ARXIV_KEY"   # 变量名保留
    assert "value" not in got["auth"]              # 评审#13: 永不出现凭证值
    assert reg.set_enabled("arxiv", False)
    assert reg.get_server("arxiv")["enabled"] is False
    assert reg.remove_server("arxiv")
    assert reg.get_server("arxiv") is None


def test_validate_rejects_bad(reg):
    assert reg.validate_server({"id": "x", "transport": "http"})  # 缺 url
    assert reg.validate_server({"id": "x y", "transport": "http", "url": "u"})  # 非法 id
    assert reg.validate_server({"id": "x", "transport": "ftp"})  # 非法 transport


def test_stdio_argv_allowlist(reg):
    # rm 不在 allowlist → 拒绝
    errs = reg.validate_server({"id": "evil", "transport": "stdio",
                                "command_argv": ["rm", "-rf", "/"]})
    assert any("allowlist" in e for e in errs)
    # npx 在 allowlist → 通过
    assert reg.validate_server({"id": "ok", "transport": "stdio",
                                "command_argv": ["npx", "some-mcp"]}) == []
    # 显式加白
    assert reg.validate_server({"id": "ok2", "transport": "stdio",
                                "command_argv": ["mybin"]},
                               argv_allowlist=reg.DEFAULT_ARGV_ALLOWLIST | {"mybin"}) == []


def test_risk_classification(reg):
    ov = {"special_tool": "low"}
    assert reg.classify_mcp_tool("search_papers", {}) == "medium"   # 默认 medium
    assert reg.classify_mcp_tool("delete_paper", {}) == "high"      # 动词升 high
    assert reg.classify_mcp_tool("send_email", {}) == "high"
    assert reg.classify_mcp_tool("special_tool", ov) == "low"       # override 优先


# ── 运行时注入桥（评审#14）──

def test_inject_only_enabled_http(reg):
    reg.upsert_server({"id": "on", "transport": "http", "url": "http://h/mcp",
                       "auth": {"kind": "none"}})
    reg.upsert_server({"id": "off", "transport": "http", "url": "http://h2/mcp",
                       "auth": {"kind": "none"}, "enabled": False})
    reg.upsert_server({"id": "stdio1", "transport": "stdio",
                       "command_argv": ["npx", "x"]})
    cfg = reg.inject_into_config({"name": "a", "type": "simple"})
    nodes = cfg["app"]["mcp"]["custom"]["nodes"]
    assert "on" in nodes                 # 启用的 http 注入
    assert "off" not in nodes            # 禁用的不注入（评审#14）
    assert "stdio1" not in nodes         # P1 运行时桥不接 stdio


def test_inject_credential_from_env(reg, monkeypatch):
    monkeypatch.setenv("MY_MCP_KEY", "secret123")
    reg.upsert_server({"id": "s", "transport": "http", "url": "http://h/mcp",
                       "auth": {"kind": "env_bearer", "env_key": "MY_MCP_KEY"}})
    cfg = reg.inject_into_config({"name": "a"})
    hdr = cfg["app"]["mcp"]["custom"]["nodes"]["s"]["headers"]
    assert hdr["Authorization"] == "Bearer secret123"


def test_inject_no_servers_passthrough(reg):
    cfg = {"name": "a", "type": "simple"}
    assert reg.inject_into_config(cfg) == cfg


# ── ToolGateway MCP 风险修复（评审#15）──

def test_gateway_mcp_risk():
    from lambdagent.tool_gateway import classify_tool_call, RiskLevel
    assert classify_tool_call("mcp_search", "")[0] == RiskLevel.MEDIUM
    assert classify_tool_call("arxiv.search", "")[0] == RiskLevel.MEDIUM
    assert classify_tool_call("mcp_delete_file", "")[0] == RiskLevel.HIGH
    assert classify_tool_call("github.create_issue", "")[0] == RiskLevel.HIGH


# ── API 端点 ──

@pytest.fixture()
def api(tmp_path, monkeypatch):
    import secrets
    from fastapi.testclient import TestClient
    from agentpaas.api.app import app
    from agentpaas.api.middleware.auth import hash_key
    from agentpaas.db.models import Database, gen_id, now_utc
    from agentpaas.engine import mcp_registry as r
    import agentpaas.db.session as _session_mod
    monkeypatch.setattr(r, "MCP_FILE", str(tmp_path / "mcp.json"))
    prev = _session_mod._db
    _session_mod._db = Database("sqlite:///:memory:")
    db = _session_mod._db
    tid, uid = gen_id("tn_"), gen_id("usr_")
    raw = f"ap_{secrets.token_hex(16)}"
    now = now_utc()
    db.execute("INSERT INTO tenants (id,name,plan,status,created_at) VALUES (?,'t','free','active',?)", (tid, now))
    db.execute("INSERT INTO users (id,tenant_id,email,role,created_at) VALUES (?,?,'','admin',?)", (uid, tid, now))
    db.execute("INSERT INTO api_keys (id,tenant_id,user_id,key_hash,key_prefix,name,scopes,rate_limit,status,created_at) "
               "VALUES (?,?,?,?,?,'t',?,600,'active',?)",
               (gen_id("key_"), tid, uid, hash_key(raw), raw[:8], json.dumps(["agents:*"]), now))
    db.commit()
    with TestClient(app) as c:
        yield c, raw
    _session_mod._db = prev


def _auth(k):
    return {"Authorization": f"Bearer {k}"}


def test_api_crud_and_list(api):
    c, key = api
    r = c.post("/api/v1/mcp", headers=_auth(key),
               json={"id": "arxiv", "name": "arXiv", "transport": "http",
                     "url": "http://localhost:9000/mcp"})
    assert r.status_code == 201, r.text[:200]
    lst = c.get("/api/v1/mcp", headers=_auth(key)).json()
    assert any(s["id"] == "arxiv" for s in lst["servers"])
    # 启停
    assert c.put("/api/v1/mcp/arxiv", headers=_auth(key), json={"enabled": False}).status_code == 200
    assert c.get("/api/v1/mcp", headers=_auth(key)).json()["servers"][0]["enabled"] is False
    assert c.delete("/api/v1/mcp/arxiv", headers=_auth(key)).status_code == 200


def test_api_add_rejects_bad_argv(api):
    c, key = api
    r = c.post("/api/v1/mcp", headers=_auth(key),
               json={"id": "evil", "transport": "stdio", "command_argv": ["rm", "-rf", "/"]})
    assert r.status_code == 400 and "allowlist" in r.json()["detail"]


def test_api_probe_unreachable(api):
    c, key = api
    c.post("/api/v1/mcp", headers=_auth(key),
           json={"id": "dead", "transport": "http", "url": "http://127.0.0.1:1/mcp"})
    r = c.post("/api/v1/mcp/dead/probe", headers=_auth(key))
    assert r.status_code == 200 and r.json()["status"] == "unreachable"


def test_api_probe_unknown_404(api):
    c, key = api
    assert c.post("/api/v1/mcp/nope/probe", headers=_auth(key)).status_code == 404
