"""M12 测试：身份切语料、强制出处、SOP 检索、引用生成 + P0-1 身份闸门。"""
import ast
import os
import re
import sys
from contextlib import contextmanager
from pathlib import Path

PKG_ROOT = Path(__file__).resolve().parents[1]
SRC = PKG_ROOT / "src"
sys.path.insert(0, str(SRC))

# P0-1：模拟部署侧注入身份。缺失即 fail-closed，故测试必须显式注入。
os.environ.setdefault("A207_CALLER", "doctor_assistant")

import a207_knowledge_mcp.core as core  # noqa: E402
from a207_policy import CallerUnknown, PermissionDenied  # noqa: E402


def check(name, cond, detail=""):
    print(("PASS" if cond else "FAIL"), name, detail)
    assert cond, f"{name} | {detail}"


@contextmanager
def as_caller(name):
    """临时切换部署身份（等价于换一个部署实例）。"""
    saved = os.environ.get("A207_CALLER")
    os.environ["A207_CALLER"] = name
    try:
        yield
    finally:
        if saved is None:
            os.environ.pop("A207_CALLER", None)
        else:
            os.environ["A207_CALLER"] = saved


def test_role_full_vs_popular():
    with as_caller("doctor_assistant"):
        d = core.search_guideline("蛋白目标")
    with as_caller("parent_assistant"):
        p = core.search_guideline("蛋白目标")
    check("doctor 命中 PRNT 蛋白条目", d["count"] >= 1 and d["results"][0]["id"] == "PRNT2020-PROTEIN", str(d["results"][0]["id"]))
    # 同条目不同身份返回不同语料
    did = next((r for r in d["results"] if r["id"] == "PRNT2020-PROTEIN"), None)
    pid = next((r for r in p["results"] if r["id"] == "PRNT2020-PROTEIN"), None)
    check("同条目 doctor/parent 语料不同", did and pid and did["text"] != pid["text"], f"{did['text'][:10]} != {pid['text'][:10]}")


def test_child_view():
    with as_caller("child_companion"):
        c = core.search_guideline("钾")
    check("child 视图返回科普语料", c["view"] == "child" and c["count"] >= 1, str(c["view"]))


def test_every_result_has_source():
    r = core.search_guideline("生长")
    check("所有结果带 source 出处", all(x["source"] for x in r["results"]), str([x["id"] for x in r["results"]]))
    check("所有结果带推荐强度+证据级别", all(x["strength"] and x["evidence"] for x in r["results"]))


def test_guideline_set_filter():
    r = core.search_guideline("能量", guideline_set="PRNT2020")
    check("按 set 过滤只返回 PRNT", r["count"] >= 1 and all(x["set"] == "PRNT2020" for x in r["results"]), str([x["set"] for x in r["results"]]))


def test_sop_search():
    r = core.search_sop("肌酐")
    check("SOP 检索命中 ACR-RIS", any(s["id"] == "SOP-ACR-RISE" for s in r["results"]), str([s["id"] for s in r["results"]]))
    check("SOP 带 source", all(s["source"] for s in r["results"]))


def test_citation_guideline():
    c = core.get_citation("KDIGO2024-G1-5")
    check("指南 id 生成引用串", c["citation"] and "KDIGO 2024" in c["citation"], str(c))
    check("引用含强度与证据级别", "强推荐" in c["citation"] and "1A" in c["citation"])


def test_citation_sop():
    c = core.get_citation("SOP-HYPERK-EMERG")
    check("SOP id 生成引用串", c["citation"] and "高钾血症" in c["citation"], str(c))


def test_citation_notfound():
    c = core.get_citation("NOPE")
    check("未知 id 返回 error", c.get("error") is not None and c["citation"] is None)


def test_all_refs_resolvable():
    refs = core._self_test_refs()
    bad = [r for r in refs if core.get_citation(r)["citation"] is None]
    check("所有 id 均可解析引用", not bad, str(bad))


