#!/usr/bin/env python3
"""
Error handling and edge case test suite for the Alloy language interpreter.
Tests error conditions, boundary cases, and graceful failure handling.
"""

import unittest
import sys
import os
from unittest.mock import Mock, patch

# Add the src directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'src'))

from alloy import Interpreter, parser, RuntimeError


class TestAlloyErrorCases(unittest.TestCase):
    """Test suite focusing on error conditions and edge cases"""
    
    def setUp(self):
        """Set up test fixtures"""
        self.parser = parser
        self.interpreter = Interpreter()
        # Mock the OpenAI client to avoid real API calls
        self.interpreter.openai_client = Mock()
    
    def parse_code(self, code: str):
        """Helper method to parse code"""
        return self.parser.parse(code)
    
    def interpret_code(self, code: str):
        """Helper method to parse and interpret code"""
        ast = self.parse_code(code)
        return self.interpreter.interpret(ast)

    # ========== Syntax Error Tests ==========
    
    def test_invalid_syntax_unclosed_string(self):
        """Test handling of unclosed string literals"""
        # Note: This test depends on the lexer's error handling
        invalid_codes = [
            'name = "unclosed string',
            "value = 'missing quote",
        ]
        
        for code in invalid_codes:
            with self.subTest(code=code):
                # The lexer should handle this, but we'll test what happens
                try:
                    ast = self.parse_code(code)
                    # If it parses, it might have recovered somehow
                except Exception:
                    # Expected behavior - syntax error should be caught
                    pass
    
    def test_invalid_syntax_mismatched_braces(self):
        """Test handling of mismatched braces"""
        invalid_codes = [
            'obj = {key: value',  # Missing closing brace
            'arr = [1, 2, 3',     # Missing closing bracket
            'block = { x = 1',    # Missing closing brace in block
        ]
        
        for code in invalid_codes:
            with self.subTest(code=code):
                try:
                    ast = self.parse_code(code)
                    # Some parsers might recover, test interpretation
                    try:
                        self.interpret_code(code)
                    except Exception:
                        pass  # Expected - syntax error
                except Exception:
                    pass  # Expected - parsing error
    
    def test_invalid_tool_syntax(self):
        """Test invalid tool definition syntax"""
        invalid_codes = [
            'tool missing_takes x returns y {}',  # Missing 'takes' keyword
            'tool test takes returns {}',         # Missing return variable
            'tool test takes x y returns z {}',   # Missing comma in parameters
        ]
        
        for code in invalid_codes:
            with self.subTest(code=code):
                try:
                    ast = self.parse_code(code)
                    # If parsing succeeds unexpectedly, test interpretation
                    try:
                        self.interpret_code(code)
                    except Exception:
                        pass  # Expected behavior
                except Exception:
                    pass  # Expected - parsing should fail
    
    def test_explicit_return_statements_invalid(self):
        """Test that explicit return statements are invalid syntax"""
        invalid_codes = [
            '''tool test_tool takes x returns result {
                return x + 1  # Explicit return should be invalid
            }''',
            '''tool another_tool takes a, b returns sum {
                sum = a + b
                return sum  # Explicit return should be invalid
            }''',
        ]
        
        for code in invalid_codes:
            with self.subTest(code=code):
                try:
                    ast = self.parse_code(code)
                    # If parsing succeeds, the return statement wasn't recognized as invalid
                    # This would be a problem in the language implementation
                    try:
                        self.interpret_code(code)
                        # If it runs without error, the return statement was ignored
                        # which might be acceptable behavior
                    except Exception:
                        pass  # Expected behavior - should fail
                except Exception:
                    pass  # Expected - parsing should fail on return statement
    
    def test_explicit_return_in_control_flow_invalid(self):
        """Test that explicit return statements in control flow blocks are invalid syntax"""
        invalid_codes = [
            '''x = 5
            result = when x > 0 then {
                return x + 1  # Explicit return should be invalid
            } otherwise {
                0
            }''',
            '''count = 0
            while count < 5 do {
                count = count + 1
                return count  # Explicit return should be invalid
            }''',
        ]
        
        for code in invalid_codes:
            with self.subTest(code=code):
                try:
                    ast = self.parse_code(code)
                    # If parsing succeeds, the return statement wasn't recognized as invalid
                    try:
                        self.interpret_code(code)
                        # If it runs without error, the return statement was ignored
                    except Exception:
                        pass  # Expected behavior - should fail
                except Exception:
                    pass  # Expected - parsing should fail on return statement

    # ========== Runtime Error Tests ==========
    
    def test_undefined_variable_access(self):
        """Test accessing undefined variables"""
        code = 'result = undefined_variable'
        
        with self.assertRaises(RuntimeError):
            self.interpret_code(code)
    
    def test_undefined_tool_call(self):
        """Test calling undefined tools"""
        code = 'result = undefined_tool(1, 2, 3)'
        
        with self.assertRaises(RuntimeError):
            self.interpret_code(code)
    
    def test_undefined_agent_reference(self):
        """Test referencing undefined agents"""
        code = 'result = undefined_agent "Do something"'
        
        with self.assertRaises(RuntimeError):
            self.interpret_code(code)
    
    def test_tool_parameter_mismatch(self):
        """Test calling tools with wrong number of parameters"""
        code = '''
        tool add_two takes a, b returns sum {
            sum = a + b
        }
        
        result = add_two(5)  # Missing one parameter
        '''
        
        # This should fail during tool execution with RuntimeError when 'b' is not defined
        with self.assertRaises(RuntimeError):
            self.interpret_code(code)
    
    def test_agent_tool_validation(self):
        """Test agent tool validation (agent doesn't have requested tool)"""
        # Mock the execute_agent method to simulate tool validation
        original_execute_agent = self.interpreter.execute_agent
        
        def mock_execute_agent(node, env):
            # Simulate an agent that doesn't have the requested tool
            return None, "Agent does not have tool 'nonexistent_tool'"
        
        self.interpreter.execute_agent = mock_execute_agent
        
        code = '''
        claude = agent "gpt-4o-mini"
        result = claude nonexistent_tool
        '''
        
        self.interpret_code(code)
        # The result should contain an error
        # Since we're mocking, we need to check differently
        
        # Restore original method
        self.interpreter.execute_agent = original_execute_agent

    # ========== Type Error Tests ==========
    
    def test_invalid_array_access(self):
        """Test invalid operations on arrays"""
        # Note: Current implementation might not validate this
        code = '''
        arr = [1, 2, 3]
        invalid = arr + "string"  # Type mismatch
        '''
        
        # Test if this runs - might depend on Python's type handling
        try:
            self.interpret_code(code)
        except Exception:
            pass  # Might fail due to type incompatibility
    
    def test_invalid_object_operations(self):
        """Test invalid operations on objects"""
        code = '''
        obj = {name: "Alice"}
        invalid = obj > 5  # Invalid comparison
        '''
        
        try:
            self.interpret_code(code)
        except Exception:
            pass  # Might fail due to invalid comparison

    # ========== Control Flow Error Tests ==========
    
    def test_infinite_while_loop_protection(self):
        """Test potential infinite loops (if any protection exists)"""
        # This is more of a performance/safety test
        code = '''
        count = 0
        while count >= 0 do {
            count = count + 1
        }
        '''
        
        # We'll limit this test to avoid actual infinite loops
        # In a real implementation, you might want timeout protection
        try:
            # Don't actually run this - it would be infinite
            # ast = self.parse_code(code)
            pass
        except Exception:
            pass
    
    # def test_invalid_loop_conditions(self):
    #     """Test loops with invalid conditions"""
    #     code = '''
    #     obj = {key: "value"}
    #     while obj do {  # Object as condition
    #         break_somehow = true
    #     }
    #     '''
        
    #     try:
    #         self.interpret_code(code)
    #         # In Python, objects are truthy, so this might work
    #     except Exception:
    #         pass

    # ========== Agent Error Tests ==========
    
    def test_agent_api_failure(self):
        """Test handling of agent API failures"""
        # Mock the OpenAI client to simulate API failure
        self.interpreter.openai_client.chat.completions.create.side_effect = Exception("API Error")
        
        code = '''
        claude = agent "gpt-4o-mini"
        result = claude "Test command"
        '''
        
        self.interpret_code(code)
        # The result should contain an error
        result = self.interpreter.global_env.get('result')
        # Since execute_agent returns (result, error), check the error part
        if isinstance(result, tuple):
            self.assertIsNotNone(result[1])  # Error should be present
    
    def test_agent_no_api_key(self):
        """Test handling when no API key is configured"""
        # Create interpreter without API key
        no_key_interpreter = Interpreter()
        no_key_interpreter.openai_client = None
        
        code = '''
        claude = agent "gpt-4o-mini"
        result = claude "Test command"
        '''
        
        ast = self.parse_code(code)
        no_key_interpreter.interpret(ast)
        
        # Should handle gracefully and return error
        result = no_key_interpreter.global_env.get('result')
        if isinstance(result, tuple):
            self.assertIsNotNone(result[1])  # Error should indicate missing API key

    # ========== Edge Cases Tests ==========
    
    def test_empty_tool_body(self):
        """Test tool with empty body"""
        code = '''
        tool empty_tool takes x returns result {
            # Empty body
        }
        '''
        
        try:
            self.interpret_code(code)
            # Should work, but tool might return None
        except Exception:
            pass
    
    def test_empty_agent_block(self):
        """Test agent block with no statements"""
        code = '''
        claude = agent "gpt-4o-mini"
        result = claude {
            # Empty block
        }
        '''
        
        try:
            self.interpret_code(code)
            # Should work but might return empty response
        except Exception:
            pass
    
    def test_deeply_nested_expressions(self):
        """Test deeply nested expressions"""
        code = '''
        result = ((((1 + 2) + 3) + 4) + 5)
        nested_obj = {
            a: {
                b: {
                    c: {
                        d: "deep_value"
                    }
                }
            }
        }
        '''
        
        try:
            self.interpret_code(code)
            # Should work fine
            result = self.interpreter.global_env.get('result')
            self.assertEqual(result, 15)
        except Exception as e:
            self.fail(f"Nested expressions should work: {e}")
    
    def test_very_long_strings(self):
        """Test handling of very long strings"""
        long_string = "x" * 10000
        code = f'long_text = "{long_string}"'
        
        try:
            self.interpret_code(code)
            result = self.interpreter.global_env.get('long_text')
            self.assertEqual(len(result), 10000)
        except Exception as e:
            self.fail(f"Long strings should be handled: {e}")
    
    def test_large_arrays(self):
        """Test handling of large arrays"""
        # Create array with many elements
        large_array = "[" + ", ".join(str(i) for i in range(1000)) + "]"
        code = f'large_arr = {large_array}'
        
        try:
            self.interpret_code(code)
            result = self.interpreter.global_env.get('large_arr')
            self.assertEqual(len(result), 1000)
        except Exception as e:
            self.fail(f"Large arrays should be handled: {e}")

    # ========== Variable Scoping Error Tests ==========
    
    def test_variable_scoping_in_blocks(self):
        """Test variable scoping within blocks"""
        code = '''
        x = 10
        result = when true then {
            x = 20  # This should create new variable in block scope
            y = 30  # This should be local to block
            x
        } otherwise {
            0
        }
        
        # y should not be accessible here
        try_y = y
        '''
        
        try:
            self.interpret_code(code)
            # This might fail when trying to access 'y' outside its scope
        except RuntimeError:
            pass  # Expected - 'y' should not be accessible
    
    def test_tool_parameter_scoping(self):
        """Test that tool parameters don't leak to global scope"""
        code = '''
        tool test_scope takes param1, param2 returns result {
            local_var = param1 + param2
            result = local_var
        }
        
        tool_result = test_scope(5, 10)
        
        # These should fail - parameters shouldn't be accessible
        access_param1 = param1
        '''
        
        try:
            self.interpret_code(code)
        except RuntimeError:
            pass  # Expected - parameters should not be in global scope

    # ========== Result Pattern Error Tests ==========
    
    def test_tuple_assignment_length_mismatch(self):
        """Test tuple assignment with mismatched lengths"""
        code = '''
        tool return_single takes x returns result {
            result = x
        }
        
        # Tool returns (result, error), but we try to assign to 3 variables
        a, b, c = return_single(42)
        '''
        
        try:
            self.interpret_code(code)
            # Current implementation handles this by assigning None to extra variables
            a = self.interpreter.global_env.get('a')
            b = self.interpreter.global_env.get('b')
            c = self.interpreter.global_env.get('c')
            
            # Should have assigned something to 'a' and None to others
            self.assertIsNotNone(a)
            # b and c behavior depends on implementation
        except Exception:
            pass  # Might fail depending on implementation
    
    def test_non_tuple_in_tuple_assignment(self):
        """Test tuple assignment with non-tuple value"""
        code = '''
        single_value = 42
        a, b = single_value  # Trying to unpack non-tuple
        '''
        
        try:
            self.interpret_code(code)
            # Implementation should handle this gracefully
        except Exception:
            pass  # Might fail depending on how it's handled

    # ========== Integration Error Tests ==========
    
    def test_chained_operations_with_errors(self):
        """Test error propagation in chained operations"""
        code = '''
        tool failing_tool takes x returns result {
            result = (null, "Tool failed")
        }
        
        tool processing_tool takes data returns processed {
            processed = (data + "_processed", null)
        }
        
        # This should propagate the error
        final_result = failing_tool(10) then processing_tool()
        '''
        
        try:
            self.interpret_code(code)
            # Error handling in chains might not be fully implemented
        except Exception:
            pass
    
    def test_then_chaining_error_propagation(self):
        """Test that errors propagate properly in then chaining"""
        code = '''
        tool success_tool takes x returns result {
            result = x * 2
        }
        
        tool error_tool takes x returns result {
            # Return error tuple format (result, error)
            result = (null, "This tool failed")
        }
        
        tool never_called takes x returns result {
            result = x + 100
        }
        
        # Chain should stop at error_tool and propagate the error
        final_result, error = success_tool(5) then error_tool then never_called
        '''
        
        try:
            self.interpret_code(code)
            # Check that error was propagated
            final_result = self.interpreter.global_env.get('final_result')
            error = self.interpreter.global_env.get('error')
            
            # Should have null result and error message
            if final_result is None and error == "This tool failed":
                pass  # Expected behavior
            else:
                # Check if the implementation returns the full tuple
                result_tuple = self.interpreter.global_env.get('final_result')
                if isinstance(result_tuple, tuple) and len(result_tuple) == 2:
                    res, err = result_tuple
                    if res is None and err == "This tool failed":
                        pass  # Expected behavior
        except Exception:
            pass  # Might fail due to implementation details
    
    def test_recursive_tool_calls(self):
        """Test recursive tool calls (potential stack overflow)"""
        code = '''
        tool recursive_tool takes n returns result {
            result = when n <= 0 then {
                1
            } otherwise {
                prev, error = recursive_tool(n - 1)
                n * prev
            }
        }
        
        # Test with reasonable depth
        factorial_result = recursive_tool(5)
        '''
        
        try:
            self.interpret_code(code)
            # Should work for reasonable recursion depth
        except Exception:
            pass  # Might fail due to implementation limitations


