"""Tests for Permission Analyzer."""

from __future__ import annotations

import json

import pytest

from mcpaudit.permission_analyzer import (
    ALL_CATEGORIES,
    PermissionCategory,
    PermissionIssue,
    PermissionMatrix,
    PermissionPattern,
    PermissionReport,
    PermissionUsage,
    ToolPermission,
    _categorize_severity,
    _compute_risk_score,
    _detect_permissions,
    _extract_tool_functions,
    _find_function_end,
    _generate_recommendations,
    analyze_permissions,
    format_permission_json,
    format_permission_text,
    get_permission_categories,
    get_permission_patterns,
)


# --- Data Models ---

class TestPermissionCategory:
    def test_filesystem_read(self):
        assert PermissionCategory.FILESYSTEM_READ == "filesystem:read"

    def test_filesystem_write(self):
        assert PermissionCategory.FILESYSTEM_WRITE == "filesystem:write"

    def test_network(self):
        assert PermissionCategory.NETWORK == "network"

    def test_subprocess_cat(self):
        assert PermissionCategory.SUBPROCESS == "subprocess"

    def test_env_access(self):
        assert PermissionCategory.ENV_ACCESS == "env:access"

    def test_database(self):
        assert PermissionCategory.DATABASE == "database"

    def test_crypto(self):
        assert PermissionCategory.CRYPTO == "crypto"

    def test_system_info(self):
        assert PermissionCategory.SYSTEM_INFO == "system:info"

    def test_ipc(self):
        assert PermissionCategory.IPC == "ipc"

    def test_dangerous_eval(self):
        assert PermissionCategory.DANGEROUS_EVAL == "dangerous:eval"


class TestPermissionPattern:
    def test_to_dict(self):
        p = PermissionPattern("network", r"\brequests\.get\b", "HTTP GET", "HIGH")
        d = p.to_dict()
        assert d["category"] == "network"
        assert d["description"] == "HTTP GET"
        assert d["confidence"] == "HIGH"

    def test_default_confidence(self):
        p = PermissionPattern("network", r"\bfoo\b", "Test")
        assert p.confidence == "HIGH"


class TestPermissionUsage:
    def test_to_dict(self):
        u = PermissionUsage("network", 10, "requests.get(url)", "HTTP call", "HIGH")
        d = u.to_dict()
        assert d["category"] == "network"
        assert d["line"] == 10
        assert d["code_snippet"] == "requests.get(url)"

    def test_fields(self):
        u = PermissionUsage("subprocess", 5, "sub.run(cmd)", "Exec", "MEDIUM")
        assert u.category == "subprocess"
        assert u.confidence == "MEDIUM"


class TestPermissionIssue:
    def test_to_dict(self):
        i = PermissionIssue("over-privileged", "network", "Not used", "LOW", "Remove it")
        d = i.to_dict()
        assert d["issue_type"] == "over-privileged"
        assert d["permission"] == "network"
        assert d["recommendation"] == "Remove it"

    def test_under_privileged(self):
        i = PermissionIssue("under-privileged", "subprocess", "Used but not declared")
        assert i.severity == "MEDIUM"


class TestToolPermission:
    def test_to_dict(self):
        t = ToolPermission("read_file", ["filesystem:read"])
        d = t.to_dict()
        assert d["tool_name"] == "read_file"
        assert "filesystem:read" in d["required_permissions"]

    def test_empty_permissions(self):
        t = ToolPermission("noop")
        assert t.required_permissions == []


class TestPermissionMatrix:
    def test_empty(self):
        m = PermissionMatrix()
        assert m.all_permissions == []
        d = m.to_dict()
        assert d["tools"] == []

    def test_all_permissions_deduped(self):
        m = PermissionMatrix(tools=[
            ToolPermission("a", ["network", "subprocess"]),
            ToolPermission("b", ["network", "database"]),
        ])
        perms = m.all_permissions
        assert "network" in perms
        assert "subprocess" in perms
        assert "database" in perms
        assert len(perms) == 3


