#!/usr/bin/env python3
"""
Test Suite: CSS Selector Sanitization (UVS-H01)
===============================================
Black box and white box tests for selector_sanitizer.py

VERIFICATION_STAMP
Story: UVS-H01
Verified By: Claude Opus 4.5
Verified At: 2026-02-03
"""

import sys
import pytest
sys.path.insert(0, '/mnt/e/genesis-system')

from core.security.selector_sanitizer import (
    css_escape,
    sanitize_selector,
    validate_selector,
    detect_dangerous_patterns,
    escape_selector_value,
    SelectorValidationError,
    GHL_SELECTOR_ALLOWLIST
)


class TestCssEscape:
    """White box tests for CSS.escape() implementation."""

    def test_empty_string(self):
        """Empty string returns empty."""
        assert css_escape('') == ''

    def test_simple_identifier(self):
        """Simple identifiers pass through."""
        assert css_escape('button') == 'button'
        assert css_escape('myClass') == 'myClass'
        assert css_escape('data-id') == 'data-id'

    def test_leading_digit(self):
        """Leading digits are escaped."""
        result = css_escape('123abc')
        assert result.startswith('\\')
        assert 'abc' in result

    def test_leading_hyphen_digit(self):
        """Hyphen followed by digit at start is escaped."""
        result = css_escape('-1test')
        assert result.startswith('\\')

    def test_null_byte(self):
        """Null bytes become replacement character."""
        assert css_escape('\x00') == '\uFFFD'

    def test_control_characters(self):
        """Control characters are hex-escaped."""
        result = css_escape('\x01')
        assert '\\' in result

    def test_special_characters(self):
        """Special CSS characters are backslash-escaped."""
        assert css_escape('a.b') == 'a\\.b'
        assert css_escape('a#b') == 'a\\#b'
        assert css_escape('a[b') == 'a\\[b'

    def test_unicode_passthrough(self):
        """Non-ASCII Unicode passes through."""
        assert css_escape('') == ''
        assert css_escape('') == ''


class TestSanitizeSelector:
    """Black box tests for XSS prevention."""

    def test_valid_id_selector(self):
        """Valid ID selectors pass."""
        assert sanitize_selector('#myButton') == '#myButton'

    def test_valid_class_selector(self):
        """Valid class selectors pass."""
        assert sanitize_selector('.submit-btn') == '.submit-btn'

    def test_valid_attribute_selector(self):
        """Valid attribute selectors pass."""
        assert sanitize_selector('[data-testid="submit"]') == '[data-testid="submit"]'

    def test_block_javascript_protocol(self):
        """Block javascript: protocol."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('a[href="javascript:alert(1)"]')

    def test_block_event_handlers(self):
        """Block onclick= style event handlers."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('[onclick="malicious()"]')

    def test_block_script_tags(self):
        """Block script tag injection."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('<script>alert(1)</script>')

    def test_block_eval(self):
        """Block eval() patterns."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('[data-x="eval(code)"]')

    def test_block_document_access(self):
        """Block document.* access."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('document.cookie')

    def test_block_fetch(self):
        """Block fetch() calls."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('[data-x="fetch(url)"]')

    def test_block_xmlhttprequest(self):
        """Block XMLHttpRequest."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('XMLHttpRequest')

    def test_block_innerhtml(self):
        """Block innerHTML manipulation."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('.innerHTML=malicious')

    def test_empty_selector_rejected(self):
        """Empty selector raises error."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('')

    def test_long_selector_rejected(self):
        """Selector > 1000 chars rejected."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('a' * 1001)

    def test_complex_valid_selector(self):
        """Complex but valid selectors pass."""
        selector = 'div.container > ul#menu li:nth-child(2) a[href^="https"]'
        assert sanitize_selector(selector) == selector

    def test_disallow_attribute_selectors_option(self):
        """Can optionally block all attribute selectors."""
        with pytest.raises(SelectorValidationError):
            sanitize_selector('[data-id="x"]', allow_attribute_selectors=False)


