"""Security tests for Quizzical Beats."""
import pytest
import os
import re
import importlib
import musicround.config


class TestSecurityConfiguration:
    """Test security configuration and settings."""
    
    def test_secret_key_required(self):
        """Test that SECRET_KEY must be set."""
        # Remove SECRET_KEY from environment
        secret_key = os.environ.get('SECRET_KEY')
        if secret_key:
            del os.environ['SECRET_KEY']
        
        try:
            # Force module reload to re-evaluate class-level checks
            with pytest.raises(ValueError, match="SECRET_KEY environment variable must be set"):
                importlib.reload(musicround.config)
        finally:
            # Restore environment
            if secret_key:
                os.environ['SECRET_KEY'] = secret_key
                # Reload with correct env so other tests work
                importlib.reload(musicround.config)
    
    def test_automation_token_required(self):
        """Test that AUTOMATION_TOKEN must be set."""
        # Remove AUTOMATION_TOKEN from environment
        token = os.environ.get('AUTOMATION_TOKEN')
        if token:
            del os.environ['AUTOMATION_TOKEN']
        
        try:
            # Force module reload to re-evaluate class-level checks
            with pytest.raises(ValueError, match="AUTOMATION_TOKEN environment variable must be set"):
                importlib.reload(musicround.config)
        finally:
            # Restore environment
            if token:
                os.environ['AUTOMATION_TOKEN'] = token
                # Reload with correct env so other tests work
                importlib.reload(musicround.config)
    
    def test_no_credentials_in_code(self):
        """Test that no credentials are hardcoded in Python files."""
        project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
        musicround_dir = os.path.join(project_root, 'musicround')
        
        # Patterns that indicate potential credentials
        dangerous_patterns = [
            r'password\s*=\s*["\'][^"\']{8,}["\']',  # password = "something"
            r'token\s*=\s*["\'][A-Za-z0-9]{20,}["\']',  # token = "longstring"
            r'api[_-]?key\s*=\s*["\'][A-Za-z0-9]{20,}["\']',  # api_key = "key"
            r'secret\s*=\s*["\'][A-Za-z0-9]{20,}["\']',  # secret = "value"
        ]
        
        issues = []
        
        for root, dirs, files in os.walk(musicround_dir):
            # Skip __pycache__ and test files
            dirs[:] = [d for d in dirs if d != '__pycache__']
            
            for file in files:
                if not file.endswith('.py'):
                    continue
                
                filepath = os.path.join(root, file)
                with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
                    content = f.read()
                    
                    for pattern in dangerous_patterns:
                        matches = re.finditer(pattern, content, re.IGNORECASE)
                        for match in matches:
                            # Exclude common test/example patterns
                            matched_text = match.group(0)
                            if any(safe in matched_text.lower() for safe in [
                                'test', 'example', 'placeholder', 'changeme', 
                                'your-', 'secret-key-here', 'getenvi'
                            ]):
                                continue
                            
                            issues.append({
                                'file': filepath,
                                'line': content[:match.start()].count('\n') + 1,
                                'match': matched_text
                            })
        
        if issues:
            message = "Found potential hardcoded credentials:\n"
            for issue in issues[:5]:  # Show first 5
                message += f"  {issue['file']}:{issue['line']}: {issue['match']}\n"
            pytest.fail(message)
    
    def test_env_example_exists(self):
        """Test that .env.example file exists."""
        project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
        env_example = os.path.join(project_root, '.env.example')
        assert os.path.exists(env_example), ".env.example file should exist"
    
    def test_security_md_exists(self):
        """Test that SECURITY.md file exists."""
        project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
        security_md = os.path.join(project_root, 'SECURITY.md')
        assert os.path.exists(security_md), "SECURITY.md file should exist"


class TestDependencySecurity:
    """Test dependency security."""
    
    def test_authlib_version(self):
        """Test that authlib is at least version 1.6.5."""
        requirements_path = os.path.join(
            os.path.dirname(__file__), '..', 'requirements.txt'
        )
        
        with open(requirements_path, 'r') as f:
            content = f.read()
        
        # Check for authlib with version constraint
        assert 'authlib>=1.6.5' in content, \
            "authlib should be pinned to >= 1.6.5 to fix known vulnerabilities"


