"""
Tests for LangGraph Auto-Patching (Zero-Config Agent Tracing)

TDD Red Phase - These tests define expected behavior.
All tests should FAIL initially because stubs return wrong values.

Test Categories:
- Golden Path: Core functionality that must work
- Edge Case: Boundary conditions and special cases
- Failure Mode: Graceful degradation when things go wrong
- Integration: Full flow with nested agents

Following 32-testing-tdd-quality.mdc guidelines:
- Minimal mocking (external boundaries only)
- Assert on outcomes, not implementation details
- Business-focused test descriptions
"""

import pytest
from unittest.mock import MagicMock, AsyncMock, patch, PropertyMock
import asyncio


# =============================================================================
# GOLDEN PATH TESTS
# =============================================================================

class TestTraceNameInference:
    """Test automatic trace name inference from state class names"""
    
    @pytest.mark.unit
    def test_infer_trace_name_from_camelcase_state(self):
        """
        Business: When developers use standard naming like ProductivityAgentState,
        the SDK should automatically create human-readable trace names without
        manual configuration.
        
        Red phase: Stub returns empty string, assertion will fail.
        """
        from swisper_studio_sdk.tracing.langgraph_patch import _infer_trace_name
        
        # Create mock class with standard naming
        class ProductivityAgentState:
            pass
        
        result = _infer_trace_name(ProductivityAgentState)
        
        # Should convert CamelCase to snake_case and strip "State"
        assert result == "productivity_agent", f"Expected 'productivity_agent', got '{result}'"
    
    @pytest.mark.unit
    def test_infer_trace_name_global_supervisor(self):
        """Test inference for GlobalSupervisorState"""
        from swisper_studio_sdk.tracing.langgraph_patch import _infer_trace_name
        
        class GlobalSupervisorState:
            pass
        
        result = _infer_trace_name(GlobalSupervisorState)
        assert result == "global_supervisor"
    
    @pytest.mark.unit
    def test_infer_trace_name_research_agent(self):
        """Test inference for ResearchAgentState"""
        from swisper_studio_sdk.tracing.langgraph_patch import _infer_trace_name
        
        class ResearchAgentState:
            pass
        
        result = _infer_trace_name(ResearchAgentState)
        assert result == "research_agent"


class TestTraceNameSuffix:
    """Test that 'State' suffix is properly stripped"""
    
    @pytest.mark.unit
    def test_infer_trace_name_strips_state_suffix(self):
        """
        Business: Trace names in UI should be clean (e.g., "my_agent")
        not cluttered with technical suffixes like "my_agent_state".
        
        Red phase: Stub returns empty string.
        """
        from swisper_studio_sdk.tracing.langgraph_patch import _infer_trace_name
        
        class MyAgentState:
            pass
        
        result = _infer_trace_name(MyAgentState)
        
        # Should NOT include "_state" suffix
        assert result == "my_agent", f"Expected 'my_agent', got '{result}'"
        assert not result.endswith("_state"), "Should strip 'State' suffix"


class TestAddNodeWrapping:
    """Test that add_node automatically wraps functions with tracing"""
    
    @pytest.mark.unit
    def test_add_node_wraps_functions_with_traced_decorator(self):
        """
        Business: When developers add nodes to a graph, they should be
        automatically traced without any decorator annotations.
        This is the core zero-config feature.
        
        Red phase: patch_langgraph returns False, no wrapping occurs.
        """
        from swisper_studio_sdk.tracing.langgraph_patch import patch_langgraph, unpatch_langgraph
        
        # For Red phase, we just verify patch_langgraph returns True
        # The actual node wrapping will be tested in Green phase with full integration
        
        # Apply patches
        result = patch_langgraph()
        
        # Verify patching succeeded (stub returns False, so this fails)
        assert result is True, "patch_langgraph should return True on success"
        
        # Clean up
        unpatch_langgraph()


