"""Test the Rust Python bindings from python.rs"""

import pytest
from pywasot._pywasot import (
    PyQuotedString,
    PyKeywordArgument,
    PyPositionalArgument,
    PyMetaline,
    PyContext,
    PyBlockStartedEvent,
    PyBlockFinishedEvent,
    PyTextEvent,
    PySpecificity,
    parse_wasot_string_to_events,
    parse_wasot_file_to_events,
    parse_wasot_string,
    parse_wasot_file,
    with_tools,
    calculate_specificity,
    parse_metaline,
)


class TestPyQuotedString:
    def test_creation(self):
        qs = PyQuotedString("test", quoted=True)
        assert qs.content == "test"
        assert qs.quoted is True
        assert str(qs) == "test"

    def test_default_quoted(self):
        qs = PyQuotedString("test")
        assert qs.content == "test"
        assert qs.quoted is False


class TestPyKeywordArgument:
    def test_creation(self):
        key = PyQuotedString("key")
        value = PyQuotedString("value", quoted=True)
        kwarg = PyKeywordArgument(key, value)
        assert kwarg.key.content == "key"
        assert kwarg.value.content == "value"
        assert kwarg.value.quoted is True


class TestPyPositionalArgument:
    def test_creation(self):
        value = PyQuotedString("arg1")
        pos_arg = PyPositionalArgument(value)
        assert pos_arg.value.content == "arg1"
        assert pos_arg.value.quoted is False


class TestPyMetaline:
    def test_creation(self):
        metaline = PyMetaline("paragraph", "Some text", 1, line_index=42)
        assert metaline.type_name == "paragraph"
        assert metaline.inline_text == "Some text"
        assert metaline.level == 1
        assert metaline.line_index == 42
        assert metaline.arguments == []

    def test_with_arguments(self):
        pos_arg = PyPositionalArgument(PyQuotedString("arg1"))
        kw_arg = PyKeywordArgument(
            PyQuotedString("key"), PyQuotedString("value", quoted=True)
        )
        metaline = PyMetaline(
            "div", "Content", 2, line_index=10, arguments=[pos_arg, kw_arg]
        )
        assert metaline.type_name == "div"
        assert len(metaline.arguments) == 2


class TestPyContext:
    def test_put_and_get(self):
        context = PyContext()
        context.put("key1", "value1")
        assert context.get("key1") == "value1"

    def test_get_nonexistent(self):
        context = PyContext()
        assert context.get("nonexistent") is None

    def test_items(self):
        context = PyContext()
        context.put("key1", "value1")
        context.put("key2", "value2")
        items = context.items
        assert "key1" in items
        assert "key2" in items
        assert items["key1"] == "value1"
        assert items["key2"] == "value2"
    
    def test_scoped_variables(self):
        context = PyContext()
        
        # Put a scoped variable
        context.put_scoped("scoped_var", "scoped_value")
        
        # Should be retrievable via get_scoped
        assert context.get_scoped("scoped_var") == "scoped_value"
        
        # Should also be retrievable via regular get
        assert context.get("scoped_var") == "scoped_value"
        
        # Push a new scope
        context._push_scope()
        
        # Parent variable should still be accessible
        assert context.get_scoped("scoped_var") == "scoped_value"
        
        # Add a variable in the child scope
        context.put_scoped("child_var", "child_value")
        assert context.get_scoped("child_var") == "child_value"
        
        # Pop the scope
        context._pop_scope()
        
        # Child variable should be gone
        assert context.get_scoped("child_var") is None
        assert context.get("child_var") is None
        
        # Parent variable should still exist
        assert context.get_scoped("scoped_var") == "scoped_value"
    
    def test_scoped_items_property(self):
        context = PyContext()
        
        # Add scoped variables at different levels
        context.put_scoped("level0", "value0")
        
        context._push_scope()
        context.put_scoped("level1", "value1")
        
        # scoped_items should show all visible variables
        scoped = context.scoped_items
        assert "level0" in scoped
        assert "level1" in scoped
        
        context._pop_scope()
        
        # After popping, level1 should be gone
        scoped = context.scoped_items
        assert "level0" in scoped
        assert "level1" not in scoped


class TestPyBlockStartedEvent:
    def test_creation(self):
        metaline = PyMetaline("div", "Content", 3, line_index=100)
        context = PyContext()
        event = PyBlockStartedEvent(metaline, context)
        assert event.line_index == 100
        assert event.block_type == "div"
        assert event.inline_text == "Content"
        assert event.level == 3


