"""Tests for scanner.patterns — static pattern detection against Python ASTs.

Each detection rule is exercised with focused inline snippets. Tests are
data-driven via parametrize to stay concise, following the conventions in
test_data_flow.py.
"""

from __future__ import annotations

import ast
from pathlib import Path
from textwrap import dedent

import pytest

from sanicode.scanner.patterns import check_patterns

FILE = Path("<test>")


def _parse(source: str) -> ast.Module:
    return ast.parse(dedent(source))


# ---------------------------------------------------------------------------
# Positive detection cases — one parametrized test covers all wired rules.
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    ("source", "rule_id", "cwe_id", "severity"),
    [
        ("eval('code')", "SC001", 78, "critical"),
        ("exec('code')", "SC002", 78, "high"),
        ("os.system('ls')", "SC003", 78, "high"),
        ("subprocess.call('ls', shell=True)", "SC004", 78, "high"),
        ("pickle.loads(data)", "SC005", 502, "high"),
        # SC006 — SQL string formatting (f-string)
        ('cursor.execute(f"SELECT * FROM {table}")', "SC006", 89, "high"),
        ("__import__('os')", "SC007", 94, "medium"),
        ("yaml.load(data)", "SC008", 502, "high"),
    ],
    ids=[
        "SC001-eval",
        "SC002-exec",
        "SC003-os.system",
        "SC004-subprocess-shell",
        "SC005-pickle",
        "SC006-sql-fstring",
        "SC007-import",
        "SC008-yaml-load",
    ],
)
def test_known_bad_pattern_detected(
    source: str, rule_id: str, cwe_id: int, severity: str
) -> None:
    tree = _parse(source)
    findings = check_patterns(tree, FILE)
    matches = [f for f in findings if f.rule_id == rule_id]
    assert matches, f"Expected {rule_id} finding for: {source}"
    f = matches[0]
    assert f.cwe_id == cwe_id, f"Expected CWE {cwe_id}, got {f.cwe_id}"
    assert f.severity == severity, f"Expected severity {severity!r}, got {f.severity!r}"
    assert f.file == FILE
    assert f.line > 0, f"Expected line > 0, got {f.line}"


# ---------------------------------------------------------------------------
# Negative / clean cases — safe variants must produce no findings.
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    ("source", "description"),
    [
        ("yaml.load(data, Loader=yaml.SafeLoader)", "yaml.load with Loader"),
        ("subprocess.call(['ls', '-la'])", "subprocess without shell=True"),
        ("print('hello')", "normal function call"),
        ("obj.method()", "normal method call"),
        # cursor.execute with parameterized query is safe
        ('cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))', "parameterized SQL"),
    ],
    ids=["yaml-safe", "subprocess-safe", "print", "method-call", "parameterized-sql"],
)
def test_clean_code_no_findings(source: str, description: str) -> None:
    tree = _parse(source)
    findings = check_patterns(tree, FILE)
    assert not findings, f"Expected no findings for {description}, got: {findings}"


# ---------------------------------------------------------------------------
# Field correctness — verify all Finding fields are populated correctly.
# ---------------------------------------------------------------------------


def test_finding_fields_complete() -> None:
    """All Finding fields must be set, and the line number must be accurate."""
    source = """\
        x = 1
        eval("code")
    """
    tree = _parse(source)
    findings = check_patterns(tree, FILE)
    assert len(findings) == 1, f"Expected exactly 1 finding, got: {findings}"
    f = findings[0]
    assert f.file == FILE
    assert f.line == 2, f"Expected eval on line 2, got line {f.line}"
    assert f.column >= 0, f"Expected non-negative column, got {f.column}"
    assert f.rule_id == "SC001"
    assert f.cwe_id == 78
    assert f.severity == "critical"
    assert "eval" in f.message, f"Expected 'eval' in message, got: {f.message!r}"


# ---------------------------------------------------------------------------
# Multiple findings in a single file — one call per rule.
# ---------------------------------------------------------------------------


def test_multiple_findings_in_one_file() -> None:
    """Three distinct rules triggered in one snippet produce three findings."""
    source = """\
        eval("x")
        exec("y")
        os.system("z")
    """
    tree = _parse(source)
    findings = check_patterns(tree, FILE)
    rule_ids = {f.rule_id for f in findings}
    assert rule_ids == {"SC001", "SC002", "SC003"}, (
        f"Expected {{SC001, SC002, SC003}}, got: {rule_ids}"
    )
