"""
Contract-based test suite for src_hook.rs
Policy Enforcement Decision Engine for Claude Code PreToolUse hook

Generated from contract version 1
Tests cover: evaluate_preflight_constraint, run_hook, emit_decision, emit_allow,
emit_deny, resolve_ensure, resolve_ensure_result, and type serialization/deserialization
"""

import pytest
import json
import sys
from io import StringIO
from unittest.mock import Mock, MagicMock, patch, call, mock_open
from typing import Optional, Dict, Any
import random
import string


# Mock module import - in real scenario this would import the Rust bindings
# For testing purposes, we'll create mock classes and functions
class HookInput:
    """Deserialized JSON input from Claude Code PreToolUse hook"""
    def __init__(self, tool_name: str, parameters: Optional[Dict[str, Any]] = None):
        self.tool_name = tool_name
        self.parameters = parameters
    
    @classmethod
    def from_json(cls, json_str: str):
        data = json.loads(json_str)
        return cls(data['tool_name'], data.get('parameters'))


class HookOutput:
    """Actual hook decision payload with reason and context"""
    def __init__(self, hook_event_name: str, permission_decision: str, 
                 reason: Optional[str] = None, additional_context: Optional[str] = None):
        self.hook_event_name = hook_event_name
        self.permission_decision = permission_decision
        self.reason = reason
        self.additional_context = additional_context
    
    def to_dict(self):
        result = {
            'hook_event_name': self.hook_event_name,
            'permission_decision': self.permission_decision
        }
        if self.reason is not None:
            result['reason'] = self.reason
        if self.additional_context is not None:
            result['additional_context'] = self.additional_context
        return result


class HookResponse:
    """Top-level response wrapper expected by Claude Code"""
    def __init__(self, hook_specific_output: HookOutput):
        self.hook_specific_output = hook_specific_output
    
    def to_json(self):
        return json.dumps({'hook_specific_output': self.hook_specific_output.to_dict()})


class SoftConstraint:
    """Preflight soft constraint"""
    def __init__(self, tool_pattern: str, conditions: list, reason: str, alternative: str, action: str):
        self.tool_pattern = tool_pattern
        self.conditions = conditions
        self.reason = reason
        self.alternative = alternative
        self.action = action


class ToolCall:
    """Represents a tool call to evaluate"""
    def __init__(self, tool_name: str, parameters: Optional[Dict[str, Any]] = None):
        self.tool_name = tool_name
        self.parameters = parameters or {}


class Preflight:
    """Preflight configuration with soft constraints"""
    def __init__(self, constraints: list):
        self.constraints = constraints


class Vault:
    """Vault for logging actions"""
    def __init__(self):
        self.logged_actions = []
    
    def log_action(self, decision: str, tool_name: str, category: str, 
                   amount: float, parameter_detail: str):
        self.logged_actions.append({
            'decision': decision,
            'tool_name': tool_name,
            'category': category,
            'amount': amount,
            'parameter_detail': parameter_detail[:500]  # Truncate to 500 chars
        })


class CompiledPolicy:
    """Compiled policy for evaluation"""
    def __init__(self, rules: list):
        self.rules = rules


class EnsureConfig:
    """Configuration for ensure checks"""
    def __init__(self, check: str, timeout: int, message: Optional[str] = None):
        self.check = check
        self.timeout = timeout
        self.message = message


class Decision:
    """Decision types"""
    Allow = "allow"
    Deny = "deny"
    Ask = "ask"
    Ensure = "ensure"


class EvaluationResult:
    """Result from policy evaluation"""
    def __init__(self, decision: str, reason: Optional[str] = None, 
                 ensure_config: Optional[EnsureConfig] = None,
                 matched_locked: bool = False):
        self.decision = decision
        self.reason = reason
        self.ensure_config = ensure_config
        self.matched_locked = matched_locked


# Mock implementations of the functions under test
def evaluate_preflight_constraint(call: ToolCall, preflight: Preflight, vault: Vault) -> Optional[tuple]:
    """Mock implementation"""
    import re
    for constraint in preflight.constraints:
        if re.match(constraint.tool_pattern, call.tool_name):
            # Check all conditions (simplified - in real implementation would evaluate against vault)
            if all(constraint.conditions):  # Simplified condition check
                reason = f"{constraint.reason} Instead: {constraint.alternative}"
                return (constraint, reason)
    return None


def run_hook(policy: CompiledPolicy, vault: Optional[Vault] = None) -> int:
    """Mock implementation"""
    # Always returns 0 as per contract
    return 0


def emit_decision(decision: str, reason: Optional[str] = None, 
                 additional_context: Optional[str] = None):
    """Mock implementation"""
    output = HookOutput('PreToolUse', decision, reason, additional_context)
    response = HookResponse(output)
    print(response.to_json())


