"""
Test examples for django-hooks.

Shows how to test hooks in your Django application.
"""

import pytest
from decimal import Decimal
from dataclasses import dataclass
from typing import Any, Dict
from django_hooks import (
    HookContext,
    HookType,
    HookRejectionError,
    HookManager,
    HookRegistry,
    register_hook,
    unregister_hook,
    clear_hooks,
)


# Example context for tests
@dataclass(frozen=True)
class TestHookContext(HookContext):
    """Test context with sample fields."""
    user_id: int
    amount: Decimal


# ============================================================================
# Fixtures
# ============================================================================


@pytest.fixture(autouse=True)
def reset_hooks():
    """Clear hooks before and after each test."""
    clear_hooks()
    yield
    clear_hooks()


@pytest.fixture
def hook_manager():
    """Create a fresh hook manager for each test."""
    registry = HookRegistry(operations=['test_operation'])
    return HookManager(registry)


@pytest.fixture
def sample_context():
    """Create a sample hook context."""
    return TestHookContext(
        operation='test_operation',
        user_id=123,
        amount=Decimal('100.00')
    )


# ============================================================================
# Test: Hook Registration
# ============================================================================


def test_register_hook_success():
    """Test successful hook registration."""
    def my_hook(context):
        return True
    
    register_hook('test_hook', HookType.PRE, my_hook, operation='test_operation')
    
    # Verify hook is registered
    from django_hooks import get_global_registry
    registry = get_global_registry()
    hooks = registry.get_hooks('test_operation', HookType.PRE)
    
    assert len(hooks) == 1
    assert hooks[0].name == 'test_hook'


def test_register_duplicate_hook_raises_error():
    """Test that registering duplicate hook name raises error."""
    def hook1(context):
        return True
    
    def hook2(context):
        return True
    
    register_hook('duplicate', HookType.PRE, hook1)
    
    with pytest.raises(ValueError, match="already registered"):
        register_hook('duplicate', HookType.PRE, hook2)


def test_register_invalid_operation_raises_error(hook_manager):
    """Test that invalid operation name raises error."""
    def my_hook(context):
        return True
    
    with pytest.raises(ValueError, match="Invalid operation"):
        hook_manager.registry.register(
            'test', HookType.PRE, my_hook, operation='invalid_op'
        )


# ============================================================================
# Test: Hook Execution Order (Priority)
# ============================================================================


def test_hooks_execute_in_priority_order(hook_manager, sample_context):
    """Test that hooks execute in priority order."""
    execution_order = []
    
    def make_hook(name):
        def hook(ctx):
            execution_order.append(name)
            return True
        return hook
    
    # Register hooks with different priorities
    hook_manager.registry.register('low', HookType.PRE, make_hook('low'), 
                                   operation='test_operation', priority=100)
    hook_manager.registry.register('high', HookType.PRE, make_hook('high'), 
                                   operation='test_operation', priority=10)
    hook_manager.registry.register('medium', HookType.PRE, make_hook('medium'), 
                                   operation='test_operation', priority=50)
    
    # Execute
    hook_manager.execute_pre_hooks(sample_context)
    
    # Verify order
    assert execution_order == ['high', 'medium', 'low']


def test_hooks_with_same_priority_execute_alphabetically(hook_manager, sample_context):
    """Test that hooks with same priority execute in name order."""
    execution_order = []
    
    def make_hook(name):
        def hook(ctx):
            execution_order.append(name)
            return True
        return hook
    
    # Register with same priority, different names
    hook_manager.registry.register('charlie', HookType.PRE, make_hook('charlie'), 
                                   operation='test_operation', priority=50)
    hook_manager.registry.register('alice', HookType.PRE, make_hook('alice'), 
                                   operation='test_operation', priority=50)
    hook_manager.registry.register('bob', HookType.PRE, make_hook('bob'), 
                                   operation='test_operation', priority=50)
    
    hook_manager.execute_pre_hooks(sample_context)
    
    assert execution_order == ['alice', 'bob', 'charlie']


# ============================================================================
# Test: PRE Hook Rejection
# ============================================================================


def test_pre_hook_can_reject_with_false(hook_manager, sample_context):
    """Test that PRE hook can reject by returning False."""
    def rejecting_hook(context):
        return False
    
    hook_manager.registry.register('reject', HookType.PRE, rejecting_hook, 
                                   operation='test_operation')
    
    with pytest.raises(HookRejectionError) as exc:
        hook_manager.execute_pre_hooks(sample_context)
    
    assert 'HOOK_REJECTED_REJECT' in exc.value.error_code


