# Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved.
# MindIE is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#         `http://license.coscl.org.cn/MulanPSL2`
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.

"""
扫描所有已导入的 BoxHandler / Limit 子类:
- 如果子类重写了 sync 方法且源码中含阻塞 IO 调用
- 但未重写对应的 async_* 方法
- → 视为违反契约,PR 阶段拦截

豁免机制:在类上加 `allow_default_async_delegation(reason="...")` 装饰器即可跳过检查。
"""
import ast
import importlib
import inspect
import pkgutil
import unittest
from typing import List, Tuple

import olc
from olc.control.handler_chain.box_handler import BoxHandler
from olc.limit.limiter import Limit


# 视为"阻塞 IO 标志"的符号(最小可识别集 —— 误报可加豁免)
BLOCKING_IO_SYMBOLS = {
    "requests",
    "urlopen",
    "urllib",
    "socket",  # 注意:模块整体;仅当出现 socket.recv / socket.send 等才算
}

# "间接 IO" 方法名:BoxHandler 子类在同步方法中如果调用了这些方法
# (通常通过 OlcRateLimiterFactory 拿到的 limiter),就视为含间接 IO,
# 必须显式覆盖 async_* 以走对应的 async_xxx 方法。
LIMITER_SYNC_METHODS = {
    "try_acquire",
    "pre_check",
    "roll_back",
    "dec_token",
}

# 已知"内部其实是异步友好"或"虽含 IO 但已通过 RedisManager 抽象"的豁免类全名(点分路径)。
# 这是合规白名单,避免对已正确实现的类误报。
EXEMPT_CLASSES = {
    # AbsClusterLimiter 已在 P4 阶段显式覆盖 async_try_acquire / async_roll_back
    # 子类只暴露 _get_script_key(),本身不直接调 IO 符号 —— 不会被本扫描误判
}


def allow_default_async_delegation(reason: str):
    """装饰器:明确豁免某个子类不需要覆盖 async_*。

    必须给出 reason —— 便于审查时判断豁免是否仍然成立。
    """
    if not reason or not isinstance(reason, str):
        raise ValueError("reason must be a non-empty string")

    def decorator(cls):
        cls.__olc_async_delegation_exempt__ = reason
        return cls

    return decorator


def _iter_subclasses(base):
    seen = set()
    stack = [base]
    while stack:
        cls = stack.pop()
        for sub in cls.__subclasses__():
            if sub in seen:
                continue
            seen.add(sub)
            stack.append(sub)
            yield sub


def _import_all_modules(package):
    for _, name, _ in pkgutil.walk_packages(
        package.__path__, prefix=package.__name__ + "."
    ):
        try:
            importlib.import_module(name)
        except Exception:
            # 某些可选模块(如 fastapi adapter)可能因依赖缺失导入失败 —— 跳过
            pass


def _source_calls_blocking_io(source: str) -> List[str]:
    """简单 AST 扫描:识别源码中疑似阻塞 IO 的调用"""
    hits = []
    try:
        tree = ast.parse(source)
    except SyntaxError:
        return hits

    for node in ast.walk(tree):
        # requests.get / requests.post 等
        if isinstance(node, ast.Attribute):
            full = _dotted(node)
            if full:
                head = full.split(".")[0]
                if head in BLOCKING_IO_SYMBOLS and head != "socket":
                    hits.append(full)
                elif head == "socket" and full in (
                    "socket.recv",
                    "socket.send",
                    "socket.sendall",
                    "socket.recvfrom",
                ):
                    hits.append(full)
        # 直接 import requests; requests(...)
        elif isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name) and node.func.id in BLOCKING_IO_SYMBOLS:
                hits.append(node.func.id)
    return hits


def _source_calls_limiter_methods(source: str) -> List[str]:
    """专用扫描:识别 BoxHandler 间接调用 limiter 同步方法的场景。

    例如 FlowHandler.in_coming 里 `limiter.try_acquire(n)` 这种调用。
    """
    hits = []
    try:
        tree = ast.parse(source)
    except SyntaxError:
        return hits
    for node in ast.walk(tree):
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
            if node.func.attr in LIMITER_SYNC_METHODS:
                hits.append(node.func.attr)
    return hits


def _dotted(node):
    parts = []
    cur = node
    while isinstance(cur, ast.Attribute):
        parts.append(cur.attr)
        cur = cur.value
    if isinstance(cur, ast.Name):
        parts.append(cur.id)
        return ".".join(reversed(parts))
    return None