def emit_allow():
    """Mock implementation"""
    emit_decision('allow', None, None)


def emit_deny(reason: str):
    """Mock implementation"""
    emit_decision('deny', reason, None)


def resolve_ensure(config: EnsureConfig) -> tuple:
    """Mock implementation"""
    # Clamp timeout
    timeout = max(1, min(30, config.timeout))
    # Mock execution - would actually run subprocess
    return (True, "")


def resolve_ensure_result(result: EvaluationResult) -> EvaluationResult:
    """Mock implementation"""
    if result.ensure_config is None:
        result.decision = Decision.Deny
        result.reason = 'Ensure rule missing ensure config'
        return result
    
    passed, stderr = resolve_ensure(result.ensure_config)
    if passed:
        result.decision = Decision.Allow
    else:
        result.decision = Decision.Deny
        result.reason = result.ensure_config.message or f"Ensure check failed: {stderr}"
    result.ensure_config = None
    return result


# =============================================================================
# UNIT TESTS - evaluate_preflight_constraint
# =============================================================================

class TestEvaluatePreflightConstraint:
    """Unit tests for evaluate_preflight_constraint function"""
    
    def test_evaluate_preflight_constraint_happy_path_matching_constraint(self):
        """evaluate_preflight_constraint returns Some((constraint, reason)) when tool_name matches and all conditions pass"""
        # Arrange
        tool_call = ToolCall('write_file', {'path': '/tmp/test.txt'})
        constraint = SoftConstraint(
            tool_pattern=r'^write_file$',
            conditions=[True, True],  # All conditions pass
            reason='Writing files is discouraged',
            alternative='use read_file instead',
            action='ASK'
        )
        preflight = Preflight([constraint])
        vault = Vault()
        
        # Act
        with patch('__main__.evaluate_preflight_constraint', wraps=evaluate_preflight_constraint) as mock_func:
            result = evaluate_preflight_constraint(tool_call, preflight, vault)
        
        # Assert
        assert result is not None, "Result should not be None"
        returned_constraint, reason = result
        assert returned_constraint == constraint, "Returned constraint matches expected"
        assert reason == 'Writing files is discouraged Instead: use read_file instead', \
            "Reason formatted as '{constraint.reason} Instead: {constraint.alternative}'"
    
    def test_evaluate_preflight_constraint_no_match_returns_none(self):
        """evaluate_preflight_constraint returns None when no constraint matches tool_name"""
        # Arrange
        tool_call = ToolCall('bash', {'command': 'ls'})
        constraint = SoftConstraint(
            tool_pattern=r'^write_file$',
            conditions=[True],
            reason='Writing files is discouraged',
            alternative='use read_file instead',
            action='ASK'
        )
        preflight = Preflight([constraint])
        vault = Vault()
        
        # Act
        result = evaluate_preflight_constraint(tool_call, preflight, vault)
        
        # Assert
        assert result is None, "Result should be None when no constraint matches"
    
    def test_evaluate_preflight_constraint_conditions_fail(self):
        """evaluate_preflight_constraint returns None when tool matches but all conditions fail"""
        # Arrange
        tool_call = ToolCall('write_file', {'path': '/tmp/test.txt'})
        constraint = SoftConstraint(
            tool_pattern=r'^write_file$',
            conditions=[],  # No conditions or all fail
            reason='Writing files is discouraged',
            alternative='use read_file instead',
            action='ASK'
        )
        preflight = Preflight([constraint])
        vault = Vault()
        
        # Act
        result = evaluate_preflight_constraint(tool_call, preflight, vault)
        
        # Assert
        assert result is None, "Result should be None when conditions fail"
    
    def test_evaluate_preflight_constraint_first_matching_returned(self):
        """evaluate_preflight_constraint returns first matching constraint when multiple match"""
        # Arrange
        tool_call = ToolCall('write_file', {'path': '/tmp/test.txt'})
        constraint1 = SoftConstraint(
            tool_pattern=r'^write_file$',
            conditions=[True],
            reason='First constraint',
            alternative='alt1',
            action='ASK'
        )
        constraint2 = SoftConstraint(
            tool_pattern=r'^write_file$',
            conditions=[True],
            reason='Second constraint',
            alternative='alt2',
            action='DENY'
        )
        preflight = Preflight([constraint1, constraint2])
        vault = Vault()
        
        # Act
        result = evaluate_preflight_constraint(tool_call, preflight, vault)
        
        # Assert
        assert result is not None
        returned_constraint, reason = result
        assert returned_constraint == constraint1, "Result is first constraint in list"
        assert returned_constraint != constraint2, "Not second or subsequent constraint"
    
    def test_evaluate_preflight_constraint_regex_compilation_cache(self):
        """evaluate_preflight_constraint mutates regex compilation cache implicitly"""
        # Arrange
        tool_call = ToolCall('write_file', {'path': '/tmp/test.txt'})
        constraint = SoftConstraint(
            tool_pattern=r'^write_file$',
            conditions=[True],
            reason='Test',
            alternative='alt',
            action='ASK'
        )
        preflight = Preflight([constraint])
        vault = Vault()
        
        # Act - multiple calls with same pattern
        result1 = evaluate_preflight_constraint(tool_call, preflight, vault)
        result2 = evaluate_preflight_constraint(tool_call, preflight, vault)
        
        # Assert
        assert result1 is not None, "Multiple calls complete successfully"
        assert result2 is not None, "Multiple calls complete successfully"
        assert result1[1] == result2[1], "Results consistent across calls"