class TestInputValidation:
    """Test that user input is properly validated."""
    
    def test_sql_injection_prevention(self):
        """Test that SQLAlchemy ORM is used (not raw SQL)."""
        project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
        musicround_dir = os.path.join(project_root, 'musicround')
        
        # Look for dangerous SQL patterns
        dangerous_patterns = [
            r'\.execute\(["\'].*%s.*["\']',  # .execute("... %s ...")
            r'\.execute\(["\'].*\+.*["\']',  # .execute("... " + var)
            r'\.execute\(f["\']',  # .execute(f"...")
        ]
        
        issues = []
        
        for root, dirs, files in os.walk(musicround_dir):
            dirs[:] = [d for d in dirs if d != '__pycache__']
            
            for file in files:
                if not file.endswith('.py'):
                    continue
                
                filepath = os.path.join(root, file)
                with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
                    content = f.read()
                    
                    for pattern in dangerous_patterns:
                        matches = re.finditer(pattern, content)
                        for match in matches:
                            issues.append({
                                'file': filepath,
                                'line': content[:match.start()].count('\n') + 1,
                                'match': match.group(0)
                            })
        
        # migrations are allowed to use raw SQL
        issues = [i for i in issues if '/migrations/' not in i['file']]
        
        if issues:
            message = "Found potential SQL injection risks:\n"
            for issue in issues[:5]:
                message += f"  {issue['file']}:{issue['line']}: {issue['match']}\n"
            pytest.fail(message)


class TestSecureDefaults:
    """Test that secure defaults are in place."""
    
    def test_debug_disabled_by_default(self):
        """Test that DEBUG is disabled by default in config."""
        # Check .env.example
        env_example_path = os.path.join(os.path.dirname(__file__), '..', '.env.example')
        with open(env_example_path, 'r') as f:
            content = f.read()
        
        # Should have DEBUG=False
        assert 'DEBUG=False' in content, ".env.example should have DEBUG=False"
    
    def test_https_recommended(self):
        """Test that HTTPS is documented as recommended."""
        security_md_path = os.path.join(os.path.dirname(__file__), '..', 'SECURITY.md')
        with open(security_md_path, 'r') as f:
            content = f.read()
        
        assert 'HTTPS' in content or 'https' in content, \
            "SECURITY.md should mention HTTPS"
        assert 'USE_HTTPS' in content, \
            "SECURITY.md should document USE_HTTPS setting"


class TestSecretManagement:
    """Test secret management practices."""
    
    def test_gitignore_includes_env(self):
        """Test that .env is in .gitignore."""
        gitignore_path = os.path.join(os.path.dirname(__file__), '..', '.gitignore')
        with open(gitignore_path, 'r') as f:
            content = f.read()
        
        assert '.env' in content, ".env should be in .gitignore"
    
    def test_no_env_files_committed(self):
        """Test that actual .env files are not in git."""
        project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
        
        # .env.example is OK, but .env should not exist in repo
        # (it will exist locally but should be gitignored)
        # We're checking if demo files have placeholder values
        
        demo_files = ['.env.demo', '.env.oauth', '.env.oauth.production']
        for demo_file in demo_files:
            demo_path = os.path.join(project_root, demo_file)
            if os.path.exists(demo_path):
                with open(demo_path, 'r') as f:
                    content = f.read()
                
                # Check that these don't contain real-looking credentials
                # Real API keys are usually 32+ characters of alphanumeric
                patterns = [
                    r'[A-Za-z0-9]{40,}',  # Very long alphanumeric strings
                ]
                
                for pattern in patterns:
                    matches = re.findall(pattern, content)
                    for match in matches:
                        # Skip if it's obviously a placeholder
                        if any(x in match.lower() for x in [
                            'your-', 'example', 'changeme', 'placeholder'
                        ]):
                            continue
                        
                        # This might be a real credential
                        if len(match) > 40:
                            pytest.fail(
                                f"Found potential real credential in {demo_file}: "
                                f"{match[:20]}... (redacted)"
                            )
