"""Tests for OWASP Agentic Top 10 rules: IDOR-001, PRIV-001, AUTHZ-001.

NOTE: This test file contains intentionally vulnerable code STRINGS
that are passed to the scanner for detection testing. These are not
executed — they are static analysis targets.
"""

from __future__ import annotations

import ast

from mcpaudit.models import Finding, Severity
from mcpaudit.rules.idor import IDORRule
from mcpaudit.rules.privilege_escalation import PrivilegeEscalationRule
from mcpaudit.rules.missing_tool_authz import MissingToolAuthzRule


def _check(rule, source: str) -> list[Finding]:
    tree = ast.parse(source)
    return rule.check(tree, source)


# ===========================================================================
# IDOR-001: Insecure Direct Object Reference
# ===========================================================================

class TestIDOR:

    def test_open_with_user_param(self):
        source = 'def get_file(path):\n    return open(path).read()\n'
        findings = _check(IDORRule(), source)
        assert len(findings) == 1
        assert findings[0].rule_id == "IDOR-001"
        assert findings[0].severity == Severity.HIGH
        assert findings[0].cwe == "CWE-639"

    def test_open_with_subscript_param(self):
        source = 'def get_file(args):\n    return open(args["path"]).read()\n'
        findings = _check(IDORRule(), source)
        assert len(findings) == 1
        assert findings[0].rule_id == "IDOR-001"

    def test_open_hardcoded_path_clean(self):
        source = 'def read_config():\n    return open("/etc/config.json").read()\n'
        findings = _check(IDORRule(), source)
        assert len(findings) == 0

    def test_db_query_with_user_id(self):
        source = (
            'def get_user(user_id):\n'
            '    return db.find_one(user_id)\n'
        )
        findings = _check(IDORRule(), source)
        assert len(findings) == 1
        assert "database query" in findings[0].title

    def test_db_execute_with_user_param(self):
        source = (
            'def get_record(record_id):\n'
            '    return cursor.execute(record_id)\n'
        )
        findings = _check(IDORRule(), source)
        assert len(findings) == 1

    def test_db_query_hardcoded_clean(self):
        source = (
            'def get_default():\n'
            '    return db.find_one("default_user")\n'
        )
        findings = _check(IDORRule(), source)
        assert len(findings) == 0

    def test_function_with_auth_check_clean(self):
        source = (
            'def get_file(path, user):\n'
            '    check_permission(user, path)\n'
            '    return open(path).read()\n'
        )
        findings = _check(IDORRule(), source)
        assert len(findings) == 0

    def test_function_with_acl_clean(self):
        source = (
            'def get_record(record_id, user):\n'
            '    acl.verify(user, record_id)\n'
            '    return db.find_one(record_id)\n'
        )
        findings = _check(IDORRule(), source)
        assert len(findings) == 0

    def test_async_function_idor(self):
        source = (
            'async def fetch_document(doc_id):\n'
            '    return db.get(doc_id)\n'
        )
        findings = _check(IDORRule(), source)
        assert len(findings) == 1

    def test_no_params_clean(self):
        source = 'def helper():\n    return open("static.txt").read()\n'
        findings = _check(IDORRule(), source)
        assert len(findings) == 0

    def test_self_param_excluded(self):
        source = (
            'class Handler:\n'
            '    def get(self):\n'
            '        return open("data.txt").read()\n'
        )
        findings = _check(IDORRule(), source)
        assert len(findings) == 0

    def test_multiple_findings(self):
        source = (
            'def process(path, record_id):\n'
            '    data = open(path).read()\n'
            '    record = db.find_one(record_id)\n'
            '    return data, record\n'
        )
        findings = _check(IDORRule(), source)
        assert len(findings) == 2

    def test_rule_metadata(self):
        rule = IDORRule()
        assert rule.rule_id == "IDOR-001"
        assert rule.name == "Insecure Direct Object Reference"
        assert rule.severity == Severity.HIGH
        assert rule.cwe == "CWE-639"
        assert len(rule.detailed_description) > len(rule.description)


# ===========================================================================
# PRIV-001: Privilege Escalation / Excessive Agency
# ===========================================================================

