#!/usr/bin/env python3
"""
Test Suite: Input Validation (UVS-H06, UVS-H48)
===============================================
Tests input validation layer for tool arguments.

VERIFICATION_STAMP
Story: UVS-H06, UVS-H48
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.input_validator import (
    validate_coordinates,
    validate_string_length,
    validate_enum,
    validate_url,
    validate_tool_args,
    ValidationError,
    ValidationResult,
    MAX_VIEWPORT_WIDTH,
    MAX_VIEWPORT_HEIGHT,
    MAX_STRING_LENGTH,
    CURSOR_TOOL_SCHEMA,
    GESTURE_TOOL_SCHEMA
)


class TestValidateCoordinates:
    """Tests for coordinate validation."""

    def test_valid_coordinates(self):
        """Valid coordinates pass through unchanged."""
        result = validate_coordinates(100, 200)
        assert result.valid
        assert result.value == (100, 200)

    def test_zero_coordinates(self):
        """Zero coordinates are valid."""
        result = validate_coordinates(0, 0)
        assert result.valid
        assert result.value == (0, 0)

    def test_max_coordinates(self):
        """Max viewport coordinates are valid."""
        result = validate_coordinates(MAX_VIEWPORT_WIDTH, MAX_VIEWPORT_HEIGHT)
        assert result.valid
        assert result.value == (MAX_VIEWPORT_WIDTH, MAX_VIEWPORT_HEIGHT)

    def test_negative_x_clamped(self):
        """Negative X is clamped to 0."""
        result = validate_coordinates(-100, 200)
        assert not result.valid
        assert result.value[0] == 0
        assert result.value[1] == 200

    def test_negative_y_clamped(self):
        """Negative Y is clamped to 0."""
        result = validate_coordinates(100, -200)
        assert not result.valid
        assert result.value[0] == 100
        assert result.value[1] == 0

    def test_overflow_x_clamped(self):
        """X beyond max is clamped."""
        result = validate_coordinates(5000, 200)
        assert not result.valid
        assert result.value[0] == MAX_VIEWPORT_WIDTH

    def test_overflow_y_clamped(self):
        """Y beyond max is clamped."""
        result = validate_coordinates(100, 3000)
        assert not result.valid
        assert result.value[1] == MAX_VIEWPORT_HEIGHT

    def test_string_coordinates_converted(self):
        """String coordinates are converted to int."""
        result = validate_coordinates("100", "200")
        assert result.valid
        assert result.value == (100, 200)

    def test_invalid_type_raises(self):
        """Non-numeric types raise ValidationError."""
        with pytest.raises(ValidationError):
            validate_coordinates("abc", 200)


class TestValidateStringLength:
    """Tests for string length validation."""

    def test_valid_string(self):
        """Valid length string passes."""
        result = validate_string_length("hello")
        assert result.valid
        assert result.value == "hello"

    def test_empty_string(self):
        """Empty string is valid."""
        result = validate_string_length("")
        assert result.valid
        assert result.value == ""

    def test_none_becomes_empty(self):
        """None becomes empty string."""
        result = validate_string_length(None)
        assert result.valid
        assert result.value == ""

    def test_max_length_string(self):
        """String at max length passes."""
        s = "a" * MAX_STRING_LENGTH
        result = validate_string_length(s)
        assert result.valid
        assert len(result.value) == MAX_STRING_LENGTH

    def test_overflow_truncated(self):
        """String beyond max is truncated."""
        s = "a" * (MAX_STRING_LENGTH + 100)
        result = validate_string_length(s)
        assert not result.valid
        assert len(result.value) == MAX_STRING_LENGTH

    def test_custom_max_length(self):
        """Custom max length is respected."""
        result = validate_string_length("hello world", max_length=5)
        assert not result.valid
        assert result.value == "hello"


class TestValidateEnum:
    """Tests for enum validation."""

    def test_valid_enum(self):
        """Valid enum value passes."""
        result = validate_enum("circle", ["circle", "underline", "point"])
        assert result.valid
        assert result.value == "circle"

    def test_invalid_enum_raises(self):
        """Invalid enum value raises ValidationError."""
        with pytest.raises(ValidationError):
            validate_enum("invalid", ["circle", "underline", "point"])

    def test_case_sensitive_default(self):
        """Enum comparison is case-sensitive by default."""
        with pytest.raises(ValidationError):
            validate_enum("CIRCLE", ["circle", "underline", "point"])

    def test_case_insensitive(self):
        """Case-insensitive comparison when specified."""
        result = validate_enum("CIRCLE", ["circle", "underline"], case_insensitive=True)
        assert result.valid


class TestValidateUrl:
    """Tests for URL validation."""

    def test_valid_https_url(self):
        """Valid HTTPS URL passes."""
        result = validate_url("https://example.com/path")
        assert result.valid

    def test_valid_http_url(self):
        """Valid HTTP URL passes."""
        result = validate_url("http://example.com")
        assert result.valid

    def test_invalid_scheme_raises(self):
        """Invalid scheme raises ValidationError."""
        with pytest.raises(ValidationError):
            validate_url("ftp://example.com")

    def test_file_scheme_blocked(self):
        """File scheme is blocked."""
        with pytest.raises(ValidationError):
            validate_url("file:///etc/passwd")

    def test_javascript_scheme_blocked(self):
        """JavaScript scheme is blocked."""
        with pytest.raises(ValidationError):
            validate_url("javascript:alert(1)")

    def test_missing_host_raises(self):
        """URL without host raises ValidationError."""
        with pytest.raises(ValidationError):
            validate_url("https://")

    def test_custom_allowed_schemes(self):
        """Custom allowed schemes work."""
        result = validate_url("https://example.com", allowed_schemes=["https"])
        assert result.valid

        with pytest.raises(ValidationError):
            validate_url("http://example.com", allowed_schemes=["https"])


class TestValidateToolArgs:
    """Tests for complete tool argument validation."""

    def test_cursor_tool_valid(self):
        """Valid cursor tool args pass."""
        args = {"x": 100, "y": 200}
        result = validate_tool_args(args, CURSOR_TOOL_SCHEMA)
        assert result["x"] == 100
        assert result["y"] == 200

    def test_cursor_tool_clamped(self):
        """Out-of-bounds cursor args are clamped."""
        args = {"x": -50, "y": 5000}
        result = validate_tool_args(args, CURSOR_TOOL_SCHEMA)
        assert result["x"] == 0
        assert result["y"] == MAX_VIEWPORT_HEIGHT

    def test_missing_required_raises(self):
        """Missing required arg raises ValidationError."""
        args = {"x": 100}  # Missing y
        with pytest.raises(ValidationError):
            validate_tool_args(args, CURSOR_TOOL_SCHEMA)

    def test_gesture_tool_valid(self):
        """Valid gesture tool args pass."""
        args = {"type": "circle", "selector": "#button"}
        result = validate_tool_args(args, GESTURE_TOOL_SCHEMA)
        assert result["type"] == "circle"
        assert result["selector"] == "#button"

    def test_gesture_invalid_type_raises(self):
        """Invalid gesture type raises ValidationError."""
        args = {"type": "invalid", "selector": "#button"}
        with pytest.raises(ValidationError):
            validate_tool_args(args, GESTURE_TOOL_SCHEMA)


class TestEdgeCases:
    """Edge case tests for config validation (UVS-H48)."""

    def test_negative_viewport_handled(self):
        """Negative viewport values are handled."""
        result = validate_coordinates(-1000, -1000, max_width=100, max_height=100)
        assert result.value == (0, 0)

    def test_zero_max_length(self):
        """Zero max length truncates to empty."""
        result = validate_string_length("hello", max_length=0)
        assert result.value == ""

    def test_unicode_string(self):
        """Unicode strings are handled."""
        result = validate_string_length("Hello 世界 🌍")
        assert result.valid
        assert "世界" in result.value

    def test_very_long_url_rejected(self):
        """URLs over 2048 chars are rejected."""
        long_url = "https://example.com/" + "a" * 3000
        with pytest.raises(ValidationError):
            validate_url(long_url)


if __name__ == '__main__':
    pytest.main([__file__, '-v', '--tb=short'])
