"""Tests for MCP protocol-specific security rules (MCP-005 through MCP-019).


Tests cover all 15 new rules with positive/negative cases, edge cases,
confidence levels, and metadata validation.
"""

import ast

import pytest

from mcpaudit.rules.mcp_rules import (
    MCPBatchCallsNoRateLimitRule,
    MCPConfigHotReloadRule,
    MCPInternalPathExposureRule,
    MCPMissingTransportEncryptionRule,
    MCPNotificationArbitraryDataRule,
    MCPResourceListUnrestrictedRule,
    MCPSSEWithoutOriginCheckRule,
    MCPServerAuthCheckRule,
    MCPServerVersionDisclosureRule,
    MCPToolCallLoggingRule,
    MCPToolInputValidationRule,
    MCPToolNameCollisionRule,
    MCPToolResponseSizeRule,
    MCPToolSchemaMissingFieldsRule,
    MCPUnauthenticatedConnectionRule,
    get_all_mcp_rules,
)


def _check(rule, code: str):
    """Parse code and run rule check."""
    tree = ast.parse(code)
    return rule.check(tree, code)


# ---------------------------------------------------------------------------
# MCP-005: Tool Handler Missing Input Validation
# ---------------------------------------------------------------------------

class TestMCPToolInputValidation:
    def setup_method(self):
        self.rule = MCPToolInputValidationRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-005"
        assert self.rule.severity.value == "HIGH"
        assert self.rule.cwe == "CWE-20"

    def test_tool_without_validation_detected(self):
        code = '''
def handle_tool_read(arguments):
    path = arguments["path"]
    return open(path).read()
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-005"

    def test_tool_with_isinstance_passes(self):
        code = '''
def handle_tool_read(arguments):
    if not isinstance(arguments["path"], str):
        raise ValueError("path must be string")
    return open(arguments["path"]).read()
'''
        assert len(_check(self.rule, code)) == 0

    def test_tool_with_pydantic_passes(self):
        code = '''
def handle_tool_read(arguments):
    data = pydantic.validate(arguments)
    return data.path
'''
        assert len(_check(self.rule, code)) == 0

    def test_tool_with_schema_passes(self):
        code = '''
def handle_tool_read(arguments):
    schema.validate(arguments)
    return arguments["path"]
'''
        assert len(_check(self.rule, code)) == 0

    def test_non_tool_function_ignored(self):
        code = '''
def process_data(arguments):
    return arguments["key"]
'''
        assert len(_check(self.rule, code)) == 0

    def test_tool_without_params_ignored(self):
        code = '''
def handle_tool_ping():
    return "pong"
'''
        assert len(_check(self.rule, code)) == 0

    def test_decorated_tool_detected(self):
        code = '''
@server.tool
def my_func(arguments):
    return arguments["path"]
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_async_tool_detected(self):
        code = '''
async def handle_tool_fetch(request):
    url = request["url"]
    return await fetch(url)
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1


# ---------------------------------------------------------------------------
# MCP-006: Tool Response Size Not Limited
# ---------------------------------------------------------------------------

class TestMCPToolResponseSize:
    def setup_method(self):
        self.rule = MCPToolResponseSizeRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-006"
        assert self.rule.severity.value == "MEDIUM"
        assert self.rule.cwe == "CWE-770"

    def test_unlimited_read_detected(self):
        code = '''
def handle_tool_read(path):
    return open(path).read()
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-006"

    def test_unlimited_readlines_detected(self):
        code = '''
def handle_tool_lines(path):
    return open(path).readlines()
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_unlimited_fetchall_detected(self):
        code = '''
def handle_tool_query(query):
    cursor.run(query)
    return cursor.fetchall()
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_limited_response_passes(self):
        code = '''
def handle_tool_read(path):
    data = open(path).read()
    return data[:max_size]
'''
        assert len(_check(self.rule, code)) == 0

    def test_truncated_response_passes(self):
        code = '''
def handle_tool_read(path):
    data = open(path).read()
    return truncate(data)
'''
        assert len(_check(self.rule, code)) == 0

    def test_non_tool_function_ignored(self):
        code = '''
def helper_read(path):
    return open(path).read()
'''
        assert len(_check(self.rule, code)) == 0


# ---------------------------------------------------------------------------
# MCP-007: Server Lacks Authentication Check
# ---------------------------------------------------------------------------

