"""Tests for security checkers (SEC001-SEC004)."""

import pytest
from pathlib import Path

from stablestack.checkers.security import (
    HardcodedSecretsChecker,
    SqlInjectionChecker,
    EvalUsageChecker,
    InsecureRandomChecker,
)


class TestHardcodedSecretsChecker:
    """Tests for SEC001: hardcoded-secret."""

    @pytest.fixture
    def checker(self):
        return HardcodedSecretsChecker()

    def test_detects_api_key(self, checker, temp_py_file):
        """Should detect hardcoded API key."""
        code = '''
api_key = "sk_live_abc123def456"
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
        assert any(f.rule_id == "SEC001" for f in findings)

    def test_detects_password(self, checker, temp_py_file):
        """Should detect hardcoded password."""
        code = '''
password = "SuperSecret123!"
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1

    def test_allows_env_var(self, checker, temp_py_file):
        """Should not flag environment variable usage."""
        code = '''
import os
api_key = os.environ.get("API_KEY")
password = os.getenv("DB_PASSWORD")
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_allows_placeholder(self, checker, temp_py_file):
        """Should not flag obvious placeholders."""
        code = '''
api_key = "your-api-key-here"
password = "changeme"
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        # These might be flagged or not depending on implementation
        # The key is they shouldn't cause false positives in real code


class TestSqlInjectionChecker:
    """Tests for SEC002: sql-injection."""

    @pytest.fixture
    def checker(self):
        return SqlInjectionChecker()

    def test_detects_fstring_sql(self, checker, temp_py_file):
        """Should detect f-string SQL query."""
        code = '''
query = f"SELECT * FROM users WHERE name = '{user_input}'"
cursor.execute(query)
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
        assert any(f.rule_id == "SEC002" for f in findings)

    def test_detects_format_sql(self, checker, temp_py_file):
        """Should detect .format() SQL query."""
        code = '''
query = "SELECT * FROM users WHERE id = {}".format(user_id)
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1

    def test_allows_parameterized_query(self, checker, temp_py_file):
        """Should not flag parameterized queries."""
        code = '''
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0


class TestEvalUsageChecker:
    """Tests for SEC003: eval-usage."""

    @pytest.fixture
    def checker(self):
        return EvalUsageChecker()

    def test_detects_eval(self, checker, temp_py_file):
        """Should detect eval() usage."""
        code = '''
result = eval(user_input)
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "SEC003"

    def test_detects_exec(self, checker, temp_py_file):
        """Should detect exec() usage."""
        code = '''
exec(code_string)
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_allows_literal_eval(self, checker, temp_py_file):
        """Should not flag ast.literal_eval()."""
        code = '''
import ast
result = ast.literal_eval(user_input)
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0


class TestInsecureRandomChecker:
    """Tests for SEC004: insecure-random."""

    @pytest.fixture
    def checker(self):
        return InsecureRandomChecker()

    def test_detects_random_for_token(self, checker, temp_py_file):
        """Should detect random module for security-sensitive operations."""
        code = '''
import random
import string
token = ''.join(random.choices(string.ascii_letters, k=32))
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
        assert any(f.rule_id == "SEC004" for f in findings)

    def test_allows_secrets_module(self, checker, temp_py_file):
        """Should not flag secrets module."""
        code = '''
import secrets
token = secrets.token_hex(32)
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0


# ============================================================================
# TypeScript Tests
# ============================================================================

class TestHardcodedSecretsCheckerTS:
    """TypeScript tests for SEC001: hardcoded-secret."""

    @pytest.fixture
    def checker(self):
        return HardcodedSecretsChecker()

    def test_detects_api_key_ts(self, checker, temp_ts_file):
        """Should detect hardcoded API key in TypeScript."""
        code = '''
const apiKey = "sk_live_abc123def456";
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
        assert any(f.rule_id == "SEC001" for f in findings)

    def test_detects_password_ts(self, checker, temp_ts_file):
        """Should detect hardcoded password in TypeScript."""
        code = '''
