import sys
from lunacept.instrumentor import run_instrument
from lunacept.parse import collect_frames


def test_attribute_fallback_with_uninstrumented_object():
    """
    Test that visit_Attribute fallback works when accessing attributes
    on objects created in uninstrumented code.
    """
    def target():
        # Create an object in instrumented code
        class MyClass:
            def __init__(self):
                self.x = 42
                self.y = "hello"
        
        obj = MyClass()
        # Access attribute - this should be traced normally
        raise ValueError(obj.x)
    
    instrumented_func = run_instrument(target)
    try:
        instrumented_func()
    except Exception:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        frames = collect_frames(exc_traceback)
        tree = frames[-1].trace_tree
        
        # Find the attribute access node
        attr_node = None
        for node in tree:
            if node.expr == 'obj.x':
                attr_node = node
                break
        
        assert attr_node is not None, "Should find obj.x node"
        assert attr_node.value == 42, f"Expected value 42, got {attr_node.value}"
        
        # Check that obj node exists in children
        obj_node = None
        for child in attr_node.children:
            if child.expr == 'obj':
                obj_node = child
                break
        
        assert obj_node is not None, "Should find obj node in children"


def test_attribute_fallback_with_property():
    """
    Test that visit_Attribute fallback works with property descriptors.
    """
    def target():
        class MyClass:
            def __init__(self):
                self._value = 100
            
            @property
            def value(self):
                return self._value
        
        obj = MyClass()
        # Access property - fallback should use getattr_static
        raise ValueError(obj.value)
    
    instrumented_func = run_instrument(target)
    try:
        instrumented_func()
    except Exception:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        frames = collect_frames(exc_traceback)
        tree = frames[-1].trace_tree
        
        # Find the attribute access node
        attr_node = None
        for node in tree:
            if node.expr == 'obj.value':
                attr_node = node
                break
        
        assert attr_node is not None, "Should find obj.value node"
        # With getattr_static, we might get the property object itself
        # or the actual value depending on instrumentation
        # The important thing is that we don't get '<unknown>'
        assert attr_node.value != '<unknown>', f"Should not be unknown, got {attr_node.value}"


def test_attribute_fallback_with_nested_access():
    """
    Test that visit_Attribute fallback works with nested attribute access.
    """
    def target():
        class Inner:
            def __init__(self):
                self.data = [1, 2, 3]
        
        class Outer:
            def __init__(self):
                self.inner = Inner()
        
        obj = Outer()
        # Nested attribute access
        raise ValueError(obj.inner.data)
    
    instrumented_func = run_instrument(target)
    try:
        instrumented_func()
    except Exception:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        frames = collect_frames(exc_traceback)
        tree = frames[-1].trace_tree
        
        # Find the nested attribute access node
        attr_node = None
        for node in tree:
            if node.expr == 'obj.inner.data':
                attr_node = node
                break
        
        assert attr_node is not None, "Should find obj.inner.data node"
        assert attr_node.value == [1, 2, 3], f"Expected [1, 2, 3], got {attr_node.value}"


def test_attribute_fallback_with_missing_attribute():
    """
    Test that visit_Attribute fallback handles missing attributes gracefully.
    """
    def target():
        class MyClass:
            def __init__(self):
                self.x = 10
        
        obj = MyClass()
        # Access non-existent attribute - should raise AttributeError
        raise ValueError(obj.missing)
    
    instrumented_func = run_instrument(target)
    try:
        instrumented_func()
    except AttributeError:
        # This is expected - accessing missing attribute raises AttributeError
        # before we even get to the ValueError
        pass
    except Exception:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        frames = collect_frames(exc_traceback)
        tree = frames[-1].trace_tree
        
        # The trace tree might still be created even if the value is unknown
        # This tests that the fallback doesn't crash
        assert tree is not None


def test_attribute_fallback_with_class_attribute():
    """
    Test that visit_Attribute fallback works with class attributes.
    """
    def target():
        class MyClass:
            class_var = 999
            
            def __init__(self):
                self.instance_var = 111
        
        obj = MyClass()
        # Access class attribute through instance
        raise ValueError(obj.class_var)
    
    instrumented_func = run_instrument(target)
    try:
        instrumented_func()
    except Exception:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        frames = collect_frames(exc_traceback)
        tree = frames[-1].trace_tree
        
        # Find the attribute access node
        attr_node = None
        for node in tree:
            if node.expr == 'obj.class_var':
                attr_node = node
                break
        
        assert attr_node is not None, "Should find obj.class_var node"
        assert attr_node.value == 999, f"Expected 999, got {attr_node.value}"


def test_attribute_fallback_with_none_object():
    """
    Test that visit_Attribute fallback handles None object gracefully.
    """
    def target():
        obj = None
        # This will raise AttributeError
        raise ValueError(obj.x)
    
    instrumented_func = run_instrument(target)
    try:
        instrumented_func()
    except AttributeError:
        # Expected - None has no attribute 'x'
        pass
    except Exception:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        frames = collect_frames(exc_traceback)
        tree = frames[-1].trace_tree
        
        # Should handle gracefully without crashing
        assert tree is not None