class TestTopLevelTraceCreation:
    """Test that top-level graph invocation creates new traces"""
    
    @pytest.mark.unit
    @pytest.mark.asyncio
    async def test_top_level_graph_creates_new_trace(self):
        """
        Business: When an agent is invoked directly (not from another agent),
        the SDK should automatically create a new trace so every execution
        is captured in SwisperStudio.
        
        Red phase: Stub doesn't implement ainvoke wrapping.
        """
        from swisper_studio_sdk.tracing.langgraph_patch import patch_langgraph, unpatch_langgraph
        
        # Track handler calls
        handler_calls = []
        
        async def mock_handle_top_level(ainvoke, state, config, kwargs, trace_name):
            handler_calls.append(('top_level', trace_name))
            return await ainvoke(state, config, **kwargs)
        
        # This test verifies the handler is called - implementation will come in Green phase
        # For now, we just verify the patch function returns True
        result = patch_langgraph()
        
        # In Red phase, this should fail because stub returns False
        assert result is True, "patch_langgraph should return True"
        
        unpatch_langgraph()


# =============================================================================
# EDGE CASE TESTS
# =============================================================================

class TestPatchGuards:
    """Test double-patching prevention and thread safety"""
    
    @pytest.mark.unit
    def test_patch_prevents_double_patching(self):
        """
        Business: Multiple modules importing SDK shouldn't cause errors
        or corrupt the patching. This protects against accidental double
        initialization in complex applications.
        
        Red phase: Stub always returns False.
        """
        from swisper_studio_sdk.tracing.langgraph_patch import (
            patch_langgraph, 
            unpatch_langgraph,
            is_patched
        )
        
        # Ensure clean state
        unpatch_langgraph()
        
        # First call should succeed
        first_result = patch_langgraph()
        assert first_result is True, "First patch should return True"
        
        # Second call should return False (already patched)
        second_result = patch_langgraph()
        assert second_result is False, "Second patch should return False (already patched)"
        
        # Verify state
        assert is_patched() is True, "Should report as patched"
        
        # After unpatch, should be able to patch again
        unpatch_langgraph()
        assert is_patched() is False, "Should report as not patched after unpatch"
        
        third_result = patch_langgraph()
        assert third_result is True, "Should be able to patch again after unpatch"
        
        # Clean up
        unpatch_langgraph()


class TestExplicitTraceName:
    """Test explicit trace name override"""
    
    @pytest.mark.unit
    def test_set_trace_name_overrides_inference(self):
        """
        Business: When automatic naming isn't appropriate (e.g., non-standard
        class names), developers should be able to explicitly set trace names.
        
        Red phase: set_trace_name does nothing.
        """
        from swisper_studio_sdk.tracing.langgraph_patch import set_trace_name
        
        # Create a simple object (not MagicMock - it auto-creates attributes)
        class SimpleGraph:
            pass
        
        graph = SimpleGraph()
        
        # Verify attribute doesn't exist initially
        assert not hasattr(graph, '_swisper_trace_name'), "Should not have attribute initially"
        
        # Set explicit name
        set_trace_name(graph, "custom_agent_name")
        
        # Verify attribute was set (stub does nothing, so this fails)
        assert hasattr(graph, '_swisper_trace_name'), "Should set _swisper_trace_name attribute"
        assert graph._swisper_trace_name == "custom_agent_name"


class TestUserInTheLoopHandling:
    """Test that UserInTheLoop exceptions are handled correctly"""
    
    @pytest.mark.unit
    @pytest.mark.asyncio
    async def test_user_in_the_loop_marked_as_waiting(self):
        """
        Business: When an agent pauses for human input (UserInTheLoop),
        traces should show "WAITING" status (blue) not "ERROR" (red).
        This prevents users from thinking their workflow crashed.
        
        Red phase: decorator.py doesn't detect UserInTheLoop.
        """
        from swisper_studio_sdk.tracing.decorator import traced
        
        # Capture published events
        published_events = []
        
        async def mock_publish(event_type, trace_id=None, observation_id=None, data=None, **kwargs):
            published_events.append({
                'event_type': event_type,
                'data': data or {}
            })
            return True
        
        # Create UserInTheLoop exception class
        class UserInTheLoop(Exception):
            pass
        
        # Create a function that raises UserInTheLoop
        @traced("test_node")
        async def node_with_interrupt(state):
            raise UserInTheLoop("Need user confirmation")
        
        # Mock all necessary components for the decorator to execute tracing
        mock_redis = MagicMock()
        
        with patch('swisper_studio_sdk.tracing.decorator.publish_event', mock_publish):
            with patch('swisper_studio_sdk.tracing.decorator.get_current_trace', return_value="test-trace-id"):
                with patch('swisper_studio_sdk.tracing.decorator.get_redis_client', return_value=mock_redis):
                    # Execute and expect the exception
                    with pytest.raises(UserInTheLoop):
                        await node_with_interrupt({})
        
        # Find the end/error event
        end_events = [e for e in published_events if e['event_type'] in ('observation_end', 'observation_error')]
        
        assert len(end_events) > 0, "Should have published an end/error event"
        
        # The event should have level=WAITING, not ERROR
        last_event = end_events[-1]
        assert last_event['data'].get('level') == 'WAITING', \
            f"UserInTheLoop should produce level='WAITING', got '{last_event['data'].get('level')}'"