const password = "SuperSecret123!";
const config = { password: "AnotherSecret456!" };
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1

    def test_allows_env_var_ts(self, checker, temp_ts_file):
        """Should not flag environment variable usage in TypeScript."""
        code = '''
const apiKey = process.env.API_KEY;
const password = process.env.DB_PASSWORD;
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0


class TestSqlInjectionCheckerTS:
    """TypeScript tests for SEC002: sql-injection."""

    @pytest.fixture
    def checker(self):
        return SqlInjectionChecker()

    def test_detects_template_literal_sql(self, checker, temp_ts_file):
        """Should detect template literal SQL query in TypeScript."""
        code = '''
const query = `SELECT * FROM users WHERE name = '${userInput}'`;
db.query(query);
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
        assert any(f.rule_id == "SEC002" for f in findings)

    def test_allows_parameterized_query_ts(self, checker, temp_ts_file):
        """Should not flag parameterized queries in TypeScript."""
        code = '''
const query = "SELECT * FROM users WHERE name = $1";
db.query(query, [userInput]);
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_toast_with_updated_word(self, checker, temp_ts_file):
        """Should NOT flag strings where 'Updated' partially matches 'UPDATE' keyword."""
        code = '''
toast.success(`Updated ${item.name} successfully`);
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_ui_template_literals(self, checker, temp_ts_file):
        """Should NOT flag UI messages in template literals that aren't SQL."""
        code = '''
const msg = `Created ${count} new entries`;
console.log(`Deleted ${file} from disk`);
alert(`Selected ${name} for processing`);
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_still_flags_real_template_literal_sql(self, checker, temp_ts_file):
        """Should still flag real SQL injection in template literals."""
        code = '''
const query = `SELECT * FROM users WHERE name = '${userInput}'`;
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1


class TestEvalUsageCheckerTS:
    """TypeScript tests for SEC003: eval-usage."""

    @pytest.fixture
    def checker(self):
        return EvalUsageChecker()

    def test_detects_eval_ts(self, checker, temp_ts_file):
        """Should detect eval() usage in TypeScript."""
        code = '''
const result = eval(userInput);
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
        assert any(f.rule_id == "SEC003" for f in findings)

    def test_detects_function_constructor_ts(self, checker, temp_ts_file):
        """Should detect new Function() in TypeScript."""
        code = '''
const fn = new Function("return " + userInput);
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1

    def test_detects_settimeout_string_ts(self, checker, temp_ts_file):
        """Should detect setTimeout with string in TypeScript."""
        code = '''
setTimeout("doSomething()", 1000);
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1

    def test_allows_settimeout_function_ts(self, checker, temp_ts_file):
        """Should not flag setTimeout with function in TypeScript."""
        code = '''
setTimeout(() => doSomething(), 1000);
setTimeout(doSomething, 1000);
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0


class TestInsecureRandomCheckerTS:
    """TypeScript tests for SEC004: insecure-random."""

    @pytest.fixture
    def checker(self):
        return InsecureRandomChecker()

    def test_detects_math_random_for_token(self, checker, temp_ts_file):
        """Should detect Math.random() for security-sensitive operations."""
        code = '''
function generateToken(): string {
    return Math.random().toString(36).substring(2);
}
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
        assert any(f.rule_id == "SEC004" for f in findings)

    def test_allows_crypto_random_ts(self, checker, temp_ts_file):
        """Should not flag crypto.randomBytes in TypeScript."""
        code = '''
import crypto from 'crypto';
const token = crypto.randomBytes(32).toString('hex');
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_math_random_for_request_id(self, checker, temp_ts_file):
        """Should NOT flag Math.random() used for request/idempotency IDs."""
        code = '''
function createCalendarEvent() {
    const requestId = Math.random().toString(36).substring(2);
    return calendar.events.insert({ requestId });
}
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_math_random_for_idempotency_key(self, checker, temp_ts_file):
        """Should NOT flag Math.random() used for idempotency keys."""
        code = '''
const idempotencyKey = Math.random().toString(36).substring(2);
await stripe.charges.create({ amount: 100 }, { idempotencyKey });
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_math_random_for_trace_id(self, checker, temp_ts_file):
        """Should NOT flag Math.random() used for trace IDs."""
        code = '''
const traceId = Math.random().toString(36).substring(2, 15);
logger.info("Processing request", { traceId });
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_still_flags_math_random_for_token(self, checker, temp_ts_file):
        """Should still flag Math.random() used to generate security tokens."""
        code = '''
function generateAuthToken(): string {
    return Math.random().toString(36).substring(2);
}
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