def test_profile_view_per_caller():
    """身份 → 语料视图映射以 a207_policy.knowledge_profile 为准，患儿伙伴拿不到全量专业语料。"""
    seen = {}
    for who in ("doctor_assistant", "nutritionist", "risk_warning",
                "parent_assistant", "child_companion"):
        with as_caller(who):
            seen[who] = core.search_guideline("蛋白目标")["view"]
    check("临床三角色取全量语料 full",
          {seen["doctor_assistant"], seen["nutritionist"], seen["risk_warning"]} == {"full"},
          str(seen))
    check("家长助手取通俗语料 popular", seen["parent_assistant"] == "popular", str(seen))
    check("患儿伙伴取儿童语料 child（拿不到全量专业语料）",
          seen["child_companion"] == "child", str(seen))

    with as_caller("child_companion"):
        c = core.search_guideline("蛋白目标")
    with as_caller("doctor_assistant"):
        d = core.search_guideline("蛋白目标")
    cid = next((r for r in c["results"] if r["id"] == "PRNT2020-PROTEIN"), None)
    did = next((r for r in d["results"] if r["id"] == "PRNT2020-PROTEIN"), None)
    check("患儿伙伴正文 != 医生助手正文", cid and did and cid["text"] != did["text"],
          f"{cid['text'][:12]} vs {did['text'][:12]}")


def test_matrix_gate():
    """矩阵加闸：M12 对 orchestrator 为 '-'，越权必须抛 PermissionDenied。"""
    with as_caller("orchestrator"):
        denied = 0
        for fn, args in ((core.search_guideline, ("钾",)),
                         (core.search_sop, ("肌酐",)),
                         (core.get_citation, ("NOPE",))):
            try:
                fn(*args)
            except PermissionDenied:
                denied += 1
    check("orchestrator 越权三工具全部被拒", denied == 3, str(denied))


def test_p0_1_fail_closed():
    """身份未注入时任何工具一律拒绝（fail-closed）。"""
    saved = os.environ.pop("A207_CALLER", None)
    try:
        blocked = []
        for fn, args in ((core.search_guideline, ("钾",)),
                         (core.search_sop, ("肌酐",)),
                         (core.get_citation, ("NOPE",))):
            try:
                fn(*args)
            except CallerUnknown:
                blocked.append(fn.__name__)
        check("P0-1 无身份注入时三个工具全部 fail-closed", len(blocked) == 3, str(blocked))
    finally:
        if saved is not None:
            os.environ["A207_CALLER"] = saved


def test_server_no_identity_param():
    """P0-1 身份来源唯一：server 工具签名不得暴露 caller/role 形参（AST 校验）。"""
    tree = ast.parse((SRC / "a207_knowledge_mcp" / "server.py").read_text(encoding="utf-8"))
    offenders = []
    for node in ast.walk(tree):
        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            continue
        a = node.args
        names = [x.arg for x in (*a.posonlyargs, *a.args, *a.kwonlyargs)]
        if "caller" in names or "role" in names:
            offenders.append(node.name)
    check("server 工具层无 caller/role 形参", not offenders, str(offenders))


def test_zero_cross_package_import():
    """零跨包引用：a207_policy 是统一策略共享包（非领域 MCP），允许引用。"""
    pattern = re.compile(r"(?:^|\s)(?:import|from)\s+a207_(?!knowledge_mcp|policy)", re.M)
    offenders = [
        str(p.relative_to(PKG_ROOT))
        for p in PKG_ROOT.rglob("*.py")
        if "__pycache__" not in p.parts and pattern.search(p.read_text(encoding="utf-8"))
    ]
    check("零跨包 import（policy 除外）", not offenders, str(offenders))


def main():
    test_role_full_vs_popular()
    test_child_view()
    test_every_result_has_source()
    test_guideline_set_filter()
    test_sop_search()
    test_citation_guideline()
    test_citation_sop()
    test_citation_notfound()
    test_all_refs_resolvable()
    test_profile_view_per_caller()
    test_matrix_gate()
    test_p0_1_fail_closed()
    test_server_no_identity_param()
    test_zero_cross_package_import()
    print("\nALL M12 TESTS PASSED")


if __name__ == "__main__":
    main()
