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


def test_attribute_fallback_with_threading_object():
    """
    Test that visit_Attribute fallback works with threading.Thread objects.
    This is a real-world scenario where the object is from uninstrumented stdlib code.
    """
    def target():
        def worker():
            pass
        
        # Create a Thread object - this is from uninstrumented stdlib
        t = threading.Thread(target=worker, args=(1, 2), kwargs={'key': 'value'})
        
        # Access _target attribute - this should use fallback mechanism
        raise ValueError(t._target)
    
    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 == 't._target':
                attr_node = node
                break
        
        assert attr_node is not None, "Should find t._target node"
        # The value should not be '<unknown>' thanks to the fallback
        assert attr_node.value != '<unknown>', f"Should not be unknown, got {attr_node.value}"
        # It should be the worker function
        assert callable(attr_node.value), "Should be a callable (the worker function)"


def test_attribute_fallback_with_builtin_types():
    """
    Test that visit_Attribute fallback works with built-in types.
    """
    def target():
        # Built-in types have attributes too
        s = "hello"
        # Access __class__ attribute
        raise ValueError(s.__class__)
    
    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 == 's.__class__':
                attr_node = node
                break
        
        assert attr_node is not None, "Should find s.__class__ node"
        assert attr_node.value == str, f"Expected str class, got {attr_node.value}"


def test_attribute_fallback_with_method():
    """
    Test that visit_Attribute fallback works when accessing methods.
    """
    def target():
        lst = [1, 2, 3]
        # Access append method
        raise ValueError(lst.append)
    
    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 == 'lst.append':
                attr_node = node
                break
        
        assert attr_node is not None, "Should find lst.append node"
        assert attr_node.value != '<unknown>', f"Should not be unknown, got {attr_node.value}"
        assert callable(attr_node.value), "Should be a callable method"


def test_attribute_fallback_with_descriptor():
    """
    Test that visit_Attribute fallback works with descriptors.
    """
    def target():
        class Descriptor:
            def __get__(self, obj, objtype=None):
                return 42
        
        class MyClass:
            attr = Descriptor()
        
        obj = MyClass()
        # Access descriptor - getattr_static should return the descriptor itself
        raise ValueError(obj.attr)
    
    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.attr':
                attr_node = node
                break
        
        assert attr_node is not None, "Should find obj.attr node"
        # The value should be either the descriptor or the result (42)
        # depending on whether instrumentation captured it or fallback was used
        assert attr_node.value != '<unknown>', f"Should not be unknown, got {attr_node.value}"


def test_attribute_fallback_exception_handling():
    """
    Test that visit_Attribute fallback handles exceptions gracefully.
    """
    def target():
        class BadDescriptor:
            def __get__(self, obj, objtype=None):
                raise RuntimeError("Bad descriptor!")
        
        class MyClass:
            bad_attr = BadDescriptor()
        
        obj = MyClass()
        # This will raise RuntimeError when accessed
        raise ValueError(obj.bad_attr)
    
    instrumented_func = run_instrument(target)
    try:
        instrumented_func()
    except RuntimeError:
        # Expected - the descriptor raises RuntimeError
        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