class TestPermissionReport:
    def test_empty(self):
        r = PermissionReport()
        assert r.total_issues == 0
        assert r.is_least_privilege is True
        assert r.risk_score == 0

    def test_total_issues(self):
        r = PermissionReport(
            over_privileged=[PermissionIssue("over-privileged", "network", "Unused")],
            under_privileged=[PermissionIssue("under-privileged", "subprocess", "Used")],
        )
        assert r.total_issues == 2
        assert r.is_least_privilege is False

    def test_to_dict(self):
        r = PermissionReport(declared_permissions=["network"], used_permissions=["network"])
        d = r.to_dict()
        assert d["declared_permissions"] == ["network"]
        assert d["is_least_privilege"] is True
        assert "errors" in d

    def test_least_privilege_true(self):
        r = PermissionReport(
            declared_permissions=["network"],
            used_permissions=["network"],
        )
        assert r.is_least_privilege is True


# --- Detection Engine ---

class TestDetectPermissions:
    def test_subprocess_detection(self):
        code = "subprocess.run(['ls', '-la'])"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "subprocess" in categories

    def test_filesystem_read(self):
        code = 'f = open("file.txt", "r")'
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "filesystem:read" in categories

    def test_filesystem_write(self):
        code = 'f = open("out.txt", "w")'
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "filesystem:write" in categories

    def test_network_requests(self):
        code = "resp = requests.get('http://example.com')"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "network" in categories

    def test_env_access(self):
        code = "val = os.environ['HOME']"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "env:access" in categories

    def test_database(self):
        code = "conn = sqlite3.connect('db.sqlite')"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "database" in categories

    def test_eval_detection(self):
        code = "result = eval(user_input)"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "dangerous:eval" in categories

    def test_dynamic_exec_detection(self):
        code = "exec(code_string)"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "dangerous:eval" in categories

    def test_crypto(self):
        code = "h = hashlib.sha256(data)"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "crypto" in categories

    def test_system_info(self):
        code = "platform.system()"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "system:info" in categories

    def test_ipc_threading(self):
        code = "t = threading.Thread(target=f)"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "ipc" in categories

    def test_ipc_multiprocessing(self):
        code = "p = multiprocessing.Process(target=f)"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "ipc" in categories

    def test_comments_skipped(self):
        code = "# subprocess.run(['ls'])"
        usages = _detect_permissions(code)
        assert len(usages) == 0

    def test_empty_lines_skipped(self):
        code = "\n\n\n"
        usages = _detect_permissions(code)
        assert len(usages) == 0

    def test_multiple_permissions(self):
        code = "subprocess.run(['ls'])\nrequests.get('http://x')\nf = open('a', 'r')"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "subprocess" in categories
        assert "network" in categories
        assert "filesystem:read" in categories

    def test_os_system_detection(self):
        code = "os.system('ls')"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "subprocess" in categories

    def test_socket_detection(self):
        code = "s = socket.socket(socket.AF_INET)"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "network" in categories

    def test_path_write_text(self):
        code = "Path('/tmp/out').write_text('data')"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "filesystem:write" in categories

    def test_os_listdir_detection(self):
        code = "files = os.listdir('/tmp')"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "filesystem:read" in categories

    def test_getenv_detection(self):
        code = "val = os.getenv('SECRET')"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "env:access" in categories

    def test_ssl_detection(self):
        code = "ctx = ssl.create_default_context()"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "crypto" in categories

    def test_os_getpid_detection(self):
        code = "pid = os.getpid()"
        usages = _detect_permissions(code)
        categories = {u.category for u in usages}
        assert "system:info" in categories


class TestExtractToolFunctions:
    def test_decorated_tool(self):
        code = '@server.tool()\ndef read_file(path: str):\n    return "data"\n'
        tools = _extract_tool_functions(code)
        assert len(tools) >= 1
        assert tools[0][0] == "read_file"

    def test_named_tool_decorator(self):
        code = '@mcp.tool("list_files")\ndef list_files_handler(d: str):\n    return []\n'
        tools = _extract_tool_functions(code)
        assert len(tools) >= 1
        assert tools[0][0] == "list_files"

    def test_no_tools_fallback(self):
        code = "def handle_request(data):\n    return process(data)\n"
        tools = _extract_tool_functions(code)
        assert len(tools) >= 1
        assert tools[0][0] == "handle_request"

    def test_empty_code(self):
        tools = _extract_tool_functions("")
        assert tools == []