class TestAlloyEdgeCases(unittest.TestCase):
    """Additional edge case tests"""
    
    def setUp(self):
        """Set up test fixtures"""
        self.parser = parser
        self.interpreter = Interpreter()
        self.interpreter.openai_client = Mock()
    
    def interpret_code(self, code: str):
        """Helper method to parse and interpret code"""
        ast = self.parser.parse(code)
        return self.interpreter.interpret(ast)
    
    def test_special_characters_in_strings(self):
        """Test strings with special characters"""
        code = r'''
        unicode_text = "Hello 世界 🌍"
        newlines = "Line 1\nLine 2\nLine 3"
        tabs = "Col1\tCol2\tCol3"
        quotes = "He said \"Hello\" to me"
        '''
        
        try:
            self.interpret_code(code)
            # Should handle special characters
        except Exception as e:
            self.fail(f"Special characters should be handled: {e}")
    
    def test_numeric_edge_cases(self):
        """Test edge cases for numeric values"""
        code = '''
        zero = 0
        negative_zero = -0
        large_int = 999999999999999999
        small_float = 0.000000001
        '''
        
        try:
            self.interpret_code(code)
            # Should handle various numeric formats
        except Exception as e:
            self.fail(f"Numeric edge cases should be handled: {e}")
    
    def test_boolean_in_expressions(self):
        """Test boolean values in various expressions"""
        code = '''
        bool_true = true
        bool_false = false
        
        # Test boolean in arithmetic (might be implementation-dependent)
        bool_sum = bool_true + bool_false
        bool_comparison = bool_true > bool_false
        '''
        
        try:
            self.interpret_code(code)
            # Behavior depends on implementation
        except Exception:
            pass  # Might fail depending on type handling
    
    def test_mixed_type_operations(self):
        """Test operations with mixed types"""
        code = '''
        string_val = "Hello"
        number_val = 42
        bool_val = true
        
        # These might fail or have unexpected behavior
        mixed1 = string_val + number_val
        mixed2 = number_val > bool_val
        '''
        
        try:
            self.interpret_code(code)
            # Behavior is implementation-dependent
        except Exception:
            pass  # Type errors are acceptable here


if __name__ == '__main__':
    # Run the error case tests
    unittest.main(verbosity=2)