"""
End-to-end tests for security bug detection through public API.

These tests verify that security_scan() properly detects security bugs
through the public API, without relying on internal methods or implementation details.

Tests cover:
- SQL injection (CWE-089)
- Command injection (CWE-078)
- Path injection (CWE-022)
- SSRF (CWE-918)
- Unsafe deserialization (CWE-502)

Each test:
1. Creates a temporary Python file with a security vulnerability
2. Calls security_scan() on that file
3. Verifies that a BUG verdict is returned with the correct bug type
4. Checks that the counterexample includes necessary details

This validates the public API end-to-end, ensuring users can detect
security bugs without needing to know internal implementation.
"""

import pytest
import tempfile
from pathlib import Path
from pyfromscratch.analyzer import security_scan, AnalysisResult


class TestSecurityScanAPI:
    """Test security_scan() public API."""
    
    def test_sql_injection_detected(self):
        """security_scan() detects SQL injection through public API."""
        # Create a Python file with SQL injection
        code = '''
def query_user(user_id):
    """Vulnerable SQL query function."""
    import sqlite3
    conn = sqlite3.connect("users.db")
    cursor = conn.cursor()
    # SQL injection: user_id comes from untrusted input
    query = "SELECT * FROM users WHERE id = " + user_id
    cursor.execute(query)
    return cursor.fetchall()
'''
        
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            # Call the public API
            result = security_scan(filepath, verbose=False)
            
            # Verify BUG verdict
            assert result.verdict == "BUG", f"Expected BUG, got {result.verdict}: {result.message}"
            
            # Verify bug type is SQL injection
            assert result.bug_type is not None
            assert "SQL" in result.bug_type.upper() or "INJECTION" in result.bug_type.upper(), \
                f"Expected SQL injection, got {result.bug_type}"
            
            # Verify counterexample has details
            assert result.counterexample is not None
            assert 'location' in result.counterexample or 'reason' in result.counterexample
            
        finally:
            filepath.unlink()
    
    def test_command_injection_detected(self):
        """security_scan() detects command injection through public API."""
        code = '''
def run_command(filename):
    """Vulnerable command execution function."""
    import subprocess
    # Command injection: filename comes from untrusted input
    cmd = "cat " + filename
    subprocess.call(cmd, shell=True)
'''
        
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            result = security_scan(filepath, verbose=False)
            
            assert result.verdict == "BUG", f"Expected BUG, got {result.verdict}: {result.message}"
            assert result.bug_type is not None
            assert "COMMAND" in result.bug_type.upper() or "INJECTION" in result.bug_type.upper(), \
                f"Expected command injection, got {result.bug_type}"
            
        finally:
            filepath.unlink()
    
    def test_path_injection_detected(self):
        """security_scan() detects path injection through public API."""
        code = '''
def read_file(filename):
    """Vulnerable file read function."""
    # Path injection: filename comes from untrusted input
    with open(filename, 'r') as f:
        return f.read()
'''
        
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            result = security_scan(filepath, verbose=False)
            
            # Path injection may be detected as PATH_INJECTION or FILE_WRITE sink
            assert result.verdict in ["BUG", "UNKNOWN"], \
                f"Expected BUG or UNKNOWN, got {result.verdict}: {result.message}"
            
            if result.verdict == "BUG":
                assert result.bug_type is not None
                assert "PATH" in result.bug_type.upper() or "FILE" in result.bug_type.upper(), \
                    f"Expected path/file bug, got {result.bug_type}"
        
        finally:
            filepath.unlink()
    
    def test_ssrf_detected(self):
        """security_scan() detects SSRF through public API."""
        code = '''
def fetch_url(url):
    """Vulnerable URL fetch function."""
    import requests
    # SSRF: url comes from untrusted input
    response = requests.get(url)
    return response.text
'''
        
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            result = security_scan(filepath, verbose=False)
            
            assert result.verdict in ["BUG", "UNKNOWN"], \
                f"Expected BUG or UNKNOWN, got {result.verdict}: {result.message}"
            
            if result.verdict == "BUG":
                assert result.bug_type is not None
                assert "SSRF" in result.bug_type.upper() or "URL" in result.bug_type.upper(), \
                    f"Expected SSRF, got {result.bug_type}"
        
        finally:
            filepath.unlink()
    
    def test_unsafe_deserialization_detected(self):
        """security_scan() detects unsafe deserialization through public API."""
        code = '''
def load_data(data):
    """Vulnerable deserialization function."""
    import pickle
    # Unsafe deserialization: data comes from untrusted input
    return pickle.loads(data)
'''
        
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            result = security_scan(filepath, verbose=False)
            
            assert result.verdict in ["BUG", "UNKNOWN"], \
                f"Expected BUG or UNKNOWN, got {result.verdict}: {result.message}"
            
            if result.verdict == "BUG":
                assert result.bug_type is not None
                assert "DESERIAL" in result.bug_type.upper() or "PICKLE" in result.bug_type.upper(), \
                    f"Expected deserialization bug, got {result.bug_type}"
        
        finally:
            filepath.unlink()
    
    def test_safe_code_no_false_positive(self):
        """security_scan() does not flag safe parameterized SQL."""
        code = '''
def query_user_safe(user_id):
    """Safe SQL query with parameterization."""
    import sqlite3
    conn = sqlite3.connect("users.db")
    cursor = conn.cursor()
    # Safe: parameterized query
    cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
    return cursor.fetchall()
'''
        
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            result = security_scan(filepath, verbose=False)
            
            # Should be SAFE or UNKNOWN (not BUG)
            assert result.verdict in ["SAFE", "UNKNOWN"], \
                f"Expected SAFE/UNKNOWN for safe code, got {result.verdict}: {result.bug_type}"
        
        finally:
            filepath.unlink()
    
    def test_function_specific_scan(self):
        """security_scan() can analyze specific functions."""
        code = '''
def vulnerable_function(user_id):
    """Vulnerable SQL query."""
    import sqlite3
    cursor = sqlite3.connect("db").cursor()
    cursor.execute("SELECT * FROM users WHERE id = " + user_id)

def safe_function(x):
    """Safe computation."""
    return x * 2
'''
        
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            # Scan only the vulnerable function
            result = security_scan(filepath, function_names=["vulnerable_function"], verbose=False)
            assert result.verdict == "BUG"
            
            # Scan only the safe function
            result_safe = security_scan(filepath, function_names=["safe_function"], verbose=False)
            assert result_safe.verdict in ["SAFE", "UNKNOWN"]
        
        finally:
            filepath.unlink()
    
    def test_multiple_bugs_in_file(self):
        """security_scan() detects bugs when multiple vulnerabilities exist."""
        code = '''
def sql_bug(user_id):
    import sqlite3
    cursor = sqlite3.connect("db").cursor()
    cursor.execute("SELECT * FROM users WHERE id = " + user_id)

def command_bug(filename):
    import subprocess
    subprocess.call("cat " + filename, shell=True)
'''
        
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            result = security_scan(filepath, verbose=False)
            
            # Should detect at least one bug
            assert result.verdict == "BUG", f"Expected BUG, got {result.verdict}"
            assert result.bug_type is not None
        
        finally:
            filepath.unlink()
    
    @pytest.mark.xfail(reason="Interprocedural taint tracking is Phase 2 (CODEQL_PARITY plan)")
    def test_interprocedural_taint(self):
        """security_scan() tracks taint across function calls."""
        code = '''
def get_user_input():
    """Returns untrusted user input."""
    # Use a placeholder string to simulate user input
    return "user_input"

def query_user():
    """Queries database with user input."""
    import sqlite3
    user_id = get_user_input()
    cursor = sqlite3.connect("db").cursor()
    # SQL injection: user_id comes from get_user_input() which returns tainted data
    cursor.execute("SELECT * FROM users WHERE id = " + user_id)
'''
        
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            result = security_scan(filepath, verbose=False)
            
            # Should detect interprocedural taint flow or other bugs
            # May be UNKNOWN if interprocedural analysis is incomplete
            # May detect TYPE_CONFUSION or other bugs related to the code
            assert result.verdict in ["BUG", "UNKNOWN"], \
                f"Expected BUG or UNKNOWN, got {result.verdict}"
            
            if result.verdict == "BUG":
                # Accept SQL injection, TYPE_CONFUSION, or other security bugs
                # The important part is that it detects *something* is wrong
                assert result.bug_type is not None
        
        finally:
            filepath.unlink()