class TestMCPServerAuthCheck:
    def setup_method(self):
        self.rule = MCPServerAuthCheckRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-007"
        assert self.rule.severity.value == "CRITICAL"
        assert self.rule.cwe == "CWE-306"

    def test_handler_without_auth_detected(self):
        code = '''
from mcp import Server
async def handle_request(msg):
    return process(msg)
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-007"

    def test_handler_with_auth_passes(self):
        code = '''
from mcp import Server
async def handle_request(msg):
    verify_token(msg.headers)
    return process(msg)
'''
        assert len(_check(self.rule, code)) == 0

    def test_non_server_file_ignored(self):
        code = '''
def handle_request(msg):
    return process(msg)
'''
        assert len(_check(self.rule, code)) == 0

    def test_on_connect_without_auth(self):
        code = '''
Server()
async def on_connect(client):
    sessions.add(client)
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_serve_with_auth_passes(self):
        code = '''
Server()
async def serve(request):
    authenticate(request)
    return data
'''
        assert len(_check(self.rule, code)) == 0


# ---------------------------------------------------------------------------
# MCP-008: Resource Listing Unrestricted
# ---------------------------------------------------------------------------

class TestMCPResourceListUnrestricted:
    def setup_method(self):
        self.rule = MCPResourceListUnrestrictedRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-008"
        assert self.rule.severity.value == "MEDIUM"
        assert self.rule.cwe == "CWE-284"

    def test_unrestricted_listing_detected(self):
        code = '''
def list_resources():
    return all_resources
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_restricted_listing_passes(self):
        code = '''
def list_resources(user):
    allowed = check_access(user, all_resources)
    return allowed
'''
        assert len(_check(self.rule, code)) == 0

    def test_list_tools_unrestricted(self):
        code = '''
def list_tools():
    return tool_registry.all()
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_get_resources_with_permission(self):
        code = '''
def get_resources(caller):
    if not permission.check(caller):
        raise Forbidden()
    return resources
'''
        assert len(_check(self.rule, code)) == 0

    def test_non_listing_function_ignored(self):
        code = '''
def process_item(item):
    return item
'''
        assert len(_check(self.rule, code)) == 0


# ---------------------------------------------------------------------------
# MCP-009: Server Exposes Internal Paths
# ---------------------------------------------------------------------------

class TestMCPInternalPathExposure:
    def setup_method(self):
        self.rule = MCPInternalPathExposureRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-009"
        assert self.rule.severity.value == "MEDIUM"
        assert self.rule.cwe == "CWE-200"

    def test_internal_path_in_return_detected(self):
        code = '''
Server()
def get_info():
    return "/home/user/secrets/config.yml"
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-009"

    def test_windows_path_in_response_detected(self):
        code = '''
Server()
def get_path():
    response = "C:\\\\Users\\\\admin\\\\data"
    return response
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_path_not_in_response_ignored(self):
        code = '''
Server()
config_path = "/home/user/config.yml"
def process():
    pass
'''
        assert len(_check(self.rule, code)) == 0

    def test_non_server_file_ignored(self):
        code = '''
def get_info():
    return "/home/user/config.yml"
'''
        assert len(_check(self.rule, code)) == 0

    def test_comment_ignored(self):
        code = '''
Server()
# return "/home/user/secrets/config.yml"
def process():
    pass
'''
        assert len(_check(self.rule, code)) == 0


# ---------------------------------------------------------------------------
# MCP-010: Tool Name Collision with System Tools
# ---------------------------------------------------------------------------

class TestMCPToolNameCollision:
    def setup_method(self):
        self.rule = MCPToolNameCollisionRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-010"
        assert self.rule.severity.value == "HIGH"
        assert self.rule.cwe == "CWE-441"

    def test_exec_tool_name_detected(self):
        code = '''
@server.tool(name="exec")
def run_code(code):
    return evaluate(code)
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert "exec" in findings[0].description

    def test_shell_function_name_detected(self):
        code = '''
@server.tool
def shell(cmd):
    return subprocess.run(cmd)
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_safe_tool_name_passes(self):
        code = '''
@server.tool(name="myapp_search")
def search_docs(query):
    return results
'''
        assert len(_check(self.rule, code)) == 0

    def test_system_name_in_decorator_arg(self):
        code = '''
@server.tool("delete")
def remove_item(item_id):
    return db.delete(item_id)
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_non_tool_function_ignored(self):
        code = '''
def my_exec(code):
    return evaluate(code)
'''
        assert len(_check(self.rule, code)) == 0

    def test_sudo_tool_detected(self):
        code = '''