class TestPyTextEvent:
    def test_creation(self):
        event = PyTextEvent(["Line 1", "Line 2"])
        assert event.lines == ["Line 1", "Line 2"]


class TestPySpecificity:
    def test_creation(self):
        spec = PySpecificity(1, 2, 3)
        assert spec.ids == 1
        assert spec.classes_attributes_pseudos == 2
        assert spec.types == 3
        assert str(spec) == "(1,2,3)"
        assert repr(spec) == "Specificity(ids=1, classes_attributes_pseudos=2, types=3)"


class TestSelectorFunctions:
    def test_calculate_specificity(self):
        # Type selector
        spec = calculate_specificity("div")
        assert spec.ids == 0
        assert spec.classes_attributes_pseudos == 0
        assert spec.types == 1

        # ID selector
        spec = calculate_specificity("#myid")
        assert spec.ids == 1
        assert spec.classes_attributes_pseudos == 0
        assert spec.types == 0

    def test_calculate_specificity_invalid(self):
        with pytest.raises(ValueError):
            calculate_specificity("@invalid")


class TestParseMetaline:
    def test_valid_metaline(self):
        # Parse a metaline without inline text
        result = parse_metaline(".paragraph", 10)
        assert result.type_name == "paragraph"
        assert result.level == 1
        assert result.line_index == 10
        
        # Parse a nested metaline
        result2 = parse_metaline("..section", 20)
        assert result2.type_name == "section"
        assert result2.level == 2
        assert result2.line_index == 20

    def test_invalid_metaline(self):
        with pytest.raises(ValueError):
            parse_metaline("not a metaline", 0)


class TestParseWasotString:
    def test_parse_string_to_events(self):
        content = ".paragraph\nSome text\n.section\nMore text"
        events = parse_wasot_string_to_events(content)
        assert len(events) >= 6  # At least 2 blocks × 3 events
        
        # Check first event is BlockStartedEvent
        assert isinstance(events[0], PyBlockStartedEvent)

    def test_parse_string_with_selectors(self):
        content = ".document\n.paragraph\nSome text\n.section\nMore text"
        events = parse_wasot_string_to_events(content, selectors=["paragraph"])
        assert len(events) > 0


class TestPyWasotIterator:
    def test_iterator_creation(self):
        content = ".paragraph\nText content"
        iterator = parse_wasot_string(content)
        
        # Test iteration
        events = list(iterator)
        assert len(events) > 0
        assert isinstance(events[0], PyBlockStartedEvent)

    def test_iterator_with_tools(self):
        class MockTool:
            def __init__(self):
                self.selectors = ["paragraph"]
                self.called = False
                self.event = None

            def __call__(self, event):
                self.called = True
                self.event = event

        content = ".document\n.paragraph\nText"
        tool = MockTool()
        iterator = parse_wasot_string(content, tools=[tool])
        
        # Iterate through events
        events = list(iterator)
        assert len(events) > 0
        
        # Check if tool was called for paragraph
        if tool.called:
            assert tool.event is not None


class TestWithTools:
    def test_tool_iterator(self):
        class Tool:
            selectors = ["div"]
            called = False

            def __call__(self, event):
                self.called = True

        # Create a simple iterator from events
        content = ".div\nContent"
        events_iter = parse_wasot_string(content)
        
        tool = Tool()
        tool_iterator = with_tools(events_iter, [tool])
        
        # Process events
        list(tool_iterator)


class TestStackIntegration:
    def test_stack_in_context(self):
        content = """.document
.section
..paragraph
Text here
"""
        iterator = parse_wasot_string(content)
        
        # Get first event (document)
        first_event = next(iterator)
        assert isinstance(first_event, PyBlockStartedEvent)
        
        # Check context has stack and breadcrumbs
        if hasattr(first_event, 'context'):
            context = first_event.context
            stack = context.get("stack")
            breadcrumbs = context.get("breadcrumbs")
            
            # Stack should contain current path
            if stack is not None:
                assert isinstance(stack, list)
            if breadcrumbs is not None:
                assert isinstance(breadcrumbs, list)


class TestContextSharing:
    def test_shared_context(self):
        """Test that context is shared between events"""
        content = ".block1\n.block2"
        
        class ContextTool:
            def __init__(self):
                self.selectors = ["block1"]
                
            def __call__(self, event):
                # Put value in context
                event.context.put("shared_key", "shared_value")
        
        tool = ContextTool()
        iterator = parse_wasot_string(content, tools=[tool])
        
        events = list(iterator)
        # Check if context is shared (this would need actual implementation)
        # For now, just verify the tool mechanism works
        assert len(events) > 0


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