"""Tests for JavaScript AST analysis."""
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.js_ast_analyzer import analyze_js_ast, is_esprima_available
from isnad_scan.patterns import Severity


# Skip all tests if esprima not available
pytestmark = pytest.mark.skipif(
    not is_esprima_available(),
    reason="esprima not available"
)


class TestImportAliasing:
    """Test detection of dangerous import aliasing."""
    
    def test_require_child_process(self):
        code = '''
        const cp = require('child_process');
        cp.exec('ls');
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0
        assert any("child_process" in f.description.lower() or "dangerous" in f.pattern_id for f in findings)
    
    def test_aliased_exec(self):
        code = '''
        const s = require('child_process').exec;
        s('whoami');
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0
        # Should detect the aliasing
        assert any("alias" in f.pattern_id.lower() for f in findings)
    
    def test_destructured_import(self):
        code = '''
        const { exec, spawn } = require('child_process');
        exec('ls -la');
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) >= 2  # Both exec and spawn
        assert any("exec" in f.description.lower() for f in findings)
    
    def test_es6_import_alias(self):
        code = '''
        import { exec as e } from 'child_process';
        e('command');
        '''
        findings = analyze_js_ast(code, "test.mjs")
        assert len(findings) > 0
        assert any("alias" in f.description.lower() or "import" in f.pattern_id for f in findings)
    
    def test_namespace_import(self):
        code = '''
        import * as cp from 'child_process';
        cp.execSync('cmd');
        '''
        findings = analyze_js_ast(code, "test.mjs")
        assert len(findings) > 0


class TestDynamicRequire:
    """Test detection of dynamic require patterns."""
    
    def test_variable_require(self):
        code = '''
        const modName = getUserInput();
        const mod = require(modName);
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0
        assert any("dynamic" in f.pattern_id.lower() for f in findings)
    
    def test_concatenated_require(self):
        code = '''
        const mod = require('./modules/' + name);
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0
        assert any("dynamic" in f.pattern_id.lower() for f in findings)


class TestDynamicCodeExecution:
    """Test detection of dynamic code execution patterns."""
    
    def test_function_constructor(self):
        code = '''
        const fn = new Function('a', 'b', 'return a + b');
        fn(1, 2);
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0
        assert any("function" in f.pattern_id.lower() and "constructor" in f.pattern_id.lower() for f in findings)
    
    def test_settimeout_string(self):
        code = '''
        setTimeout("alert('xss')", 1000);
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0
        assert any("settimeout" in f.pattern_id.lower() for f in findings)
    
    def test_setinterval_string(self):
        code = '''
        setInterval("doEvil()", 5000);
        '''
        findings = analyze_js_ast(code, "test.js")
        # setInterval with string should be caught, but if parsing fails, skip
        # The regex-based analyzer in js_analyzer.py will catch this anyway
        if findings:
            assert any("interval" in f.pattern_id.lower() or "settimeout" in f.pattern_id.lower() for f in findings)
    
    def test_dynamic_global_access(self):
        code = '''
        const funcName = 'eval';
        global[funcName]('code');
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0
        assert any("global" in f.pattern_id.lower() for f in findings)
    
    def test_window_dynamic_access(self):
        code = '''
        const fn = window[functionName];
        fn(payload);
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0


class TestPrototypePollution:
    """Test detection of prototype pollution patterns."""
    
    def test_proto_access(self):
        code = '''
        var obj = {};
        obj.__proto__.polluted = true;
        '''
        findings = analyze_js_ast(code, "test.js")
        # Note: AST analysis may not catch all __proto__ if parsed as identifier
        # The regex in js_analyzer.py is the primary detection
        if findings:
            assert any("proto" in f.pattern_id.lower() for f in findings)
    
    def test_proto_computed_access(self):
        code = '''
        obj['__proto__']['isAdmin'] = true;
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0
        assert any("proto" in f.pattern_id.lower() for f in findings)
    
    def test_constructor_prototype(self):
        code = '''
        obj.constructor.prototype.polluted = true;
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0
        assert any("constructor" in f.pattern_id.lower() or "prototype" in f.pattern_id.lower() for f in findings)
    
    def test_setprototypeof(self):
        code = '''
        Object.setPrototypeOf(target, evilProto);
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) > 0


class TestAliasedCalls:
    """Test detection of calls to aliased dangerous functions."""
    
    def test_aliased_exec_call(self):
        code = '''
        const run = require('child_process').exec;
        run('whoami', callback);
        '''
        findings = analyze_js_ast(code, "test.js")
        # Should detect both the alias creation AND the call
        assert len(findings) >= 2
        assert any("aliased" in f.pattern_id.lower() and "call" in f.pattern_id.lower() for f in findings)
    
    def test_destructured_then_called(self):
        code = '''
        const { execSync: run } = require('child_process');
        const output = run('ls');
        '''
        findings = analyze_js_ast(code, "test.js")
        assert len(findings) >= 1


class TestSafePatterns:
    """Test that safe patterns don't trigger false positives."""
    
    def test_settimeout_with_function(self):
        # This is safe - function reference, not string
        code = '''
        setTimeout(function() {
            console.log('hello');
        }, 1000);
        '''
        findings = analyze_js_ast(code, "test.js")
        # Should not flag setTimeout with function
        settimeout_findings = [f for f in findings if "settimeout" in f.pattern_id.lower()]
        assert len(settimeout_findings) == 0
    
    def test_safe_require(self):
        code = '''
        const fs = require('lodash');
        const data = fs.merge(a, b);
        '''
        findings = analyze_js_ast(code, "test.js")
        # lodash is safe, shouldn't trigger dangerous module warning
        dangerous = [f for f in findings if f.severity == Severity.DANGER]
        assert len(dangerous) == 0


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