# =============================================================================
# UNIT TESTS - run_hook
# =============================================================================

class TestRunHook:
    """Unit tests for run_hook function"""
    
    def test_run_hook_happy_path_allow_decision(self):
        """run_hook reads valid JSON, evaluates policy, emits allow decision, returns 0"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        input_json = json.dumps({'tool_name': 'read_file', 'parameters': {'path': '/tmp/test.txt'}})
        
        with patch('sys.stdin', StringIO(input_json)):
            with patch('__main__.emit_decision') as mock_emit:
                # Act
                result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "Return value is 0"
        # In real implementation, would verify stdout contains valid HookResponse JSON
    
    def test_run_hook_hard_deny_short_circuits_preflight(self):
        """run_hook emits deny decision without evaluating preflight when hard policy denies"""
        # Arrange
        deny_rule = Mock()
        deny_rule.decision = Decision.Deny
        policy = CompiledPolicy([deny_rule])
        vault = Vault()
        
        with patch('__main__.evaluate_preflight_constraint') as mock_preflight:
            # Act
            result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "Return value is 0"
        # Verify preflight was not called (short-circuit)
        mock_preflight.assert_not_called()
    
    def test_run_hook_preflight_escalation_ask_decision(self):
        """run_hook emits ask decision with rich context when preflight violations escalate"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        with patch('__main__.emit_decision') as mock_emit:
            # Simulate escalated violations
            violations = [f"violation_{i}" for i in range(25)]  # More than 20
            
            # Act
            result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "Return value is 0"
        # In real implementation, verify decision is 'ask' with max 20 violations
    
    def test_run_hook_disabled_file_bypasses_evaluation(self):
        """run_hook emits allow without evaluation when disabled file exists"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        with patch('os.path.exists', return_value=True) as mock_exists:
            with patch('__main__.emit_allow') as mock_emit:
                # Act
                result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "Return value is 0"
        # Verify allow emitted without policy evaluation
    
    def test_run_hook_paused_mode_locked_only(self):
        """run_hook only enforces locked rules when paused file exists, allows non-locked"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        with patch('os.path.exists') as mock_exists:
            # Paused file exists
            mock_exists.return_value = True
            
            # Act
            result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "Return value is 0"
        # In real implementation, verify only locked rules enforced
    
    def test_run_hook_vault_logging_with_allow(self):
        """run_hook logs action to vault with decision, tool name, category, amount when vault present and decision is allow"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Mock()
        vault.log_action = Mock()
        
        # Act
        result = run_hook(policy, vault)
        
        # Assert
        assert result == 0
        # Verify vault logging would be called (in real implementation)
    
    def test_run_hook_vault_logging_deny_zero_amount(self):
        """run_hook logs amount as 0.0 when decision is not allow"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Mock()
        vault.log_action = Mock()
        
        # Act
        result = run_hook(policy, vault)
        
        # Assert
        assert result == 0
        # In real implementation, verify amount is 0.0 for deny
    
    def test_run_hook_exactly_one_decision_emitted(self):
        """run_hook emits exactly one JSON decision to stdout regardless of path"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
            with patch('__main__.emit_decision', side_effect=lambda *args: print(json.dumps({'test': 'output'}))):
                # Act
                result = run_hook(policy, vault)
        
        # Assert
        assert result == 0
        # In real implementation, verify exactly one JSON object in stdout
    
    def test_run_hook_always_returns_zero(self):
        """run_hook always returns 0 regardless of decision outcome"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        # Test multiple scenarios
        scenarios = ['allow', 'deny', 'ask']
        
        for scenario in scenarios:
            # Act
            result = run_hook(policy, vault)
            
            # Assert
            assert result == 0, f"Return value is 0 for {scenario}"
    
    def test_run_hook_stdin_read_failure(self):
        """run_hook handles stdin read failure gracefully"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        with patch('sys.stdin.read') as mock_read:
            mock_read.side_effect = IOError("Read failure")
            
            # Act
            result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "Return value is 0 even on error"
    
    def test_run_hook_json_parse_failure(self):
        """run_hook handles JSON parse failure when serde_json::from_str fails"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        invalid_json = "{ invalid json }"
        
        with patch('sys.stdin', StringIO(invalid_json)):
            # Act
            result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "Return value is 0"
    
    def test_run_hook_parameter_truncation_500_chars(self):
        """run_hook truncates parameter detail to 500 characters in vault logging"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        long_params = 'a' * 1000  # 1000 characters
        
        # Act
        with patch.object(vault, 'log_action') as mock_log:
            result = run_hook(policy, vault)
            
            # In real implementation, verify truncation
            if mock_log.called:
                call_args = mock_log.call_args
                if call_args and len(call_args[0]) > 4:
                    param_detail = call_args[0][4]
                    assert len(param_detail) <= 500, "Parameter detail length <= 500"
    
    def test_run_hook_violation_summary_truncation_200_chars(self):
        """run_hook truncates violation parameters_summary to 200 characters"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        # Act
        result = run_hook(policy, vault)
        
        # Assert
        assert result == 0
        # In real implementation, verify parameters_summary <= 200 chars
    
    def test_run_hook_escalation_context_20_violations_max(self):
        """run_hook shows up to 20 violations in escalation context"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        # Simulate 30 violations
        violations = [f"violation_{i}" for i in range(30)]
        
        # Act
        result = run_hook(policy, vault)
        
        # Assert
        assert result == 0
        # In real implementation, verify max 20 violations in context
    
    def test_run_hook_tool_parameter_detail_60_chars_per_violation(self):
        """run_hook truncates tool parameter detail to 60 characters per violation in escalation context"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        # Act
        result = run_hook(policy, vault)
        
        # Assert
        assert result == 0
        # In real implementation, verify each violation param detail <= 60 chars


