"""Tests for OWASP Agentic TypeScript security rules.

Tests TS-IDOR-001, TS-PRIV-001, and TS-AUTHZ-001 rules which cover
OWASP agentic threat categories: Insecure Direct Object Reference,
Privilege Escalation / Excessive Agency, and Missing Per-Tool Authorization.

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 for the security scanner.
"""

from __future__ import annotations

from mcpaudit.models import Finding, Severity
from mcpaudit.ts_rules import get_all_ts_rules, TSRule
from mcpaudit.ts_rules.idor import TSIDORRule
from mcpaudit.ts_rules.privilege_escalation import TSPrivilegeEscalationRule
from mcpaudit.ts_rules.missing_tool_authz import TSMissingToolAuthzRule


def _check(rule: TSRule, source: str) -> list[Finding]:
    """Run a single TS rule against source code."""
    return rule.check(source)


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

class TestTSIDOR:
    """Tests for TS-IDOR-001 rule."""

    def test_db_find_by_id_with_args_detected(self):
        source = 'const user = await db.findById(args.id);\n'
        findings = _check(TSIDORRule(), source)
        assert any(f.rule_id == "TS-IDOR-001" for f in findings)

    def test_collection_get_with_args_detected(self):
        source = 'const doc = collection.get(args.userId);\n'
        findings = _check(TSIDORRule(), source)
        assert any(f.rule_id == "TS-IDOR-001" for f in findings)

    def test_fs_readfile_with_args_path_detected(self):
        source = 'const data = fs.readFileSync(args.path);\n'
        findings = _check(TSIDORRule(), source)
        assert any(f.rule_id == "TS-IDOR-001" for f in findings)

    def test_model_findone_with_params_detected(self):
        source = 'const item = await model.findOne(params.itemId);\n'
        findings = _check(TSIDORRule(), source)
        assert any(f.rule_id == "TS-IDOR-001" for f in findings)

    def test_prisma_findunique_detected(self):
        source = 'const user = await prisma.findUnique(args.userId);\n'
        findings = _check(TSIDORRule(), source)
        assert any(f.rule_id == "TS-IDOR-001" for f in findings)

    def test_records_bracket_access_detected(self):
        source = 'const record = records[args.recordId];\n'
        findings = _check(TSIDORRule(), source)
        assert any(f.rule_id == "TS-IDOR-001" for f in findings)

    def test_fs_writefile_with_args_detected(self):
        source = 'fs.writeFileSync(args.filePath, content);\n'
        findings = _check(TSIDORRule(), source)
        assert any(f.rule_id == "TS-IDOR-001" for f in findings)

    def test_db_find_with_ownership_check_clean(self):
        source = (
            'const user = await db.findById(args.id);\n'
            'if (user.ownerId === session.userId) {\n'
            '  return user;\n'
            '}\n'
        )
        findings = _check(TSIDORRule(), source)
        assert len(findings) == 0

    def test_collection_with_authorize_clean(self):
        source = (
            'authorize(ctx, args.resourceId);\n'
            'const doc = collection.get(args.resourceId);\n'
        )
        findings = _check(TSIDORRule(), source)
        assert len(findings) == 0

    def test_static_db_query_clean(self):
        source = 'const users = await db.findById("admin-user-id");\n'
        findings = _check(TSIDORRule(), source)
        assert len(findings) == 0

    def test_comment_ignored(self):
        source = '// const user = await db.findById(args.id);\n'
        findings = _check(TSIDORRule(), source)
        assert len(findings) == 0

    def test_severity_is_high(self):
        source = 'const user = await db.findById(args.id);\n'
        findings = _check(TSIDORRule(), source)
        assert all(f.severity == Severity.HIGH for f in findings)

    def test_cwe_is_639(self):
        rule = TSIDORRule()
        assert rule.cwe == "CWE-639"


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

class TestTSPrivilegeEscalation:
    """Tests for TS-PRIV-001 rule."""

    def test_dynamic_require_detected(self):
        source = 'const mod = require(args.module);\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert any(f.rule_id == "TS-PRIV-001" for f in findings)

    def test_dynamic_import_detected(self):
        source = 'const mod = await import(args.moduleName);\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert any(f.rule_id == "TS-PRIV-001" for f in findings)

    def test_process_kill_detected(self):
        source = 'process.kill(pid);\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert any(f.rule_id == "TS-PRIV-001" for f in findings)

    def test_dirname_with_args_detected(self):
        source = 'const p = path.join(__dirname, args.subpath);\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert any(f.rule_id == "TS-PRIV-001" for f in findings)

    def test_dynamic_global_access_detected(self):
        source = 'const val = global[args.prop];\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert any(f.rule_id == "TS-PRIV-001" for f in findings)

    def test_dynamic_process_access_detected(self):
        source = 'const val = process[args.key];\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert any(f.rule_id == "TS-PRIV-001" for f in findings)

    def test_spawn_with_args_command_detected(self):
        # Detection target: spawn with user-controlled command
        source = 'spawn(args.command, args.cmdArgs);\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert any(f.rule_id == "TS-PRIV-001" for f in findings)

    def test_dynamic_require_severity_critical(self):
        source = 'const mod = require(args.module);\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        critical = [f for f in findings if f.severity == Severity.CRITICAL]
        assert len(critical) >= 1

    def test_allowlist_validation_clean(self):
        source = (
            'const allowedModules = ["fs", "path"];\n'
            'if (allowedModules.includes(args.module)) {\n'
            '  const mod = require(args.module);\n'
            '}\n'
        )
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert len(findings) == 0

    def test_static_require_clean(self):
        source = 'const fs = require("fs");\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert len(findings) == 0

    def test_comment_ignored(self):
        source = '// const mod = require(args.module);\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert len(findings) == 0

    def test_cwe_is_269(self):
        rule = TSPrivilegeEscalationRule()
        assert rule.cwe == "CWE-269"

    def test_dirname_concat_detected(self):
        source = 'const fullPath = __dirname + args.relPath;\n'
        findings = _check(TSPrivilegeEscalationRule(), source)
        assert any(f.rule_id == "TS-PRIV-001" for f in findings)


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