# --- Severity & Risk ---

class TestCategorizeSeverity:
    def test_under_privileged_subprocess(self):
        assert _categorize_severity("under-privileged", "subprocess") == "CRITICAL"

    def test_under_privileged_dangerous_eval(self):
        assert _categorize_severity("under-privileged", "dangerous:eval") == "CRITICAL"

    def test_under_privileged_network(self):
        assert _categorize_severity("under-privileged", "network") == "HIGH"

    def test_under_privileged_crypto(self):
        assert _categorize_severity("under-privileged", "crypto") == "MEDIUM"

    def test_over_privileged_subprocess(self):
        assert _categorize_severity("over-privileged", "subprocess") == "HIGH"

    def test_over_privileged_network(self):
        assert _categorize_severity("over-privileged", "network") == "MEDIUM"

    def test_over_privileged_crypto(self):
        assert _categorize_severity("over-privileged", "crypto") == "LOW"


class TestComputeRiskScore:
    def test_zero(self):
        assert _compute_risk_score(0, 0, 0) == 0

    def test_under_privileged_adds(self):
        assert _compute_risk_score(0, 2, 0) == 30

    def test_over_privileged_adds(self):
        assert _compute_risk_score(3, 0, 0) == 15

    def test_dangerous_adds(self):
        assert _compute_risk_score(0, 0, 2) == 20

    def test_capped_at_100(self):
        assert _compute_risk_score(10, 10, 10) == 100

    def test_mixed(self):
        score = _compute_risk_score(1, 1, 1)
        assert score == 30  # 5 + 15 + 10


class TestGenerateRecommendations:
    def test_no_issues(self):
        recs = _generate_recommendations([], [], ["network"])
        assert any("least privilege" in r.lower() for r in recs)

    def test_over_privileged_recs(self):
        over = [PermissionIssue("over-privileged", "network", "unused")]
        recs = _generate_recommendations(over, [], [])
        assert any("Remove" in r for r in recs)

    def test_under_privileged_recs(self):
        under = [PermissionIssue("under-privileged", "subprocess", "used")]
        recs = _generate_recommendations([], under, ["subprocess"])
        assert any("Declare" in r for r in recs)

    def test_dangerous_warning_recs(self):
        recs = _generate_recommendations([], [], ["subprocess"])
        assert any("dangerous" in r.lower() or "Review" in r for r in recs)


# --- Main Analysis ---

class TestAnalyzePermissions:
    def test_empty_code(self):
        report = analyze_permissions("")
        assert len(report.errors) > 0

    def test_whitespace_only(self):
        report = analyze_permissions("   \n\n  ")
        assert len(report.errors) > 0

    def test_no_declared_perms(self):
        code = "subprocess.run(['ls'])"
        report = analyze_permissions(code)
        assert "subprocess" in report.used_permissions
        assert len(report.under_privileged) > 0

    def test_perfect_match_is_least_privilege(self):
        code = "subprocess.run(['ls'])"
        report = analyze_permissions(code, declared_perms=["subprocess"])
        assert report.is_least_privilege is True
        assert report.total_issues == 0

    def test_over_privileged_detected(self):
        code = "x = 1 + 1"
        report = analyze_permissions(code, declared_perms=["network", "subprocess"])
        assert len(report.over_privileged) == 2

    def test_under_privileged_detected(self):
        code = "subprocess.run(['ls'])\nrequests.get('http://x')"
        report = analyze_permissions(code, declared_perms=[])
        assert len(report.under_privileged) >= 2

    def test_mixed_issues(self):
        code = "subprocess.run(['ls'])"
        report = analyze_permissions(code, declared_perms=["network"])
        assert len(report.over_privileged) >= 1
        assert len(report.under_privileged) >= 1

    def test_risk_score_increases_with_issues(self):
        code = "subprocess.run(['ls'])"
        r1 = analyze_permissions(code, declared_perms=["subprocess"])
        r2 = analyze_permissions(code, declared_perms=[])
        assert r2.risk_score > r1.risk_score

    def test_usages_populated(self):
        code = "subprocess.run(['ls'])\nos.listdir('/tmp')"
        report = analyze_permissions(code)
        assert len(report.usages) >= 2

    def test_recommendations_generated(self):
        code = "subprocess.run(['ls'])"
        report = analyze_permissions(code, declared_perms=[])
        assert len(report.recommendations) >= 1

    def test_to_dict_structure(self):
        code = "subprocess.run(['ls'])"
        report = analyze_permissions(code, declared_perms=["subprocess"])
        d = report.to_dict()
        assert "declared_permissions" in d
        assert "used_permissions" in d
        assert "matrix" in d

    def test_none_declared_defaults_to_empty(self):
        code = "x = 1"
        report = analyze_permissions(code)
        assert report.declared_permissions == []