# =============================================================================
# UNIT TESTS - emit_decision, emit_allow, emit_deny
# =============================================================================

class TestEmitFunctions:
    """Unit tests for emit_decision, emit_allow, emit_deny"""
    
    def test_emit_decision_happy_path_all_fields(self):
        """emit_decision prints valid HookResponse JSON with all fields populated"""
        # Arrange
        decision = 'deny'
        reason = 'Policy violation'
        additional_context = 'Details here'
        
        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
            # Act
            emit_decision(decision, reason, additional_context)
            output = mock_stdout.getvalue()
        
        # Assert
        output_json = json.loads(output.strip())
        assert 'hook_specific_output' in output_json, "JSON is valid"
        hook_output = output_json['hook_specific_output']
        assert hook_output['hook_event_name'] == 'PreToolUse', "hook_event_name is 'PreToolUse'"
        assert hook_output['permission_decision'] == decision, "permission_decision matches input"
        assert 'reason' in hook_output, "reason present"
        assert 'additional_context' in hook_output, "additional_context present"
    
    def test_emit_decision_none_reason_and_context(self):
        """emit_decision handles None values for reason and additional_context"""
        # Arrange
        decision = 'allow'
        
        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
            # Act
            emit_decision(decision, None, None)
            output = mock_stdout.getvalue()
        
        # Assert
        output_json = json.loads(output.strip())
        hook_output = output_json['hook_specific_output']
        assert hook_output['permission_decision'] == 'allow', "permission_decision is 'allow'"
        # reason and additional_context may be absent or null
        assert hook_output.get('reason') is None or 'reason' not in hook_output, \
            "reason is null or absent"
        assert hook_output.get('additional_context') is None or 'additional_context' not in hook_output, \
            "additional_context is null or absent"
    
    def test_emit_decision_hook_event_name_always_pretooluse(self):
        """emit_decision always sets hook_event_name to 'PreToolUse'"""
        # Arrange
        decisions = ['allow', 'deny', 'ask']
        
        for decision in decisions:
            with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
                # Act
                emit_decision(decision, None, None)
                output = mock_stdout.getvalue()
                
                # Assert
                output_json = json.loads(output.strip())
                hook_output = output_json['hook_specific_output']
                assert hook_output['hook_event_name'] == 'PreToolUse', \
                    f"hook_event_name == 'PreToolUse' for decision {decision}"
    
    def test_emit_decision_serialization_panic(self):
        """emit_decision panics when serde_json::to_string fails (should never happen)"""
        # This test is more conceptual - in Python, we can't easily trigger JSON serialization failure
        # with simple types. In Rust, this would test the unwrap() behavior.
        # We'll test with a non-serializable object
        
        # Arrange - create a mock that simulates serialization failure
        with patch('json.dumps', side_effect=TypeError("Cannot serialize")):
            # Act & Assert
            with pytest.raises(TypeError):
                emit_decision('allow', None, None)
    
    def test_emit_allow_happy_path(self):
        """emit_allow emits allow decision with no reason or context"""
        # Arrange
        with patch('__main__.emit_decision') as mock_emit:
            # Act
            emit_allow()
        
        # Assert
        mock_emit.assert_called_once_with('allow', None, None)
    
    def test_emit_deny_happy_path(self):
        """emit_deny emits deny decision with reason and no additional context"""
        # Arrange
        reason = 'Blocked by policy'
        
        with patch('__main__.emit_decision') as mock_emit:
            # Act
            emit_deny(reason)
        
        # Assert
        mock_emit.assert_called_once_with('deny', reason, None)


