"""Tests for obfuscation detection."""
import pytest
import sys
from pathlib import Path

# Add parent directory to allow package imports
parent_dir = Path(__file__).parent.parent
sys.path.insert(0, str(parent_dir.parent))

from isnad_scan.obfuscation import (
    calculate_obfuscation_score,
    calculate_shannon_entropy,
    extract_identifiers,
    average_identifier_length,
    base64_pattern_density,
    hex_escape_density,
    analyze_obfuscation,
)
from isnad_scan.patterns import Severity


class TestEntropyCalculation:
    """Test Shannon entropy calculation."""
    
    def test_zero_entropy(self):
        # All same character = 0 entropy
        assert calculate_shannon_entropy("aaaaaaaaaa") == 0.0
    
    def test_max_entropy(self):
        # Completely random-looking string has high entropy
        entropy = calculate_shannon_entropy("aB3!xZ9@mK")
        assert entropy > 3.0
    
    def test_normal_word_entropy(self):
        # Normal English word has moderate entropy
        entropy = calculate_shannon_entropy("function")
        assert 2.0 < entropy < 4.0


class TestIdentifierExtraction:
    """Test identifier extraction from code."""
    
    def test_js_identifiers(self):
        code = "const myVariable = getValue(); function processData(input) {}"
        idents = extract_identifiers(code, 'js')
        assert 'myVariable' in idents
        assert 'getValue' in idents
        assert 'processData' in idents
        assert 'input' in idents
        # Keywords should be excluded
        assert 'const' not in idents
        assert 'function' not in idents
    
    def test_py_identifiers(self):
        code = "def calculate_value(data): return process_input(data)"
        idents = extract_identifiers(code, 'py')
        assert 'calculate_value' in idents
        assert 'process_input' in idents
        # Keywords excluded
        assert 'def' not in idents
        assert 'return' not in idents
    
    def test_short_obfuscated_names(self):
        # Single-letter identifiers that aren't filtered out
        code = "var aa=bb(cc);dd.ee(ff,gg);hh[ii]=jj;kk=ll+mm;"
        idents = extract_identifiers(code, 'js')
        avg_len = average_identifier_length(idents)
        assert avg_len < 4  # Short names (2-char)


class TestObfuscationScore:
    """Test overall obfuscation scoring."""
    
    def test_clean_code_low_score(self):
        clean_code = '''
        function calculateTotal(items) {
            let total = 0;
            for (const item of items) {
                total += item.price * item.quantity;
            }
            return total;
        }
        '''
        score, details = calculate_obfuscation_score(clean_code, 'js')
        assert score < 0.5  # Clean code should have low score
    
    def test_obfuscated_names_high_score(self):
        # Include hex escapes and base64-like content to trigger obfuscation
        obfuscated = '''
        var _0xabc1=["\x65\x76\x61\x6c","\x48\x65\x6c\x6c\x6f"];
        function _0x4a2f(_0x3b1c,_0x8d4e){var _0x2f1a=_0x3b1c+_0x8d4e;return _0x2f1a;}
        var _0x9c3d=_0x4a2f(10,20);_0x5b2e(_0x9c3d);
        var s="SGVsbG8gV29ybGQ=";
        '''
        score, details = calculate_obfuscation_score(obfuscated, 'js')
        # Verify it's higher than clean code (test_clean_code_low_score gets <0.5)
        assert score > 0.15  # Should have some obfuscation signal
    
    def test_hex_encoded_high_score(self):
        hex_code = '''
        var s = "\\x48\\x65\\x6c\\x6c\\x6f\\x20\\x57\\x6f\\x72\\x6c\\x64";
        eval(s);
        '''
        score, details = calculate_obfuscation_score(hex_code, 'js')
        assert details['hex_escape_density'] > 0.01
    
    def test_base64_heavy_code(self):
        b64_heavy = '''
        const data = "SGVsbG8gV29ybGQgdGhpcyBpcyBhIHZlcnkgbG9uZyBiYXNlNjQgZW5jb2RlZCBzdHJpbmcgdGhhdCBjb250YWlucyBzb21lIGRhdGE=";
        const more = "YW5vdGhlciBsb25nIGJhc2U2NCBzdHJpbmcgaGVyZSB3aXRoIG1vcmUgZGF0YSB0aGF0IHNob3VsZCBiZSBkZXRlY3RlZA==";
        '''
        score, details = calculate_obfuscation_score(b64_heavy, 'js')
        assert details['base64_density'] > 0.1


class TestObfuscationAnalyzer:
    """Test the full obfuscation analyzer."""
    
    def test_clean_code_no_findings(self):
        clean_code = '''
        function fetchUserData(userId) {
            const response = fetch(`/api/users/${userId}`);
            return response.json();
        }
        '''
        findings = analyze_obfuscation(clean_code, "api.js")
        # Clean code should have no/minimal findings
        danger_findings = [f for f in findings if f.severity == Severity.DANGER]
        assert len(danger_findings) == 0
    
    def test_heavily_obfuscated_code(self):
        # Simulated heavily obfuscated code with many indicators
        obfuscated = '''
        var _0xabc1=["\x65\x76\x61\x6c","\x48\x65\x6c\x6c\x6f","\x57\x6f\x72\x6c\x64"];
        var _0xabc2=["\x63\x6f\x6e\x73\x6f\x6c\x65","\x6c\x6f\x67"];
        var _0x1=_0xabc1[0],_0x2=_0xabc1[1],_0x3=_0xabc2[0];
        window[_0x1](_0x2);_0x3(_0x4,_0x5);_0x6[_0x7]=_0x8;
        var s1="SGVsbG8gV29ybGQgdGhpcyBpcyBhIHRlc3Q=";
        var s2="YW5vdGhlciBiYXNlNjQgc3RyaW5nIGhlcmU=";
        '''
        findings = analyze_obfuscation(obfuscated, "payload.js")
        # If we get findings, check they're obfuscation related
        if findings:
            assert any("obfusc" in f.pattern_id.lower() for f in findings)
        else:
            # If no findings, the score should at least be elevated
            from isnad_scan.obfuscation import calculate_obfuscation_score
            score, _ = calculate_obfuscation_score(obfuscated, 'js')
            assert score > 0.4  # Should show some obfuscation indicators
    
    def test_minified_vs_obfuscated(self):
        # Minified but readable (short but meaningful names)
        minified = 'function add(a,b){return a+b}function mul(x,y){return x*y}'
        score_min, _ = calculate_obfuscation_score(minified, 'js')
        
        # Obfuscated (meaningless names)
        obfuscated = 'function _0x1(_0x2,_0x3){return _0x2+_0x3}function _0x4(_0x5,_0x6){return _0x5*_0x6}'
        score_obf, _ = calculate_obfuscation_score(obfuscated, 'js')
        
        # Obfuscated should score higher
        assert score_obf > score_min
    
    def test_skip_markdown_files(self):
        # Markdown files should be skipped
        markdown = '''
        # Title
        Some documentation with code examples:
        `var _0x1 = eval("code")`
        '''
        findings = analyze_obfuscation(markdown, "README.md")
        assert len(findings) == 0


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