# --- Tool Matrix ---

class TestToolMatrix:
    def test_matrix_built_for_decorated_tools(self):
        code = '@server.tool()\ndef read_file(path):\n    return open(path, "r").read()\n'
        report = analyze_permissions(code)
        assert len(report.matrix.tools) >= 1

    def test_matrix_permissions_assigned(self):
        code = '@server.tool("my_exec")\ndef do_exec(cmd):\n    subprocess.run(cmd, shell=True)\n'
        report = analyze_permissions(code)
        tools = report.matrix.tools
        if tools:
            assert "subprocess" in tools[0].required_permissions


# --- Formatters ---

class TestFormatPermissionText:
    def test_basic_output(self):
        report = PermissionReport(risk_score=42)
        text = format_permission_text(report)
        assert "Permission Analysis Report" in text
        assert "42/100" in text

    def test_with_issues(self):
        report = PermissionReport(
            over_privileged=[PermissionIssue("over-privileged", "network", "Unused", "LOW")],
            under_privileged=[PermissionIssue("under-privileged", "subprocess", "Used", "CRITICAL")],
        )
        text = format_permission_text(report)
        assert "OVER-PRIVILEGED" in text
        assert "UNDER-PRIVILEGED" in text

    def test_with_matrix(self):
        report = PermissionReport(
            matrix=PermissionMatrix(tools=[
                ToolPermission("read_file", ["filesystem:read"]),
            ])
        )
        text = format_permission_text(report)
        assert "PERMISSION MATRIX" in text
        assert "read_file" in text

    def test_with_recommendations(self):
        report = PermissionReport(recommendations=["Remove unused perms"])
        text = format_permission_text(report)
        assert "RECOMMENDATIONS" in text

    def test_with_errors(self):
        report = PermissionReport(errors=["Something failed"])
        text = format_permission_text(report)
        assert "Error:" in text

    def test_least_privilege_yes(self):
        report = PermissionReport()
        text = format_permission_text(report)
        assert "YES" in text


class TestFormatPermissionJson:
    def test_valid_json(self):
        report = PermissionReport(
            declared_permissions=["network"],
            used_permissions=["network"],
            risk_score=0,
        )
        text = format_permission_json(report)
        data = json.loads(text)
        assert data["declared_permissions"] == ["network"]
        assert data["is_least_privilege"] is True

    def test_round_trip(self):
        code = "subprocess.run(['ls'])"
        report = analyze_permissions(code, declared_perms=["subprocess"])
        text = format_permission_json(report)
        data = json.loads(text)
        assert isinstance(data["usages"], list)
        assert isinstance(data["matrix"], dict)


# --- Public API ---

class TestPublicAPI:
    def test_get_permission_patterns(self):
        patterns = get_permission_patterns()
        assert len(patterns) > 30
        categories = {p.category for p in patterns}
        assert "subprocess" in categories
        assert "network" in categories

    def test_get_permission_categories(self):
        cats = get_permission_categories()
        assert "filesystem:read" in cats
        assert "subprocess" in cats
        assert len(cats) == 10

    def test_all_categories_list(self):
        assert len(ALL_CATEGORIES) == 10
        assert PermissionCategory.FILESYSTEM_READ in ALL_CATEGORIES