def _async_override_missing(cls, sync_name: str, async_name: str) -> bool:
    """sync 在 cls 自己有重写,但 async 仍然停留在 ABC 默认实现"""
    sync_owner = next((c for c in cls.__mro__ if sync_name in c.__dict__), None)
    async_owner = next((c for c in cls.__mro__ if async_name in c.__dict__), None)
    if sync_owner is None:
        return False
    return sync_owner is cls and async_owner is not cls


CONTRACT_PAIRS_HANDLER = [
    ("do_in_coming", "async_do_in_coming"),
    ("do_out_coming", "async_do_out_coming"),
]
CONTRACT_PAIRS_LIMIT = [
    ("try_acquire", "async_try_acquire"),
    ("pre_check", "async_pre_check"),
    ("roll_back", "async_roll_back"),
    ("dec_token", "async_dec_token"),
]


def _collect_violations(
    base, pairs, extra_detector=None
) -> List[Tuple[type, str, List[str]]]:
    """收集违规:sync 方法在子类重写、但 async 仍走 ABC 默认委托。

    含 IO 的判定:默认走 _source_calls_blocking_io;如果传入 extra_detector,
    则两个判定**或**关系(任何一个识别到 IO 信号即纳入候选)。
    """
    violations = []
    for cls in _iter_subclasses(base):
        if inspect.isabstract(cls):
            continue
        full_name = f"{cls.__module__}.{cls.__qualname__}"
        if full_name in EXEMPT_CLASSES:
            continue
        if getattr(cls, "__olc_async_delegation_exempt__", None):
            continue
        try:
            src = inspect.getsource(cls)
        except (OSError, TypeError):
            continue
        blocking_hits = _source_calls_blocking_io(src)
        if extra_detector is not None:
            blocking_hits = blocking_hits + extra_detector(src)
        if not blocking_hits:
            continue
        for sync_name, async_name in pairs:
            if _async_override_missing(cls, sync_name, async_name):
                violations.append((cls, async_name, blocking_hits))
    return violations


def _format_violations(violations) -> str:
    lines = ["以下类含阻塞 IO 但未覆盖对应 async_* 方法:"]
    for cls, async_name, hits in violations:
        lines.append(
            f"  - {cls.__module__}.{cls.__qualname__}: "
            f"缺失 {async_name},疑似 IO 调用 = {sorted(set(hits))}"
        )
    lines.append(
        "\n请显式覆盖对应的 async_* 方法走真异步路径,"
        "或加 @allow_default_async_delegation(reason='...') 装饰器豁免。"
    )
    return "\n".join(lines)


class TestAsyncContract(unittest.TestCase):
    """spec §14.2:异步契约的 CI 强制检查"""

    @classmethod
    def setUpClass(cls):
        _import_all_modules(olc)

    def test_handler_async_contract(self):
        # 双重判定:直接 IO(requests/urllib/socket) + 间接 IO(调用 limiter.*)
        violations = _collect_violations(
            BoxHandler,
            CONTRACT_PAIRS_HANDLER,
            extra_detector=_source_calls_limiter_methods,
        )
        self.assertFalse(violations, _format_violations(violations))

    def test_limit_async_contract(self):
        violations = _collect_violations(Limit, CONTRACT_PAIRS_LIMIT)
        self.assertFalse(violations, _format_violations(violations))


# ===================== 自检:装饰器与扫描逻辑本身的单元测试 =====================


class TestContractInfrastructure(unittest.TestCase):
    """验证 _collect_violations / allow_default_async_delegation 装饰器本身可用"""

    def test_decorator_requires_reason(self):
        with self.assertRaises(ValueError):
            allow_default_async_delegation("")

    def test_decorator_sets_marker(self):
        @allow_default_async_delegation(reason="test")
        class Foo:
            pass

        self.assertEqual(Foo.__olc_async_delegation_exempt__, "test")

    def test_blocking_io_detection_positive(self):
        src = "def x():\n    requests.get('http://x')\n"
        hits = _source_calls_blocking_io(src)
        self.assertTrue(any("requests" in h for h in hits))

    def test_blocking_io_detection_negative(self):
        src = "def x():\n    return 1 + 2\n"
        hits = _source_calls_blocking_io(src)
        self.assertEqual(hits, [])

    def test_limiter_method_detection_positive(self):
        src = "def x():\n    limiter.try_acquire(1)\n    limiter.dec_token(3)\n"
        hits = _source_calls_limiter_methods(src)
        self.assertIn("try_acquire", hits)
        self.assertIn("dec_token", hits)

    def test_limiter_method_detection_negative(self):
        src = "def x():\n    return 1 + 2\n"
        hits = _source_calls_limiter_methods(src)
        self.assertEqual(hits, [])


if __name__ == "__main__":
    unittest.main(verbosity=2)