# =============================================================================
# FAILURE MODE TESTS
# =============================================================================

class TestGracefulDegradation:
    """Test that tracing failures don't break agent execution"""
    
    @pytest.mark.unit
    @pytest.mark.asyncio
    async def test_patching_gracefully_degrades_on_error(self):
        """
        Business: If something goes wrong during tracing (Redis down, SDK bug),
        the original agent execution MUST continue without crashing.
        Observability should never break production workflows.
        
        Red phase: No graceful degradation implemented.
        """
        from swisper_studio_sdk.tracing.langgraph_patch import patch_langgraph, unpatch_langgraph
        
        # The actual graceful degradation test requires the full implementation
        # For Red phase, we verify the patch function handles missing langgraph gracefully
        
        # Mock langgraph as unavailable
        with patch.dict('sys.modules', {'langgraph': None, 'langgraph.graph': None}):
            # Should not raise, should return False
            try:
                result = patch_langgraph()
                # If langgraph not available, should return False gracefully
                assert result is False, "Should return False when langgraph unavailable"
            except ImportError:
                pytest.fail("patch_langgraph should handle missing langgraph gracefully")


# =============================================================================
# INTEGRATION TESTS
# =============================================================================

class TestNestedAgentHandling:
    """Test nested agent hierarchy preservation"""
    
    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_nested_agent_creates_agent_observation(self):
        """
        Business: When a supervisor agent invokes a sub-agent (like GlobalSupervisor
        calling ProductivityAgent), the trace should show the sub-agent as a nested
        AGENT observation. This is critical for understanding agent orchestration.
        
        Red phase: Nested agent detection not implemented.
        """
        from swisper_studio_sdk.tracing.langgraph_patch import patch_langgraph, unpatch_langgraph
        
        # Track which handler was called
        handler_calls = []
        
        # Mock the handlers from graph_wrapper
        async def mock_handle_nested(*args, **kwargs):
            handler_calls.append('nested')
            return {"result": "nested"}
        
        async def mock_handle_top_level(*args, **kwargs):
            handler_calls.append('top_level')
            return {"result": "top_level"}
        
        # For Red phase, we verify patch returns True (it currently returns False)
        result = patch_langgraph()
        
        # This will fail in Red phase because stub returns False
        assert result is True, "patch_langgraph should succeed"
        
        # In Green phase, we'll add full integration test with actual nested invocation
        
        unpatch_langgraph()


# =============================================================================
# VERSION COMPATIBILITY TESTS
# =============================================================================

class TestVersionCompatibility:
    """Test LangGraph version checking"""
    
    @pytest.mark.unit
    def test_check_langgraph_version_returns_true_for_compatible(self):
        """
        Business: SDK should verify LangGraph version is compatible before
        patching to avoid runtime errors from API changes.
        
        Red phase: Stub returns False.
        """
        from swisper_studio_sdk.tracing.langgraph_patch import _check_langgraph_version
        
        # Mock langgraph with compatible version
        mock_langgraph = MagicMock()
        mock_langgraph.__version__ = "0.2.0"
        
        with patch.dict('sys.modules', {'langgraph': mock_langgraph}):
            result = _check_langgraph_version()
            
            # Should return True for compatible versions
            assert result is True, "Should return True for compatible LangGraph version"