class TestPrivilegeEscalation:

    def test_os_system_in_tool(self):
        source = (
            'import os\n'
            'def handle_tool_run(cmd):\n'
            '    os.system(cmd)\n'
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 1
        assert findings[0].rule_id == "PRIV-001"
        assert findings[0].severity == Severity.HIGH
        assert findings[0].cwe == "CWE-269"

    def test_os_popen_in_tool(self):
        source = (
            'import os\n'
            'def my_tool(cmd):\n'
            '    return os.popen(cmd).read()\n'
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 1
        assert "os.popen" in findings[0].title

    def test_subprocess_shell_true_in_tool(self):
        source = (
            'import subprocess\n'
            'def tool_runner(cmd):\n'
            '    subprocess.run(cmd, shell=True)\n'
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 1
        assert "shell=True" in findings[0].title

    def test_subprocess_no_shell_clean(self):
        source = (
            'import subprocess\n'
            'def tool_runner(args):\n'
            '    subprocess.run(["ls", "-la"])\n'
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 0

    def test_eval_in_tool(self):
        # NOTE: The string below contains "eval()" as a detection target,
        # not as executed code. This is a security scanner test.
        source = (
            'def handle_tool_compute(expr):\n'
            '    return eval(expr)\n'  # noqa: S307 - detection target
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 1
        assert findings[0].severity == Severity.CRITICAL
        assert findings[0].cwe == "CWE-95"

    def test_dunder_import_in_tool(self):
        source = (
            'def tool_plugin_loader(module_name):\n'
            '    mod = __import__(module_name)\n'
            '    return mod\n'
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 1
        assert "__import__" in findings[0].title

    def test_shutil_rmtree_in_tool(self):
        source = (
            'import shutil\n'
            'def handle_tool_cleanup(path):\n'
            '    shutil.rmtree(path)\n'
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 1
        assert "shutil.rmtree" in findings[0].title

    def test_os_remove_in_tool(self):
        source = (
            'import os\n'
            'def tool_delete(path):\n'
            '    os.remove(path)\n'
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 1

    def test_non_tool_function_clean(self):
        source = (
            'import os\n'
            'def helper(cmd):\n'
            '    os.system(cmd)\n'
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 0

    def test_decorated_tool_detected(self):
        source = (
            'import os\n'
            '@mcp.tool()\n'
            'async def run_command(cmd):\n'
            '    os.system(cmd)\n'
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 1

    def test_multiple_violations(self):
        # NOTE: eval() below is a detection target string, not executed code.
        source = (
            'import os\n'
            'def handle_tool_dangerous(cmd, path):\n'
            '    os.system(cmd)\n'
            '    eval(path)\n'  # noqa: S307 - detection target
            '    os.remove(path)\n'
        )
        findings = _check(PrivilegeEscalationRule(), source)
        assert len(findings) == 3

    def test_rule_metadata(self):
        rule = PrivilegeEscalationRule()
        assert rule.rule_id == "PRIV-001"
        assert rule.name == "Privilege Escalation / Excessive Agency"
        assert rule.severity == Severity.HIGH
        assert rule.cwe == "CWE-269"
        assert len(rule.detailed_description) > len(rule.description)


# ===========================================================================
# AUTHZ-001: Missing Per-Tool Authorization
# ===========================================================================

class TestMissingToolAuthz:

    def test_tool_handler_no_auth(self):
        source = (
            'def handle_tool_read(path):\n'
            '    return open(path).read()\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 1
        assert findings[0].rule_id == "AUTHZ-001"
        assert findings[0].severity == Severity.MEDIUM
        assert findings[0].cwe == "CWE-862"

    def test_tool_with_auth_check_clean(self):
        source = (
            'def handle_tool_read(path, user):\n'
            '    check_permission(user, path)\n'
            '    return open(path).read()\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_tool_with_token_param_clean(self):
        source = (
            'def handle_tool_read(path, token):\n'
            '    return open(path).read()\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_tool_with_auth_decorator_clean(self):
        source = (
            '@require_auth\n'
            'def handle_tool_write(path, data):\n'
            '    open(path, "w").write(data)\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_decorated_tool_no_auth(self):
        source = (
            '@server.tool\n'
            'async def search(query):\n'
            '    return db.search(query)\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 1

    def test_non_tool_function_clean(self):
        source = (
            'def helper(data):\n'
            '    return process(data)\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_tool_with_verify_token_clean(self):
        source = (
            'def handle_tool_delete(item_id):\n'
            '    verify_token(request.headers)\n'
            '    return db.delete(item_id)\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_async_tool_no_auth(self):
        source = (
            'async def handle_tool_update(data):\n'
            '    return db.update(data)\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 1

    def test_tool_with_api_key_param_clean(self):
        source = (
            'def my_tool(data, api_key):\n'
            '    return process(data)\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_tool_with_login_required_decorator_clean(self):
        source = (
            '@login_required\n'
            'def tool_action(data):\n'
            '    return run(data)\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_multiple_tools_mixed(self):
        source = (
            'def handle_tool_a(data):\n'
            '    check_auth(data)\n'
            '    return process(data)\n'
            '\n'
            'def handle_tool_b(data):\n'
            '    return process(data)\n'
        )
        findings = _check(MissingToolAuthzRule(), source)
        assert len(findings) == 1
        assert "handle_tool_b" in findings[0].description

    def test_rule_metadata(self):
        rule = MissingToolAuthzRule()
        assert rule.rule_id == "AUTHZ-001"
        assert rule.name == "Missing Per-Tool Authorization"
        assert rule.severity == Severity.MEDIUM
        assert rule.cwe == "CWE-862"
        assert len(rule.detailed_description) > len(rule.description)


# ===========================================================================
# Integration: verify rules are registered
# ===========================================================================

class TestRulesRegistered:

    def test_idor_in_all_rules(self):
        from mcpaudit.rules import get_all_rules
        rules = get_all_rules()
        rule_ids = [r.rule_id for r in rules]
        assert "IDOR-001" in rule_ids

    def test_priv_in_all_rules(self):
        from mcpaudit.rules import get_all_rules
        rules = get_all_rules()
        rule_ids = [r.rule_id for r in rules]
        assert "PRIV-001" in rule_ids

    def test_authz_in_all_rules(self):
        from mcpaudit.rules import get_all_rules
        rules = get_all_rules()
        rule_ids = [r.rule_id for r in rules]
        assert "AUTHZ-001" in rule_ids