class TestSecurityScanResultStructure:
    """Test the structure of AnalysisResult returned by security_scan()."""
    
    def test_result_has_required_fields(self):
        """AnalysisResult has all required fields."""
        code = '''
def dummy():
    pass
'''
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            result = security_scan(filepath, verbose=False)
            
            # Check required fields exist
            assert hasattr(result, 'verdict')
            assert hasattr(result, 'bug_type')
            assert hasattr(result, 'message')
            assert hasattr(result, 'counterexample')
            
            # Verdict must be one of the three values
            assert result.verdict in ["BUG", "SAFE", "UNKNOWN"]
        
        finally:
            filepath.unlink()
    
    def test_bug_result_has_counterexample(self):
        """BUG verdict includes counterexample details."""
        code = '''
def sql_bug(user_id):
    import sqlite3
    cursor = sqlite3.connect("db").cursor()
    cursor.execute("SELECT * FROM users WHERE id = " + user_id)
'''
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            filepath = Path(f.name)
        
        try:
            result = security_scan(filepath, verbose=False)
            
            if result.verdict == "BUG":
                # BUG verdict should include counterexample
                assert result.counterexample is not None, \
                    "BUG verdict must include counterexample"
                
                # Counterexample should be a dict with useful info
                assert isinstance(result.counterexample, dict), \
                    "Counterexample should be a dict"
        
        finally:
            filepath.unlink()


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