@server.tool(name="sudo")
def elevate(cmd):
    return run_as_root(cmd)
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_read_file_tool_detected(self):
        code = '''
@server.tool(name="read_file")
def read(path):
    return open(path).read()
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1


# ---------------------------------------------------------------------------
# MCP-011: Missing Transport Encryption
# ---------------------------------------------------------------------------

class TestMCPMissingTransportEncryption:
    def setup_method(self):
        self.rule = MCPMissingTransportEncryptionRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-011"
        assert self.rule.severity.value == "HIGH"
        assert self.rule.cwe == "CWE-319"

    def test_http_url_detected(self):
        code = '''
Server()
url = "http://api.example.com/data"
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-011"

    def test_https_url_passes(self):
        code = '''
Server()
url = "https://api.example.com/data"
'''
        assert len(_check(self.rule, code)) == 0

    def test_localhost_http_ignored(self):
        code = '''
Server()
url = "http://localhost:8080/api"
'''
        assert len(_check(self.rule, code)) == 0

    def test_127_0_0_1_http_ignored(self):
        code = '''
Server()
url = "http://127.0.0.1:3000/api"
'''
        assert len(_check(self.rule, code)) == 0

    def test_non_server_file_ignored(self):
        code = '''
url = "http://api.example.com/data"
'''
        assert len(_check(self.rule, code)) == 0

    def test_comment_ignored(self):
        code = '''
Server()
# url = "http://api.example.com/data"
def process():
    pass
'''
        assert len(_check(self.rule, code)) == 0


# ---------------------------------------------------------------------------
# MCP-012: Server Version Disclosure
# ---------------------------------------------------------------------------

class TestMCPServerVersionDisclosure:
    def setup_method(self):
        self.rule = MCPServerVersionDisclosureRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-012"
        assert self.rule.severity.value == "LOW"
        assert self.rule.cwe == "CWE-200"

    def test_version_in_response_detected(self):
        code = '''
Server()
def get_capabilities():
    return {"server_version": "1.2.3", "capabilities": []}
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_version_in_metadata_detected(self):
        code = '''
Server()
def info():
    return {"version": "2.0.1", "capabilities": []}
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_no_version_passes(self):
        code = '''
Server()
def get_info():
    return {"name": "my-server"}
'''
        assert len(_check(self.rule, code)) == 0

    def test_non_server_file_ignored(self):
        code = '''
version = "1.0.0"
'''
        assert len(_check(self.rule, code)) == 0


# ---------------------------------------------------------------------------
# MCP-013: Tool Schema Missing Required Fields
# ---------------------------------------------------------------------------

class TestMCPToolSchemaMissingFields:
    def setup_method(self):
        self.rule = MCPToolSchemaMissingFieldsRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-013"
        assert self.rule.severity.value == "MEDIUM"
        assert self.rule.cwe == "CWE-1059"

    def test_missing_description_detected(self):
        code = '''
tool = {"name": "read_file", "inputSchema": {"type": "object"}}
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert "description" in findings[0].description

    def test_missing_input_schema_detected(self):
        code = '''
tool = {"name": "search", "description": "Search docs"}
handler = tool
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert "inputSchema" in findings[0].description

    def test_complete_schema_passes(self):
        code = '''
tool = {"name": "search", "description": "Search docs", "inputSchema": {"type": "object"}}
'''
        assert len(_check(self.rule, code)) == 0

    def test_parameters_alternative_passes(self):
        code = '''
tool = {"name": "search", "description": "Search", "parameters": {"type": "object"}}
'''
        assert len(_check(self.rule, code)) == 0

    def test_non_tool_dict_ignored(self):
        code = '''
config = {"name": "my-app", "port": 8080}
'''
        assert len(_check(self.rule, code)) == 0


# ---------------------------------------------------------------------------
# MCP-014: Notification Handler Accepts Arbitrary Data
# ---------------------------------------------------------------------------

class TestMCPNotificationArbitraryData:
    def setup_method(self):
        self.rule = MCPNotificationArbitraryDataRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-014"
        assert self.rule.severity.value == "HIGH"
        assert self.rule.cwe == "CWE-20"

    def test_unvalidated_notification_detected(self):
        code = '''
async def handle_notification(data):
    process(data["payload"])
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-014"

    def test_validated_notification_passes(self):
        code = '''
async def handle_notification(data):
    schema.validate(data)
    process(data["payload"])