class TestTSMissingToolAuthz:
    """Tests for TS-AUTHZ-001 rule."""

    def test_tool_handler_without_authz_detected(self):
        source = (
            'server.tool("deleteUser", async (args) => {\n'
            '  await db.deleteUser(args.id);\n'
            '});\n'
        )
        findings = _check(TSMissingToolAuthzRule(), source)
        assert any(f.rule_id == "TS-AUTHZ-001" for f in findings)

    def test_multiple_tools_without_authz_all_flagged(self):
        source = (
            'server.tool("readDoc", async (args) => {\n'
            '  return await db.getDoc(args.id);\n'
            '});\n'
            'server.tool("writeDoc", async (args) => {\n'
            '  await db.updateDoc(args.id, args.data);\n'
            '});\n'
        )
        findings = _check(TSMissingToolAuthzRule(), source)
        authz_findings = [f for f in findings if f.rule_id == "TS-AUTHZ-001"]
        assert len(authz_findings) == 2

    def test_tool_with_checkpermission_clean(self):
        source = (
            'server.tool("deleteUser", async (args) => {\n'
            '  checkPermission(ctx, "admin");\n'
            '  await db.deleteUser(args.id);\n'
            '});\n'
        )
        findings = _check(TSMissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_tool_with_rbac_check_clean(self):
        source = (
            'server.tool("manage", async (args) => {\n'
            '  rbac.check(ctx.user, "manage:users");\n'
            '  await doManage(args);\n'
            '});\n'
        )
        findings = _check(TSMissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_tool_with_auth_token_clean(self):
        source = (
            'server.tool("getData", async (args) => {\n'
            '  const authToken = ctx.headers.authorization;\n'
            '  const data = await fetch(args.url);\n'
            '});\n'
        )
        findings = _check(TSMissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_no_server_tool_clean(self):
        source = 'function helper() { return 42; }\n'
        findings = _check(TSMissingToolAuthzRule(), source)
        assert len(findings) == 0

    def test_severity_is_medium(self):
        source = (
            'server.tool("test", async (args) => {\n'
            '  return args.value;\n'
            '});\n'
        )
        findings = _check(TSMissingToolAuthzRule(), source)
        assert all(f.severity == Severity.MEDIUM for f in findings)

    def test_cwe_is_862(self):
        rule = TSMissingToolAuthzRule()
        assert rule.cwe == "CWE-862"

    def test_tool_name_in_finding_description(self):
        source = (
            'server.tool("sensitiveAction", async (args) => {\n'
            '  await doSomething(args);\n'
            '});\n'
        )
        findings = _check(TSMissingToolAuthzRule(), source)
        assert any("sensitiveAction" in f.description for f in findings)

    def test_comment_tool_registration_ignored(self):
        source = '// server.tool("test", async (args) => {});\n'
        findings = _check(TSMissingToolAuthzRule(), source)
        assert len(findings) == 0


# ===========================================================================
# Architecture — new rules properly registered
# ===========================================================================

class TestOWASPRulesArchitecture:
    """Tests that new OWASP rules are properly registered."""

    def test_new_rules_in_get_all_ts_rules(self):
        rules = get_all_ts_rules()
        rule_ids = [r.rule_id for r in rules]
        assert "TS-IDOR-001" in rule_ids
        assert "TS-PRIV-001" in rule_ids
        assert "TS-AUTHZ-001" in rule_ids

    def test_total_rule_count_is_15(self):
        rules = get_all_ts_rules()
        assert len(rules) == 15

    def test_new_rules_have_cwe(self):
        rules = get_all_ts_rules()
        new_ids = {"TS-IDOR-001", "TS-PRIV-001", "TS-AUTHZ-001"}
        for rule in rules:
            if rule.rule_id in new_ids:
                assert rule.cwe, f"Rule {rule.rule_id} missing CWE"

    def test_new_rules_have_detailed_description(self):
        rules = get_all_ts_rules()
        new_ids = {"TS-IDOR-001", "TS-PRIV-001", "TS-AUTHZ-001"}
        for rule in rules:
            if rule.rule_id in new_ids:
                assert len(rule.detailed_description) > 50