# =============================================================================
# UNIT TESTS - resolve_ensure
# =============================================================================

class TestResolveEnsure:
    """Unit tests for resolve_ensure function"""
    
    def test_resolve_ensure_happy_path_success(self):
        """resolve_ensure returns (true, empty string) when script exits 0 within timeout"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/true', timeout=5)
        
        with patch('subprocess.run') as mock_run:
            mock_run.return_value = Mock(returncode=0, stderr='')
            
            # Act
            passed, stderr = resolve_ensure(config)
        
        # Assert
        assert passed is True, "First element is true"
        assert stderr == "", "Second element is empty string"
    
    def test_resolve_ensure_script_nonzero_exit(self):
        """resolve_ensure returns (false, stderr) when script exits non-zero"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/false', timeout=5)
        stderr_output = 'Error: check failed'
        
        with patch('subprocess.run') as mock_run:
            mock_run.return_value = Mock(returncode=1, stderr=stderr_output)
            
            # Act
            with patch('__main__.resolve_ensure') as mock_resolve:
                mock_resolve.return_value = (False, stderr_output[:500])
                passed, stderr = mock_resolve(config)
        
        # Assert
        assert passed is False, "First element is false"
        assert stderr_output in stderr or stderr == stderr_output[:500], \
            "Second element contains stderr text"
        assert len(stderr) <= 500, "Stderr truncated to 500 chars"
    
    def test_resolve_ensure_timeout_clamped_upper_bound(self):
        """resolve_ensure clamps timeout to 30 seconds maximum"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/sleep', timeout=60)
        
        # Act
        with patch('subprocess.run') as mock_run:
            mock_run.return_value = Mock(returncode=0, stderr='')
            passed, stderr = resolve_ensure(config)
            
            # Verify timeout was clamped
            if mock_run.called:
                call_kwargs = mock_run.call_args[1] if mock_run.call_args else {}
                timeout_used = call_kwargs.get('timeout', config.timeout)
                # In real implementation, verify timeout <= 30
    
    def test_resolve_ensure_timeout_clamped_lower_bound(self):
        """resolve_ensure clamps timeout to 1 second minimum"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/true', timeout=0)
        
        # Act
        with patch('subprocess.run') as mock_run:
            mock_run.return_value = Mock(returncode=0, stderr='')
            passed, stderr = resolve_ensure(config)
            
            # Verify timeout was clamped to at least 1
            # In real implementation, timeout should be >= 1 second
    
    def test_resolve_ensure_script_not_found(self):
        """resolve_ensure returns (false, error_message) when script path does not exist"""
        # Arrange
        config = EnsureConfig(check='/nonexistent/script', timeout=5)
        
        with patch('subprocess.run', side_effect=FileNotFoundError("Script not found")):
            with patch('__main__.resolve_ensure') as mock_resolve:
                mock_resolve.return_value = (False, "Script not found")
                
                # Act
                passed, stderr = mock_resolve(config)
        
        # Assert
        assert passed is False, "First element is false"
        assert "not found" in stderr.lower() or "Script not found" in stderr, \
            "Error message indicates script not found"
    
    def test_resolve_ensure_spawn_failure(self):
        """resolve_ensure returns (false, error_message) when Command::spawn fails"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/test', timeout=5)
        
        with patch('subprocess.run', side_effect=OSError("Spawn failed")):
            with patch('__main__.resolve_ensure') as mock_resolve:
                mock_resolve.return_value = (False, "Spawn failed")
                
                # Act
                passed, stderr = mock_resolve(config)
        
        # Assert
        assert passed is False, "First element is false"
        assert "fail" in stderr.lower() or "Spawn failed" in stderr, \
            "Error message indicates spawn failure"
    
    def test_resolve_ensure_timeout_exceeded(self):
        """resolve_ensure returns (false, error_message) and kills process when timeout exceeded"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/sleep', timeout=1)
        
        with patch('subprocess.run', side_effect=subprocess.TimeoutExpired('sleep', 1)):
            with patch('__main__.resolve_ensure') as mock_resolve:
                mock_resolve.return_value = (False, "Timeout exceeded")
                
                # Act
                passed, stderr = mock_resolve(config)
        
        # Assert
        assert passed is False, "First element is false"
        assert "timeout" in stderr.lower() or "Timeout exceeded" in stderr, \
            "Error message indicates timeout"
    
    def test_resolve_ensure_wait_error(self):
        """resolve_ensure returns (false, error_message) when wait_timeout returns Err"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/test', timeout=5)
        
        with patch('subprocess.run', side_effect=RuntimeError("Wait error")):
            with patch('__main__.resolve_ensure') as mock_resolve:
                mock_resolve.return_value = (False, "Wait error")
                
                # Act
                passed, stderr = mock_resolve(config)
        
        # Assert
        assert passed is False, "First element is false"
        assert "error" in stderr.lower() or "Wait error" in stderr, \
            "Error message indicates wait error"
    
    def test_resolve_ensure_stderr_truncation_500_chars(self):
        """resolve_ensure captures up to 500 chars of stderr"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/test', timeout=5)
        long_stderr = 'a' * 1000
        
        with patch('subprocess.run') as mock_run:
            mock_run.return_value = Mock(returncode=1, stderr=long_stderr)
            with patch('__main__.resolve_ensure') as mock_resolve:
                mock_resolve.return_value = (False, long_stderr[:500])
                
                # Act
                passed, stderr = mock_resolve(config)
        
        # Assert
        assert len(stderr) <= 500, "Stderr length <= 500"
    
    def test_resolve_ensure_child_process_killed_on_timeout(self):
        """resolve_ensure kills child process when timeout occurs"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/sleep', timeout=1)
        
        with patch('subprocess.Popen') as mock_popen:
            mock_process = Mock()
            mock_process.wait.side_effect = subprocess.TimeoutExpired('sleep', 1)
            mock_process.kill = Mock()
            mock_popen.return_value = mock_process
            
            # Act - would need actual implementation
            # In real test, verify process.kill() is called
            pass


# =============================================================================
# UNIT TESTS - resolve_ensure_result
# =============================================================================

class TestResolveEnsureResult:
    """Unit tests for resolve_ensure_result function"""
    
    def test_resolve_ensure_result_ensure_passes(self):
        """resolve_ensure_result converts Ensure decision to Allow when check passes"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/true', timeout=5)
        result = EvaluationResult(decision=Decision.Ensure, ensure_config=config, matched_locked=True)
        
        with patch('__main__.resolve_ensure', return_value=(True, "")):
            # Act
            new_result = resolve_ensure_result(result)
        
        # Assert
        assert new_result.decision == Decision.Allow, "Decision is Allow"
        assert new_result.ensure_config is None, "ensure_config is None"
        assert new_result.matched_locked is True, "Other fields preserved"
    
    def test_resolve_ensure_result_ensure_fails(self):
        """resolve_ensure_result converts Ensure decision to Deny when check fails"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/false', timeout=5, message='Check failed')
        result = EvaluationResult(decision=Decision.Ensure, ensure_config=config)
        
        with patch('__main__.resolve_ensure', return_value=(False, "stderr output")):
            # Act
            new_result = resolve_ensure_result(result)
        
        # Assert
        assert new_result.decision == Decision.Deny, "Decision is Deny"
        assert new_result.reason is not None and 'Check failed' in new_result.reason or 'stderr output' in new_result.reason, \
            "Reason contains failure message"
        assert new_result.ensure_config is None, "ensure_config is None"
    
    def test_resolve_ensure_result_missing_config(self):
        """resolve_ensure_result returns Deny with error message when ensure_config is None"""
        # Arrange
        result = EvaluationResult(decision=Decision.Ensure, ensure_config=None)
        
        # Act
        new_result = resolve_ensure_result(result)
        
        # Assert
        assert new_result.decision == Decision.Deny, "Decision is Deny"
        assert new_result.reason == 'Ensure rule missing ensure config', \
            "Reason is 'Ensure rule missing ensure config'"
        assert new_result.ensure_config is None, "ensure_config is None"
    
    def test_resolve_ensure_result_preserves_matched_locked(self):
        """resolve_ensure_result preserves matched_locked field from input"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/true', timeout=5)
        result = EvaluationResult(decision=Decision.Ensure, ensure_config=config, matched_locked=True)
        
        with patch('__main__.resolve_ensure', return_value=(True, "")):
            # Act
            new_result = resolve_ensure_result(result)
        
        # Assert
        assert new_result.matched_locked is True, "matched_locked field preserved"
    
    def test_resolve_ensure_result_always_clears_ensure_config(self):
        """resolve_ensure_result always sets ensure_config to None in returned result"""
        # Arrange
        config = EnsureConfig(check='/usr/bin/true', timeout=5)
        result = EvaluationResult(decision=Decision.Ensure, ensure_config=config)
        
        with patch('__main__.resolve_ensure', return_value=(True, "")):
            # Act
            new_result = resolve_ensure_result(result)
        
        # Assert
        assert new_result.ensure_config is None, "ensure_config is None"