def test_pre_hook_can_reject_with_exception(hook_manager, sample_context):
    """Test that PRE hook can reject by raising HookRejectionError."""
    def rejecting_hook(context):
        raise HookRejectionError(
            error_code='CUSTOM_ERROR',
            message='Custom rejection message',
            details={'reason': 'test'}
        )
    
    hook_manager.registry.register('reject', HookType.PRE, rejecting_hook, 
                                   operation='test_operation')
    
    with pytest.raises(HookRejectionError) as exc:
        hook_manager.execute_pre_hooks(sample_context)
    
    assert exc.value.error_code == 'CUSTOM_ERROR'
    assert exc.value.message == 'Custom rejection message'
    assert exc.value.details == {'reason': 'test'}


def test_pre_hook_rejection_stops_execution(hook_manager, sample_context):
    """Test that rejection stops subsequent hooks from executing."""
    execution_log = []
    
    def hook1(ctx):
        execution_log.append('hook1')
        return True
    
    def hook2(ctx):
        execution_log.append('hook2')
        raise HookRejectionError('REJECTED', 'Rejected')
    
    def hook3(ctx):
        execution_log.append('hook3')
        return True
    
    hook_manager.registry.register('h1', HookType.PRE, hook1, 
                                   operation='test_operation', priority=10)
    hook_manager.registry.register('h2', HookType.PRE, hook2, 
                                   operation='test_operation', priority=20)
    hook_manager.registry.register('h3', HookType.PRE, hook3, 
                                   operation='test_operation', priority=30)
    
    with pytest.raises(HookRejectionError):
        hook_manager.execute_pre_hooks(sample_context)
    
    # Only hook1 and hook2 should have executed
    assert execution_log == ['hook1', 'hook2']


# ============================================================================
# Test: POST Hook Error Handling
# ============================================================================


def test_post_hook_errors_dont_stop_execution(hook_manager, sample_context):
    """Test that POST hook errors don't stop other hooks."""
    execution_log = []
    
    def hook1(ctx):
        execution_log.append('hook1')
    
    def hook2(ctx):
        execution_log.append('hook2')
        raise Exception('Hook 2 failed')
    
    def hook3(ctx):
        execution_log.append('hook3')
    
    hook_manager.registry.register('h1', HookType.POST, hook1, 
                                   operation='test_operation', priority=10)
    hook_manager.registry.register('h2', HookType.POST, hook2, 
                                   operation='test_operation', priority=20)
    hook_manager.registry.register('h3', HookType.POST, hook3, 
                                   operation='test_operation', priority=30)
    
    errors = hook_manager.execute_post_hooks(sample_context)
    
    # All hooks should execute
    assert execution_log == ['hook1', 'hook2', 'hook3']
    
    # One error should be captured
    assert len(errors) == 1
    assert errors[0].hook_name == 'h2'
    assert errors[0].error_code == 'HOOK_EXECUTION_ERROR'


def test_post_hook_rejection_captured_as_error(hook_manager, sample_context):
    """Test that POST hook trying to reject is captured as error."""
    def rejecting_post_hook(context):
        raise HookRejectionError('SHOULD_NOT_REJECT', 'POST hooks cannot reject')
    
    hook_manager.registry.register('reject', HookType.POST, rejecting_post_hook, 
                                   operation='test_operation')
    
    errors = hook_manager.execute_post_hooks(sample_context)
    
    # Error should be captured, not raised
    assert len(errors) == 1
    assert errors[0].error_code == 'SHOULD_NOT_REJECT'


# ============================================================================
# Test: Metadata Communication
# ============================================================================


def test_hooks_can_share_data_via_metadata(hook_manager, sample_context):
    """Test that hooks can communicate via metadata."""
    def hook1(context):
        context.metadata['shared_data'] = 'value_from_hook1'
        return True
    
    def hook2(context):
        # Read data from hook1
        assert context.metadata['shared_data'] == 'value_from_hook1'
        context.metadata['hook2_executed'] = True
        return True
    
    hook_manager.registry.register('h1', HookType.PRE, hook1, 
                                   operation='test_operation', priority=10)
    hook_manager.registry.register('h2', HookType.PRE, hook2, 
                                   operation='test_operation', priority=20)
    
    hook_manager.execute_pre_hooks(sample_context)
    
    # Verify metadata was shared
    assert sample_context.metadata['shared_data'] == 'value_from_hook1'
    assert sample_context.metadata['hook2_executed'] is True