'''
        assert len(_check(self.rule, code)) == 0

    def test_isinstance_validation_passes(self):
        code = '''
def on_notify(event):
    if not isinstance(event, dict):
        raise ValueError("bad event")
    process(event)
'''
        assert len(_check(self.rule, code)) == 0

    def test_non_notification_function_ignored(self):
        code = '''
def process_data(data):
    return data
'''
        assert len(_check(self.rule, code)) == 0

    def test_on_event_detected(self):
        code = '''
async def on_event(payload):
    store(payload)
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1


# ---------------------------------------------------------------------------
# MCP-015: Server Accepts Unauthenticated Connections
# ---------------------------------------------------------------------------

class TestMCPUnauthenticatedConnection:
    def setup_method(self):
        self.rule = MCPUnauthenticatedConnectionRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-015"
        assert self.rule.severity.value == "CRITICAL"
        assert self.rule.cwe == "CWE-306"

    def test_network_server_without_auth_detected(self):
        code = '''
server = Server()
server.run(host="0.0.0.0", port=8080, transport="sse")
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-015"

    def test_network_server_with_auth_passes(self):
        code = '''
server = Server()
server.add_middleware(auth_middleware)
server.run(host="0.0.0.0", port=8080, transport="sse")
'''
        assert len(_check(self.rule, code)) == 0

    def test_stdio_server_ignored(self):
        code = '''
server = Server()
server.run()
'''
        assert len(_check(self.rule, code)) == 0

    def test_websocket_without_auth_detected(self):
        code = '''
app.start(port=3000, websocket=True)
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1

    def test_server_with_token_passes(self):
        code = '''
server.configure(api_key=API_KEY)
server.run(host="0.0.0.0", port=8080)
'''
        assert len(_check(self.rule, code)) == 0


# ---------------------------------------------------------------------------
# MCP-016: Tool Call Logging Insufficient
# ---------------------------------------------------------------------------

class TestMCPToolCallLogging:
    def setup_method(self):
        self.rule = MCPToolCallLoggingRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-016"
        assert self.rule.severity.value == "LOW"
        assert self.rule.cwe == "CWE-778"

    def test_tool_without_logging_detected(self):
        code = '''
def handle_tool_search(query):
    return db.search(query)
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-016"

    def test_tool_with_logger_passes(self):
        code = '''
def handle_tool_search(query):
    logger.info("Searching for: %s", query)
    return db.search(query)
'''
        assert len(_check(self.rule, code)) == 0

    def test_tool_with_print_passes(self):
        code = '''
def handle_tool_read(path):
    print("Reading file:", path)
    return open(path).read()
'''
        assert len(_check(self.rule, code)) == 0

    def test_tool_with_logging_module_passes(self):
        code = '''
def handle_tool_run(cmd):
    logging.warning("Running: %s", cmd)
    return subprocess.run(cmd)
'''
        assert len(_check(self.rule, code)) == 0

    def test_non_tool_function_ignored(self):
        code = '''
def helper(x):
    return x * 2
'''
        assert len(_check(self.rule, code)) == 0


# ---------------------------------------------------------------------------
# MCP-017: Server-Sent Events Without Origin Check
# ---------------------------------------------------------------------------

class TestMCPSSEWithoutOriginCheck:
    def setup_method(self):
        self.rule = MCPSSEWithoutOriginCheckRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-017"
        assert self.rule.severity.value == "HIGH"
        assert self.rule.cwe == "CWE-346"

    def test_sse_without_origin_detected(self):
        code = '''
server = Server(transport="sse")
server.start()
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-017"

    def test_sse_with_cors_passes(self):
        code = '''
server = Server(transport="sse")
server.add_cors(allowed_origins=["https://example.com"])
server.start()
'''
        assert len(_check(self.rule, code)) == 0

    def test_sse_with_origin_check_passes(self):
        code = '''
server = Server(transport="sse")
server.set_origin_check(validate_origin)
server.start()
'''
        assert len(_check(self.rule, code)) == 0

    def test_no_sse_ignored(self):
        code = '''
server = Server(transport="stdio")
server.start()
'''
        assert len(_check(self.rule, code)) == 0

    def test_eventsource_detected(self):
        code = '''
app.route("/events", content_type="text/event-stream")
def events():
    yield data
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1


# ---------------------------------------------------------------------------
# MCP-018: Batch Tool Calls Without Rate Limit
# ---------------------------------------------------------------------------

class TestMCPBatchCallsNoRateLimit:
    def setup_method(self):
        self.rule = MCPBatchCallsNoRateLimitRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-018"
        assert self.rule.severity.value == "MEDIUM"
        assert self.rule.cwe == "CWE-770"

    def test_batch_without_rate_limit_detected(self):
        code = '''