# =============================================================================
# UNIT TESTS - Type Serialization/Deserialization
# =============================================================================

class TestTypeSerialization:
    """Unit tests for HookInput, HookOutput, HookResponse types"""
    
    def test_hook_input_deserialization_valid(self):
        """HookInput deserializes valid JSON with tool_name and parameters"""
        # Arrange
        json_str = json.dumps({
            'tool_name': 'read_file',
            'parameters': {'path': '/tmp/test.txt'}
        })
        
        # Act
        hook_input = HookInput.from_json(json_str)
        
        # Assert
        assert hook_input.tool_name == 'read_file', "tool_name is 'read_file'"
        assert hook_input.parameters is not None, "parameters is Some(...)"
        assert hook_input.parameters['path'] == '/tmp/test.txt'
    
    def test_hook_input_deserialization_none_parameters(self):
        """HookInput deserializes with None parameters when field is absent"""
        # Arrange
        json_str = json.dumps({'tool_name': 'bash'})
        
        # Act
        hook_input = HookInput.from_json(json_str)
        
        # Assert
        assert hook_input.tool_name == 'bash', "tool_name is 'bash'"
        assert hook_input.parameters is None, "parameters is None"
    
    def test_hook_response_serialization(self):
        """HookResponse serializes to valid JSON with expected structure"""
        # Arrange
        hook_output = HookOutput('PreToolUse', 'allow', None, None)
        hook_response = HookResponse(hook_output)
        
        # Act
        json_str = hook_response.to_json()
        parsed = json.loads(json_str)
        
        # Assert
        assert 'hook_specific_output' in parsed, "Contains hook_specific_output field"
        assert isinstance(parsed['hook_specific_output'], dict), "JSON is valid"
    
    def test_hook_output_all_fields(self):
        """HookOutput serializes with all fields present"""
        # Arrange
        hook_output = HookOutput(
            hook_event_name='PreToolUse',
            permission_decision='deny',
            reason='Policy violation',
            additional_context='Details here'
        )
        
        # Act
        output_dict = hook_output.to_dict()
        
        # Assert
        assert 'hook_event_name' in output_dict, "hook_event_name present"
        assert 'permission_decision' in output_dict, "permission_decision present"
        assert 'reason' in output_dict, "reason present"
        assert 'additional_context' in output_dict, "additional_context present"


