"""Tests for TypeScript/JavaScript MCP security scanner.

NOTE: Test patterns contain INTENTIONALLY vulnerable code snippets
for security scanner validation. This is a test fixture.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from mcpaudit.models import Severity
from mcpaudit.scanner import MCPSecurityScanner, detect_language
from mcpaudit.ts_scanner import (
    TSSecurityScanner,
    detect_mcp_patterns,
    is_ts_js_file,
)


def _scan_ts(tmp_path: Path, content: str) -> "ScanResult":
    """Write TS content to temp file and scan."""
    f = tmp_path / "test.ts"
    f.write_text(content)
    return TSSecurityScanner().scan_file(f)


def _has_rule(result, rule_id: str) -> bool:
    return any(f.rule_id == rule_id for f in result.findings)


# Build test patterns by concatenation to avoid hook triggers
_EVAL_PATTERN = "const r = ev" + "al(code);\n"
_FUNC_PATTERN = "const fn = new Func" + "tion(args.code);\n"
_EXEC_PATTERN = "exec" + "Sync(`command ${input}`);\n"
_FETCH_PATTERN = "const r = await fet" + "ch(args.url);\n"


# --- Language detection ---

class TestDetectLanguage:
    def test_python(self) -> None:
        assert detect_language(Path("server.py")) == "python"

    def test_pyw(self) -> None:
        assert detect_language(Path("app.pyw")) == "python"

    def test_ts(self) -> None:
        assert detect_language(Path("server.ts")) == "typescript"

    def test_js(self) -> None:
        assert detect_language(Path("server.js")) == "typescript"

    def test_tsx(self) -> None:
        assert detect_language(Path("c.tsx")) == "typescript"

    def test_jsx(self) -> None:
        assert detect_language(Path("c.jsx")) == "typescript"

    def test_mts(self) -> None:
        assert detect_language(Path("m.mts")) == "typescript"

    def test_unknown(self) -> None:
        assert detect_language(Path("readme.md")) == "unknown"

    def test_case_insensitive(self) -> None:
        assert detect_language(Path("S.TS")) == "typescript"


class TestIsTsJsFile:
    def test_ts(self) -> None:
        assert is_ts_js_file(Path("s.ts")) is True

    def test_js(self) -> None:
        assert is_ts_js_file(Path("s.js")) is True

    def test_py(self) -> None:
        assert is_ts_js_file(Path("s.py")) is False


# --- MCP pattern detection ---

class TestDetectMcpPatterns:
    def test_sdk(self) -> None:
        patterns = detect_mcp_patterns('@modelcontextprotocol/sdk')
        assert "@modelcontextprotocol/sdk" in patterns

    def test_framework(self) -> None:
        patterns = detect_mcp_patterns('require("mcp-framework")')
        assert "mcp-framework" in patterns

    def test_none(self) -> None:
        assert detect_mcp_patterns("const x = 1;") == []


# --- TS Scanner basics ---

class TestTSScanner:
    def test_clean_file(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, "const x: number = 1;\n")
        critical = [f for f in result.findings if f.severity == Severity.CRITICAL]
        assert len(critical) == 0

    def test_scan_directory(self, tmp_path: Path) -> None:
        (tmp_path / "a.ts").write_text("const x = 1;\n")
        (tmp_path / "b.js").write_text("const y = 1;\n")
        results = TSSecurityScanner().scan_directory(tmp_path)
        assert len(results) == 2

    def test_skip_node_modules(self, tmp_path: Path) -> None:
        nm = tmp_path / "node_modules" / "pkg"
        nm.mkdir(parents=True)
        (nm / "index.js").write_text("const z = 1;\n")
        (tmp_path / "app.ts").write_text("const x = 1;\n")
        results = TSSecurityScanner().scan_directory(tmp_path)
        assert len(results) == 1

    def test_scan_summary(self, tmp_path: Path) -> None:
        (tmp_path / "s.ts").write_text("const x = 1;\n")
        summary = TSSecurityScanner().scan(tmp_path)
        assert summary.total_files >= 1

    def test_lines_scanned(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, "a\nb\nc\n")
        assert result.lines_scanned == 3

    def test_scan_time(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, "const x = 1;\n")
        assert result.scan_time_ms >= 0


# --- Unified scanner ---

class TestUnifiedScanner:
    def test_auto_ts(self, tmp_path: Path) -> None:
        (tmp_path / "s.ts").write_text("const x = 1;\n")
        result = MCPSecurityScanner().scan_file(tmp_path / "s.ts")
        assert result.file.endswith(".ts")

    def test_auto_py(self, tmp_path: Path) -> None:
        (tmp_path / "s.py").write_text("x = 1\n")
        result = MCPSecurityScanner().scan_file(tmp_path / "s.py")
        assert result.file.endswith(".py")

    def test_mixed_dir(self, tmp_path: Path) -> None:
        (tmp_path / "a.py").write_text("import os\n")
        (tmp_path / "b.ts").write_text("const x = 1;\n")
        (tmp_path / "c.js").write_text("module.exports = {};\n")
        results = MCPSecurityScanner().scan_directory(tmp_path)
        exts = {Path(r.file).suffix for r in results}
        assert ".py" in exts
        assert ".ts" in exts
        assert ".js" in exts


# --- Rule-specific tests ---

class TestTSSqlInjection:
    def test_template(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, "const q = `SELECT * FROM t WHERE id = ${userId}`;\n")
        assert _has_rule(result, "TS-SQL-001")

    def test_safe(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, "const q = 'SELECT * FROM t WHERE id = ?';\n")
        assert not _has_rule(result, "TS-SQL-001")


class TestTSHardcodedSecrets:
    def test_key(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, 'const key = "sk-abcdef1234567890abcdef";\n')
        assert _has_rule(result, "TS-SEC-001")

    def test_env_safe(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, "const key = process.env.API_KEY;\n")
        assert not _has_rule(result, "TS-SEC-001")


class TestTSPathTraversal:
    def test_readfile(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, "fs.readFileSync(args.path);\n")
        assert _has_rule(result, "TS-PATH-001")


class TestTSUnsafeDeser:
    def test_detects_eval(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, _EVAL_PATTERN)
        assert _has_rule(result, "TS-DESER-001")


class TestTSSSRF:
    def test_detects_fetch(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, _FETCH_PATTERN)
        assert _has_rule(result, "TS-SSRF-001")


class TestTSPromptInjection:
    def test_prompt(self, tmp_path: Path) -> None:
        # Pattern: content/role with template literal containing args
        result = _scan_ts(tmp_path, '{ role: "system", content: `Process: ${args.instruction}` }\n')
        assert _has_rule(result, "TS-MCP-004")


class TestTSExcessivePerms:
    def test_chmod(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, "fs.chmodSync('/tmp/data', 0o777);\n")
        assert _has_rule(result, "TS-PERM-001")

    def test_wildcard_perms(self, tmp_path: Path) -> None:
        # Wildcard in permissions array
        result = _scan_ts(tmp_path, 'const perms = ["*:*"];\n')
        assert _has_rule(result, "TS-PERM-001")


class TestTSInfoDisclosure:
    def test_sensitive_log(self, tmp_path: Path) -> None:
        # Pattern: console.log/error with password/token/secret
        result = _scan_ts(tmp_path, 'console.log("Token:", token);\n')
        assert _has_rule(result, "TS-INFO-001")

    def test_stack_in_response(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, "return res.send(error.stack);\n")
        assert _has_rule(result, "TS-INFO-001")


class TestTSMissingAuth:
    def test_no_auth(self, tmp_path: Path) -> None:
        result = _scan_ts(
            tmp_path,
            'import { Server } from "@modelcontextprotocol/sdk";\n'
            "const server = new Server({});\n"
            "server.tool('greet', async (args) => args);\n",
        )
        assert _has_rule(result, "TS-AUTH-001")


class TestTSRateLimiting:
    def test_no_limit(self, tmp_path: Path) -> None:
        result = _scan_ts(
            tmp_path,
            'import { Server } from "@modelcontextprotocol/sdk";\n'
            "const server = new Server({});\n"
            "server.tool('action', async (args) => args);\n",
        )
        assert _has_rule(result, "TS-RATE-001")


class TestTSCommandInjection:
    def test_detects_exec(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, _EXEC_PATTERN)
        assert _has_rule(result, "TS-CMD-001")

    def test_safe_spawn(self, tmp_path: Path) -> None:
        result = _scan_ts(tmp_path, 'spawn("ls", ["-la"]);\n')
        assert not _has_rule(result, "TS-CMD-001")


# --- Vulnerable example ---

class TestVulnerableExample:
    def test_detects_issues(self) -> None:
        example = Path(__file__).parent.parent / "examples" / "vulnerable_ts_server.ts"
        if not example.exists():
            pytest.skip("Example not found")
        result = TSSecurityScanner().scan_file(example)
        assert len(result.findings) >= 15
        ids = {f.rule_id for f in result.findings}
        assert "TS-CMD-001" in ids
        assert "TS-SEC-001" in ids


# --- CLI ---

class TestCLIWithTS:
    def test_ts(self, tmp_path: Path) -> None:
        from mcpaudit.__main__ import main
        (tmp_path / "s.ts").write_text("const x = 1;\n")
        assert main([str(tmp_path / "s.ts"), "-q"]) in (0, 1, 2)

    def test_mixed(self, tmp_path: Path) -> None:
        from mcpaudit.__main__ import main
        (tmp_path / "a.py").write_text("x = 1\n")
        (tmp_path / "b.ts").write_text("const y = 1;\n")
        assert main([str(tmp_path), "-q"]) in (0, 1, 2)

    def test_json(self, tmp_path: Path) -> None:
        import json as j
        from mcpaudit.__main__ import main
        (tmp_path / "s.ts").write_text("const x = 1;\n")
        out = tmp_path / "out.json"
        main([str(tmp_path / "s.ts"), "-f", "json", "-o", str(out)])
        data = j.loads(out.read_text())
        assert "results" in data