# ============================================================================
# Test: Global vs Operation-Specific Hooks
# ============================================================================


def test_global_hooks_execute_for_all_operations(hook_manager):
    """Test that global hooks (*) execute for all operations."""
    execution_log = []
    
    def global_hook(ctx):
        execution_log.append(ctx.operation)
    
    hook_manager.registry.register('global', HookType.POST, global_hook, 
                                   operation='*')
    
    # Execute for different operations
    ctx1 = TestHookContext(operation='test_operation', user_id=1, amount=Decimal('10'))
    hook_manager.execute_post_hooks(ctx1)
    
    assert 'test_operation' in execution_log


def test_operation_specific_hooks_only_execute_for_that_operation(hook_manager):
    """Test that operation-specific hooks don't run for other operations."""
    execution_log = []
    
    def specific_hook(ctx):
        execution_log.append('executed')
    
    # Register for 'test_operation' only
    hook_manager.registry.register('specific', HookType.PRE, specific_hook, 
                                   operation='test_operation')
    
    # Execute for 'test_operation'
    ctx1 = TestHookContext(operation='test_operation', user_id=1, amount=Decimal('10'))
    hook_manager.execute_pre_hooks(ctx1)
    assert len(execution_log) == 1
    
    # Execute for different operation (if registry supports it)
    # Should not execute
    registry = HookRegistry(operations=['test_operation', 'other_operation'])
    manager = HookManager(registry)
    registry.register('specific', HookType.PRE, specific_hook, 
                     operation='test_operation')
    
    ctx2 = TestHookContext(operation='other_operation', user_id=1, amount=Decimal('10'))
    manager.execute_pre_hooks(ctx2)
    assert len(execution_log) == 1  # Still 1, not executed for other_operation


# ============================================================================
# Test: Hook Unregistration
# ============================================================================


def test_unregister_hook():
    """Test unregistering a hook."""
    def my_hook(context):
        return True
    
    register_hook('test', HookType.PRE, my_hook)
    
    # Verify registered
    from django_hooks import get_global_registry
    hooks = get_global_registry().get_hooks('*', HookType.PRE)
    assert len(hooks) == 1
    
    # Unregister
    result = unregister_hook('test')
    assert result is True
    
    # Verify removed
    hooks = get_global_registry().get_hooks('*', HookType.PRE)
    assert len(hooks) == 0


def test_unregister_nonexistent_hook_returns_false():
    """Test that unregistering non-existent hook returns False."""
    result = unregister_hook('nonexistent')
    assert result is False


# ============================================================================
# Test: Integration Example
# ============================================================================


def test_complete_workflow_with_hooks():
    """Test complete workflow: register, execute PRE, operation, execute POST."""
    clear_hooks()
    
    # Track execution
    log = []
    
    # PRE hook: Validation
    def validate_amount(context: TestHookContext):
        log.append('validate')
        if context.amount <= 0:
            raise HookRejectionError(
                'INVALID_AMOUNT',
                'Amount must be positive',
                {'amount': float(context.amount)}
            )
        return True
    
    # POST hook: Logging
    def log_transaction(context: TestHookContext):
        log.append('log')
        # Simulate logging
        print(f"Transaction: user={context.user_id}, amount={context.amount}")
    
    # Register hooks
    register_hook('validate', HookType.PRE, validate_amount, priority=10)
    register_hook('log', HookType.POST, log_transaction, priority=100)
    
    # Create manager
    from django_hooks import get_global_registry
    manager = HookManager(get_global_registry())
    
    # Execute for valid amount
    context = TestHookContext(operation='test', user_id=123, amount=Decimal('100'))
    
    # PRE hooks
    manager.execute_pre_hooks(context)
    assert 'validate' in log
    
    # Simulate operation
    result = {'success': True, 'transaction_id': '12345'}
    
    # POST hooks
    errors = manager.execute_post_hooks(context)
    assert 'log' in log
    assert len(errors) == 0
    
    # Test rejection case
    log.clear()
    invalid_context = TestHookContext(operation='test', user_id=123, amount=Decimal('-10'))
    
    with pytest.raises(HookRejectionError) as exc:
        manager.execute_pre_hooks(invalid_context)
    
    assert exc.value.error_code == 'INVALID_AMOUNT'
    assert 'validate' in log
    assert 'log' not in log  # POST hook shouldn't run if PRE hook rejects


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