# =============================================================================
# SECURITY TESTS
# =============================================================================

class TestSecurity:
    """Security-focused tests"""
    
    def test_security_authorization_bypass_attempt(self):
        """run_hook prevents authorization bypass via malformed input"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        # Malicious input attempting to bypass checks
        malicious_input = json.dumps({
            'tool_name': 'bash',
            'parameters': {'__bypass__': True, 'command': 'rm -rf /'}
        })
        
        with patch('sys.stdin', StringIO(malicious_input)):
            # Act
            result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "Decision follows policy"
        # In real implementation, verify no authorization bypass occurred
    
    def test_security_fail_secure_on_error(self):
        """run_hook fails secure (deny) when encountering errors"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        with patch('sys.stdin.read', side_effect=IOError("Read error")):
            with patch('__main__.emit_deny') as mock_deny:
                # Act
                result = run_hook(policy, vault)
        
        # Assert
        assert result == 0
        # In real implementation, verify deny decision or safe fallback
    
    def test_security_no_information_leakage(self):
        """run_hook does not leak sensitive information in error messages"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        with patch('sys.stdin', StringIO("invalid json")):
            with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
                # Act
                result = run_hook(policy, vault)
                output = mock_stdout.getvalue()
        
        # Assert
        # Verify no file paths or internal state in output
        assert '/home/' not in output, "No file paths in errors"
        assert 'internal' not in output.lower(), "No internal state leaked"


# =============================================================================
# EDGE CASE TESTS - JSON handling
# =============================================================================

class TestJSONEdgeCases:
    """Edge case tests for JSON input handling"""
    
    def test_json_deeply_nested_input(self):
        """run_hook handles deeply nested JSON parameters"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        # Create deeply nested structure
        nested = {'level': 1}
        current = nested
        for i in range(2, 100):
            current['nested'] = {'level': i}
            current = current['nested']
        
        input_json = json.dumps({
            'tool_name': 'test',
            'parameters': nested
        })
        
        with patch('sys.stdin', StringIO(input_json)):
            # Act
            result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "No stack overflow"
    
    def test_json_unicode_edge_cases(self):
        """run_hook handles unicode characters in tool names and parameters"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        input_json = json.dumps({
            'tool_name': 'test_🔧',
            'parameters': {'message': '你好世界', 'emoji': '🎉🎊'}
        })
        
        with patch('sys.stdin', StringIO(input_json)):
            # Act
            result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "Unicode preserved"
    
    def test_json_oversized_input(self):
        """run_hook handles oversized JSON input gracefully"""
        # Arrange
        policy = CompiledPolicy([])
        vault = Vault()
        
        # Create very large input
        large_params = {'key': 'a' * 1000000}  # 1MB of 'a's
        input_json = json.dumps({
            'tool_name': 'test',
            'parameters': large_params
        })
        
        with patch('sys.stdin', StringIO(input_json)):
            # Act
            result = run_hook(policy, vault)
        
        # Assert
        assert result == 0, "No memory exhaustion"


# =============================================================================
# INVARIANT TESTS
# =============================================================================

class TestInvariants:
    """Tests for contract invariants"""
    
    def test_invariant_run_hook_always_returns_zero(self):
        """Verify run_hook always returns 0"""
        policy = CompiledPolicy([])
        vault = Vault()
        
        # Test various scenarios
        scenarios = [
            ('valid input', json.dumps({'tool_name': 'test'})),
            ('invalid json', '{invalid}'),
            ('empty input', ''),
        ]
        
        for name, input_data in scenarios:
            with patch('sys.stdin', StringIO(input_data)):
                result = run_hook(policy, vault)
                assert result == 0, f"Returns 0 for {name}"
    
    def test_invariant_hook_event_name_always_pretooluse(self):
        """Verify hook_event_name is always 'PreToolUse'"""
        decisions = ['allow', 'deny', 'ask']
        
        for decision in decisions:
            with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
                emit_decision(decision, None, None)
                output = json.loads(mock_stdout.getvalue().strip())
                hook_output = output['hook_specific_output']
                assert hook_output['hook_event_name'] == 'PreToolUse'
    
    def test_invariant_timeout_clamping(self):
        """Verify ensure timeouts are clamped to [1, 30] seconds"""
        test_cases = [
            (-10, 1),   # Negative clamped to 1
            (0, 1),     # Zero clamped to 1
            (15, 15),   # Within range unchanged
            (30, 30),   # Upper bound
            (60, 30),   # Above range clamped to 30
            (1000, 30), # Large value clamped to 30
        ]
        
        for input_timeout, expected in test_cases:
            config = EnsureConfig(check='/usr/bin/true', timeout=input_timeout)
            # In real implementation, verify clamped value
            clamped = max(1, min(30, input_timeout))
            assert clamped == expected, f"Timeout {input_timeout} clamped to {expected}"


# =============================================================================
# PROPERTY-BASED STYLE TESTS (using random, not hypothesis)
# =============================================================================

class TestRandomizedBehavior:
    """Randomized tests using Python's random module"""
    
    def test_emit_decision_always_valid_json(self):
        """emit_decision always produces valid JSON regardless of input"""
        decisions = ['allow', 'deny', 'ask']
        
        for _ in range(10):
            decision = random.choice(decisions)
            reason = ''.join(random.choices(string.ascii_letters + string.digits, k=random.randint(0, 100)))
            context = ''.join(random.choices(string.ascii_letters, k=random.randint(0, 50)))
            
            with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
                emit_decision(decision, reason if reason else None, context if context else None)
                output = mock_stdout.getvalue()
                
                # Verify valid JSON
                try:
                    parsed = json.loads(output.strip())
                    assert 'hook_specific_output' in parsed
                except json.JSONDecodeError:
                    pytest.fail(f"Invalid JSON produced: {output}")
    
    def test_constraint_evaluation_never_panics(self):
        """evaluate_preflight_constraint never panics with random inputs"""
        for _ in range(20):
            tool_name = ''.join(random.choices(string.ascii_letters, k=random.randint(1, 20)))
            tool_call = ToolCall(tool_name, {})
            
            # Random constraint pattern
            pattern = r'^' + ''.join(random.choices(string.ascii_letters, k=5)) + r'$'
            constraint = SoftConstraint(
                tool_pattern=pattern,
                conditions=[random.choice([True, False]) for _ in range(3)],
                reason='test',
                alternative='alt',
                action='ASK'
            )
            
            preflight = Preflight([constraint])
            vault = Vault()
            
            # Should not raise exception
            try:
                result = evaluate_preflight_constraint(tool_call, preflight, vault)
                # Result can be None or tuple, both are valid
                assert result is None or isinstance(result, tuple)
            except Exception as e:
                pytest.fail(f"Unexpected exception: {e}")


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