async def handle_batch(calls):
    results = []
    for call in calls:
        results.append(await run_tool(call))
    return results
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-018"

    def test_batch_with_rate_limit_passes(self):
        code = '''
async def handle_batch(calls):
    rate_limit.check()
    results = []
    for call in calls:
        results.append(await run_tool(call))
    return results
'''
        assert len(_check(self.rule, code)) == 0

    def test_batch_with_semaphore_passes(self):
        code = '''
async def handle_batch(calls):
    async with semaphore:
        results = await asyncio.gather(*[run_tool(c) for c in calls])
    return results
'''
        assert len(_check(self.rule, code)) == 0

    def test_no_batch_ignored(self):
        code = '''
async def handle_call(call):
    return await run_tool(call)
'''
        assert len(_check(self.rule, code)) == 0

    def test_bulk_without_throttle_detected(self):
        code = '''
def bulk_process(operations):
    return [run(op) for op in operations]
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1


# ---------------------------------------------------------------------------
# MCP-019: Config Hot-Reload Without Validation
# ---------------------------------------------------------------------------

class TestMCPConfigHotReload:
    def setup_method(self):
        self.rule = MCPConfigHotReloadRule()

    def test_metadata(self):
        assert self.rule.rule_id == "MCP-019"
        assert self.rule.severity.value == "HIGH"
        assert self.rule.cwe == "CWE-94"

    def test_hot_reload_without_validation_detected(self):
        code = '''
def hot_reload():
    new_config = json.load(open("config.json"))
    server.config = new_config
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "MCP-019"

    def test_reload_with_validation_passes(self):
        code = '''
def hot_reload():
    new_config = json.load(open("config.json"))
    validate_config(new_config)
    server.config = new_config
'''
        assert len(_check(self.rule, code)) == 0

    def test_reload_with_schema_passes(self):
        code = '''
def reload_config():
    new_config = load_yaml("config.yml")
    schema.validate(new_config)
    apply(new_config)
'''
        assert len(_check(self.rule, code)) == 0

    def test_reload_with_pydantic_passes(self):
        code = '''
def watch_config(path):
    data = yaml.safe_load(open(path))
    config = pydantic.validate(data, ConfigModel)
    server.update(config)
'''
        assert len(_check(self.rule, code)) == 0

    def test_no_reload_ignored(self):
        code = '''
def load_initial_config():
    return json.load(open("config.json"))
'''
        assert len(_check(self.rule, code)) == 0

    def test_config_reload_without_validation(self):
        code = '''
def config_reload():
    cfg = yaml.safe_load(open("settings.yml"))
    server.settings = cfg
'''
        findings = _check(self.rule, code)
        assert len(findings) == 1


# ---------------------------------------------------------------------------
# Registry / Integration
# ---------------------------------------------------------------------------

class TestMCPRulesRegistry:
    def test_all_rules_returns_15(self):
        rules = get_all_mcp_rules()
        assert len(rules) == 15

    def test_all_rules_unique_ids(self):
        rules = get_all_mcp_rules()
        ids = [r.rule_id for r in rules]
        assert len(ids) == len(set(ids))

    def test_all_rules_have_cwe(self):
        for rule in get_all_mcp_rules():
            assert rule.cwe.startswith("CWE-"), f"{rule.rule_id} missing CWE"

    def test_all_rules_have_name(self):
        for rule in get_all_mcp_rules():
            assert rule.name, f"{rule.rule_id} missing name"

    def test_all_rules_have_description(self):
        for rule in get_all_mcp_rules():
            assert rule.description, f"{rule.rule_id} missing description"

    def test_rule_ids_sequential(self):
        rules = get_all_mcp_rules()
        expected_ids = [f"MCP-{i:03d}" for i in range(5, 20)]
        actual_ids = [r.rule_id for r in rules]
        assert actual_ids == expected_ids

    def test_rules_integrated_in_scanner(self):
        from mcpaudit.rules import get_all_rules
        all_rules = get_all_rules()
        rule_ids = {r.rule_id for r in all_rules}
        for i in range(5, 20):
            assert f"MCP-{i:03d}" in rule_ids, f"MCP-{i:03d} not in scanner"