class TestDetectDangerousPatterns:
    """White box tests for pattern detection."""

    def test_detect_javascript_protocol(self):
        """Detect javascript: variations."""
        assert detect_dangerous_patterns('javascript:void(0)') is not None
        assert detect_dangerous_patterns('JAVASCRIPT:alert(1)') is not None
        assert detect_dangerous_patterns('  javascript : code') is not None

    def test_detect_data_protocol(self):
        """Detect data: protocol."""
        assert detect_dangerous_patterns('data:text/html,<script>') is not None

    def test_detect_expression(self):
        """Detect CSS expression()."""
        assert detect_dangerous_patterns('expression(alert(1))') is not None

    def test_detect_settimeout(self):
        """Detect setTimeout()."""
        assert detect_dangerous_patterns('setTimeout(fn, 0)') is not None

    def test_detect_dynamic_import(self):
        """Detect dynamic import()."""
        assert detect_dangerous_patterns('import("module")') is not None

    def test_safe_patterns_pass(self):
        """Safe patterns return None."""
        assert detect_dangerous_patterns('#button') is None
        assert detect_dangerous_patterns('.my-class') is None
        assert detect_dangerous_patterns('[data-testid="x"]') is None


class TestValidateSelector:
    """Tests for allowlist-based validation."""

    def test_valid_without_allowlist(self):
        """Valid selector passes without allowlist."""
        valid, reason = validate_selector('#myId')
        assert valid is True

    def test_id_selector_in_ghl_allowlist(self):
        """ID selector matches GHL allowlist."""
        valid, reason = validate_selector('#submit-btn', GHL_SELECTOR_ALLOWLIST)
        assert valid is True

    def test_class_selector_in_ghl_allowlist(self):
        """Class selector matches GHL allowlist."""
        valid, reason = validate_selector('.primary-btn', GHL_SELECTOR_ALLOWLIST)
        assert valid is True

    def test_complex_selector_not_in_allowlist(self):
        """Complex selector rejected by strict allowlist."""
        valid, reason = validate_selector('div > span.text', GHL_SELECTOR_ALLOWLIST)
        assert valid is False
        assert 'pattern' in reason.lower()


class TestEscapeSelectorValue:
    """Tests for value escaping in selectors."""

    def test_escape_quotes(self):
        """Quotes are properly escaped."""
        result = escape_selector_value('test"value')
        assert '\\"' in result

    def test_escape_single_quotes(self):
        """Single quotes are escaped."""
        result = escape_selector_value("test'value")
        assert "\\'" in result

    def test_escape_backslashes(self):
        """Backslashes are escaped."""
        result = escape_selector_value('test\\value')
        assert '\\\\' in result


class TestOWASPXSSVectors:
    """Black box tests using OWASP XSS filter evasion cheat sheet vectors."""

    OWASP_VECTORS = [
        '<script>alert(1)</script>',
        '<img src=x onerror=alert(1)>',
        '<svg onload=alert(1)>',
        'javascript:alert(1)',
        'data:text/html,<script>alert(1)</script>',
        '<body onload=alert(1)>',
        '<iframe src="javascript:alert(1)">',
        '"><script>alert(1)</script>',
        "'-alert(1)-'",
        'expression(alert(1))',
        '<a href="javascript:alert(1)">click</a>',
        '<div style="width:expression(alert(1))">',
    ]

    @pytest.mark.parametrize("vector", OWASP_VECTORS)
    def test_owasp_vectors_blocked(self, vector):
        """All OWASP XSS vectors should be blocked."""
        # Either raises error or pattern is detected
        try:
            result = sanitize_selector(vector)
            # If it didn't raise, check if dangerous pattern was detected
            assert detect_dangerous_patterns(vector) is None, f"Vector passed: {vector}"
        except SelectorValidationError:
            pass  # Expected - blocked


class TestRealWorldSelectors:
    """Tests with real GHL-like selectors."""

    VALID_GHL_SELECTORS = [
        '#hl_sub_account_save',
        '.hl-btn-primary',
        '[data-testid="api-key-input"]',
        'input[name="apiKey"]',
        'button.submit-action',
        '#settings-form input[type="text"]',
    ]

    @pytest.mark.parametrize("selector", VALID_GHL_SELECTORS)
    def test_valid_ghl_selectors_pass(self, selector):
        """Real GHL selectors should pass validation."""
        result = sanitize_selector(selector)
        assert result == selector


if __name__ == '__main__':
    pytest.main([__file__, '-v', '--tb=short'])
