"""
Unit tests for pywasot Python wrapper classes.

Tests the Python-specific functionality, assuming Rust events work correctly.
Focus on:
- Event wrapper classes and their properties
- Direct construction via .create()
- Cloning with modifications
- Tree API (Block, TextNode, etc.)
- Navigation and finding
- Edge cases and potential breakage
"""
import pytest
import pywasot


# =============================================================================
# QuotedString Tests
# =============================================================================

class TestQuotedString:
    def test_create_unquoted(self):
        qs = pywasot.QuotedString.create("hello")
        assert qs.content == "hello"
        assert qs.quoted is False

    def test_create_quoted(self):
        qs = pywasot.QuotedString.create("hello world", quoted=True)
        assert qs.content == "hello world"
        assert qs.quoted is True

    def test_str_conversion(self):
        qs = pywasot.QuotedString.create("test")
        assert str(qs) == "test"

    def test_equality_with_string(self):
        qs = pywasot.QuotedString.create("test")
        assert qs == "test"
        assert qs != "other"

    def test_equality_with_quoted_string(self):
        qs1 = pywasot.QuotedString.create("test")
        qs2 = pywasot.QuotedString.create("test", quoted=True)
        assert qs1 == qs2  # Content equality, not quote status

    def test_hash(self):
        qs = pywasot.QuotedString.create("test")
        d = {qs: "value"}
        assert d["test"] == "value"

    def test_clone_content(self):
        qs = pywasot.QuotedString.create("original")
        cloned = qs.clone(content="modified")
        assert cloned.content == "modified"
        assert qs.content == "original"  # Original unchanged

    def test_clone_quoted(self):
        qs = pywasot.QuotedString.create("test", quoted=False)
        cloned = qs.clone(quoted=True)
        assert cloned.quoted is True
        assert qs.quoted is False


# =============================================================================
# Arguments Tests
# =============================================================================

class TestArguments:
    def test_create_empty(self):
        args = pywasot.Arguments.create()
        assert args.args_str == []
        assert args.kwargs_dict == {}
        assert len(args) == 0
        assert not args  # Falsy when empty

    def test_create_with_args(self):
        args = pywasot.Arguments.create(args=["a", "b", "c"])
        assert args.args_str == ["a", "b", "c"]
        assert len(args) == 3

    def test_create_with_kwargs(self):
        args = pywasot.Arguments.create(kwargs={"key": "value", "foo": "bar"})
        assert args.kwargs_dict == {"key": "value", "foo": "bar"}
        assert args["key"] == "value"
        assert args.get("foo") == "bar"

    def test_get_missing_key(self):
        args = pywasot.Arguments.create()
        assert args.get("missing") is None
        assert args.get("missing", "default") == "default"

    def test_getitem_missing_raises(self):
        args = pywasot.Arguments.create()
        with pytest.raises(KeyError):
            _ = args["missing"]

    def test_contains(self):
        args = pywasot.Arguments.create(kwargs={"present": "yes"})
        assert "present" in args
        assert "absent" not in args

    def test_bool_truthy(self):
        args = pywasot.Arguments.create(args=["x"])
        assert args  # Truthy when non-empty

    def test_clone(self):
        args = pywasot.Arguments.create(args=["a"], kwargs={"k": "v"})
        cloned = args.clone(args=["b", "c"])
        assert cloned.args_str == ["b", "c"]
        assert cloned.kwargs_dict == {"k": "v"}  # kwargs preserved


# =============================================================================
# MetaLine Tests
# =============================================================================

class TestMetaLine:
    def test_create_simple(self):
        ml = pywasot.MetaLine.create("paragraph")
        assert ml.type_name == "paragraph"
        assert ml.type == "paragraph"  # Alias
        assert ml.level == 1
        assert ml.text == ""
        assert ml.id is None

    def test_create_with_all_fields(self):
        ml = pywasot.MetaLine.create(
            "section",
            level=2,
            text="inline",
            id="intro",
            args=["arg1"],
            kwargs={"key": "value"},
        )
        assert ml.type_name == "section"
        assert ml.level == 2
        assert ml.text == "inline"
        assert ml.id == "intro"
        assert ml.block_id == "intro"  # Alias
        assert ml.args == ["arg1"]
        assert ml.kwargs == {"key": "value"}

    def test_clone_type_name(self):
        ml = pywasot.MetaLine.create("original", id="id1")
        cloned = ml.clone(type_name="modified")
        assert cloned.type_name == "modified"
        assert cloned.id == "id1"  # Preserved
        assert ml.type_name == "original"  # Unchanged

    def test_inline_text_property(self):
        ml = pywasot.MetaLine.create("block", text="hello")
        assert isinstance(ml.inline_text, pywasot.QuotedString)
        assert ml.inline_text.content == "hello"


# =============================================================================
# BlockStarted Event Tests
# =============================================================================

class TestBlockStartedEvent:
    def test_create_simple(self):
        event = pywasot.BlockStarted.create("section")
        assert event.type_name == "section"
        assert event.type == "section"
        assert event.level == 1
        assert event.text == ""
        assert event.id is None

    def test_create_with_all_fields(self):
        event = pywasot.BlockStarted.create(
            "div",
            level=3,
            text="inline text",
            id="myid",
            args=["arg1", "arg2"],
            kwargs={"class": "highlight"},
        )
        assert event.type_name == "div"
        assert event.level == 3
        assert event.text == "inline text"
        assert event.inline_text == "inline text"
        assert event.id == "myid"
        assert event.block_id == "myid"
        assert event.args == ["arg1", "arg2"]
        assert event.kwargs == {"class": "highlight"}

    def test_meta_property(self):
        event = pywasot.BlockStarted.create("test", level=2)
        assert isinstance(event.meta, pywasot.MetaLine)
        assert event.meta.type_name == "test"
        assert event.meta.level == 2

    def test_clone_modifies_correctly(self):
        event = pywasot.BlockStarted.create("original", id="old", text="old text")
        cloned = event.clone(type_name="cloned", id="new")
        assert cloned.type_name == "cloned"
        assert cloned.id == "new"
        assert cloned.text == "old text"  # Preserved
        assert event.type_name == "original"  # Original unchanged

    def test_repr(self):
        event = pywasot.BlockStarted.create("section", id="intro")
        r = repr(event)
        assert "section" in r
        assert "intro" in r


# =============================================================================
# Text Event Tests
# =============================================================================

class TestTextEvent:
    def test_create_from_string(self):
        event = pywasot.Text.create("line1\nline2\nline3")
        assert event.lines == ["line1", "line2", "line3"]
        assert event.content == "line1\nline2\nline3"

    def test_create_from_list(self):
        event = pywasot.Text.create(["a", "b", "c"])
        assert event.lines == ["a", "b", "c"]
        assert event.content == "a\nb\nc"

    def test_text_alias(self):
        event = pywasot.Text.create("hello")
        assert event.text == event.content

    def test_str_conversion(self):
        event = pywasot.Text.create("test content")
        assert str(event) == "test content"

    def test_iteration(self):
        event = pywasot.Text.create(["x", "y", "z"])
        assert list(event) == ["x", "y", "z"]

    def test_len(self):
        event = pywasot.Text.create(["a", "b"])
        assert len(event) == 2

    def test_clone_with_content(self):
        event = pywasot.Text.create("original")
        cloned = event.clone(content="modified")
        assert cloned.content == "modified"
        assert event.content == "original"

    def test_clone_with_lines(self):
        event = pywasot.Text.create("x")
        cloned = event.clone(lines=["a", "b"])
        assert cloned.lines == ["a", "b"]


# =============================================================================
# BlockFinished Event Tests
# =============================================================================

class TestBlockFinishedEvent:
    def test_create(self):
        event = pywasot.BlockFinished.create()
        assert repr(event) == "BlockFinished()"

    def test_clone(self):
        event = pywasot.BlockFinished.create()
        cloned = event.clone()
        assert repr(cloned) == "BlockFinished()"


# =============================================================================
# VoidElement Event Tests
# =============================================================================

class TestVoidElementEvent:
    def test_create_simple(self):
        event = pywasot.VoidElement.create("hr")
        assert event.type_name == "hr"
        assert event.type == "hr"
        assert event.id is None

    def test_create_with_id(self):
        event = pywasot.VoidElement.create("break", id="mybreak")
        assert event.id == "mybreak"

    def test_clone(self):
        event = pywasot.VoidElement.create("hr", id="old")
        cloned = event.clone(id="new")
        assert cloned.id == "new"
        assert event.id == "old"


# =============================================================================
# ContextLine Event Tests
# =============================================================================

class TestContextLineEvent:
    def test_create_simple(self):
        event = pywasot.ContextLine.create("set")
        assert event.type_name == "set"
        assert event.text == ""

    def test_create_with_text(self):
        event = pywasot.ContextLine.create("note", text="important")
        assert event.text == "important"

    def test_clone(self):
        event = pywasot.ContextLine.create("old", text="original")
        cloned = event.clone(type_name="new")
        assert cloned.type_name == "new"
        assert cloned.text == "original"


# =============================================================================
# Block (Tree Node) Tests
# =============================================================================

class TestBlock:
    def test_create_simple(self):
        block = pywasot.Block.create("div")
        assert block.type_name == "div"
        assert block.name == "div"  # BS4-style alias
        assert block.level == 1
        assert block.id is None
        assert len(block.children) == 0

    def test_create_with_fields(self):
        block = pywasot.Block.create(
            "section",
            level=2,
            text="inline",
            id="main",
            args=["a"],
            kwargs={"k": "v"},
        )
        assert block.id == "main"
        assert block.inline_text == "inline"
        assert block.args == ["a"]
        assert block.kwargs == {"k": "v"}

    def test_append_child(self):
        parent = pywasot.Block.create("parent")
        child = pywasot.Block.create("child")
        parent.append(child)
        assert len(parent.children) == 1
        assert child.parent is parent

    def test_insert_child(self):
        parent = pywasot.Block.create("parent")
        child1 = pywasot.Block.create("child1")
        child2 = pywasot.Block.create("child2")
        parent.append(child1)
        parent.insert(0, child2)
        assert parent.children[0] is child2
        assert parent.children[1] is child1

    def test_remove_child(self):
        parent = pywasot.Block.create("parent")
        child = pywasot.Block.create("child")
        parent.append(child)
        parent.remove(child)
        assert len(parent.children) == 0
        assert child.parent is None

    def test_iteration(self):
        parent = pywasot.Block.create("parent")
        children = [pywasot.Block.create(f"c{i}") for i in range(3)]
        for c in children:
            parent.append(c)
        assert list(parent) == children

    def test_len(self):
        block = pywasot.Block.create("x")
        assert len(block) == 0
        block.append(pywasot.Block.create("y"))
        assert len(block) == 1

    def test_getitem_by_index(self):
        parent = pywasot.Block.create("parent")
        child = pywasot.Block.create("child")
        parent.append(child)
        assert parent[0] is child
        assert parent[-1] is child

    def test_getitem_by_slice(self):
        parent = pywasot.Block.create("parent")
        for i in range(3):
            parent.append(pywasot.Block.create(f"c{i}"))
        assert len(parent[0:2]) == 2

    def test_getitem_by_type_name(self):
        parent = pywasot.Block.create("parent")
        section = pywasot.Block.create("section")
        parent.append(section)
        assert parent["section"] is section

    def test_getitem_missing_raises(self):
        block = pywasot.Block.create("x")
        with pytest.raises(KeyError):
            _ = block["nonexistent"]

    def test_contains_child_type(self):
        parent = pywasot.Block.create("parent")
        parent.append(pywasot.Block.create("child"))
        assert "child" in parent
        assert "missing" not in parent

    def test_get_with_default(self):
        block = pywasot.Block.create("x")
        assert block.get("missing") is None
        assert block.get("missing", "default") == "default"


# =============================================================================
# Block Navigation Tests
# =============================================================================

class TestBlockNavigation:
    def test_parent(self):
        parent = pywasot.Block.create("parent")
        child = pywasot.Block.create("child")
        parent.append(child)
        assert child.parent is parent
        assert parent.parent is None

    def test_parents_iterator(self):
        root = pywasot.Block.create("root")
        mid = pywasot.Block.create("mid")
        leaf = pywasot.Block.create("leaf")
        root.append(mid)
        mid.append(leaf)
        parents = list(leaf.parents)
        assert len(parents) == 2
        assert parents[0] is mid
        assert parents[1] is root

    def test_ancestors_alias(self):
        root = pywasot.Block.create("root")
        child = pywasot.Block.create("child")
        root.append(child)
        assert list(child.ancestors) == list(child.parents)

    def test_root(self):
        root = pywasot.Block.create("root")
        mid = pywasot.Block.create("mid")
        leaf = pywasot.Block.create("leaf")
        root.append(mid)
        mid.append(leaf)
        assert leaf.root is root
        assert mid.root is root
        assert root.root is root

    def test_next_sibling(self):
        parent = pywasot.Block.create("parent")
        c1 = pywasot.Block.create("c1")
        c2 = pywasot.Block.create("c2")
        parent.append(c1)
        parent.append(c2)
        assert c1.next_sibling is c2
        assert c2.next_sibling is None

    def test_previous_sibling(self):
        parent = pywasot.Block.create("parent")
        c1 = pywasot.Block.create("c1")
        c2 = pywasot.Block.create("c2")
        parent.append(c1)
        parent.append(c2)
        assert c2.previous_sibling is c1
        assert c1.previous_sibling is None

    def test_descendants(self):
        root = pywasot.Block.create("root")
        a = pywasot.Block.create("a")
        b = pywasot.Block.create("b")
        c = pywasot.Block.create("c")
        root.append(a)
        a.append(b)
        root.append(c)
        descs = list(root.descendants)
        assert len(descs) == 3
        assert a in descs
        assert b in descs
        assert c in descs


# =============================================================================
# Block Finding Tests
# =============================================================================

class TestBlockFinding:
    def test_find_by_type(self):
        root = pywasot.Block.create("root")
        section = pywasot.Block.create("section")
        para = pywasot.Block.create("paragraph")
        root.append(section)
        section.append(para)
        results = root.find("paragraph")
        assert len(results) == 1
        assert results[0] is para

    def test_find_returns_all_matches(self):
        root = pywasot.Block.create("root")
        for _ in range(3):
            root.append(pywasot.Block.create("item"))
        results = root.find("item")
        assert len(results) == 3

    def test_find_one_by_type(self):
        root = pywasot.Block.create("root")
        root.append(pywasot.Block.create("first"))
        root.append(pywasot.Block.create("second"))
        result = root.find_one("first")
        assert result.type_name == "first"

    def test_find_one_returns_none(self):
        root = pywasot.Block.create("root")
        assert root.find_one("missing") is None

    def test_find_by_id(self):
        root = pywasot.Block.create("root")
        section = pywasot.Block.create("section", id="intro")
        root.append(section)
        results = root.find("#intro")
        assert len(results) == 1
        assert results[0] is section

    def test_find_by_type_and_id(self):
        root = pywasot.Block.create("root")
        s1 = pywasot.Block.create("section", id="a")
        s2 = pywasot.Block.create("div", id="a")  # Same ID, different type
        root.append(s1)
        root.append(s2)
        results = root.find("section#a")
        assert len(results) == 1
        assert results[0] is s1

    def test_find_direct_child(self):
        # Create: container > [section > nested_para, direct_para]
        # Selector 'section > paragraph' finds paragraph directly under section
        container = pywasot.Block.create("container")
        section = pywasot.Block.create("section")
        nested_para = pywasot.Block.create("paragraph")
        direct_para = pywasot.Block.create("paragraph")
        container.append(section)
        section.append(nested_para)
        container.append(direct_para)
        # 'section > paragraph' means: find section descendants, then direct children named paragraph
        results = container.find("section > paragraph")
        assert len(results) == 1
        assert results[0] is nested_para  # Only the one directly under section

    def test_find_descendant(self):
        root = pywasot.Block.create("root")
        section = pywasot.Block.create("section")
        para = pywasot.Block.create("paragraph")
        root.append(section)
        section.append(para)
        results = root.find("section paragraph")
        assert len(results) == 1
        assert results[0] is para


# =============================================================================
# Block Text Extraction Tests
# =============================================================================

class TestBlockTextExtraction:
    def test_inline_text(self):
        block = pywasot.Block.create("heading", text="Title")
        assert block.inline_text == "Title"

    def test_text_from_text_node(self):
        block = pywasot.Block.create("paragraph")
        text = pywasot.TextNode.create("Content here")
        block.append(text)
        assert "Content here" in block.text

    def test_text_recursive(self):
        root = pywasot.Block.create("root")
        child = pywasot.Block.create("child", text="child text")
        root.append(child)
        assert "child text" in root.text

    def test_direct_text(self):
        root = pywasot.Block.create("root", text="root text")
        child = pywasot.Block.create("child", text="child text")
        root.append(child)
        assert root.direct_text == "root text"
        assert "child text" not in root.direct_text

    def test_strings_iterator(self):
        root = pywasot.Block.create("root", text="first")
        child = pywasot.Block.create("child", text="second")
        root.append(child)
        strings = list(root.strings)
        assert "first" in strings
        assert "second" in strings

    def test_stripped_strings(self):
        root = pywasot.Block.create("root", text="  spaced  ")
        strings = list(root.stripped_strings)
        assert strings == ["spaced"]

    def test_get_text_with_separator(self):
        root = pywasot.Block.create("root", text="a")
        child = pywasot.Block.create("child", text="b")
        root.append(child)
        result = root.get_text(" | ")
        assert " | " in result

    def test_get_text_with_strip(self):
        root = pywasot.Block.create("root", text="  x  ")
        result = root.get_text(strip=True)
        assert result == "x"


# =============================================================================
# TextNode Tests
# =============================================================================

class TestTextNode:
    def test_create_from_string(self):
        node = pywasot.TextNode.create("hello\nworld")
        assert node.content == "hello\nworld"
        assert node.lines == ["hello", "world"]

    def test_create_from_list(self):
        node = pywasot.TextNode.create(["a", "b"])
        assert node.lines == ["a", "b"]
        assert node.content == "a\nb"

    def test_str_conversion(self):
        node = pywasot.TextNode.create("test")
        assert str(node) == "test"


# =============================================================================
# Void Node Tests
# =============================================================================

class TestVoidNode:
    def test_create(self):
        void = pywasot.Void.create("hr")
        assert void.type_name == "hr"
        assert void.type == "hr"

    def test_create_with_id(self):
        void = pywasot.Void.create("br", id="break1")
        assert void.id == "break1"

    def test_to_wasot(self):
        void = pywasot.Void.create("hr")
        assert void.to_wasot() == ".hr:"


# =============================================================================
# Directive Node Tests
# =============================================================================

class TestDirectiveNode:
    def test_create(self):
        directive = pywasot.Directive.create("set")
        assert directive.type_name == "set"

    def test_create_with_args(self):
        directive = pywasot.Directive.create("config", kwargs={"debug": "true"})
        assert directive.kwargs == {"debug": "true"}


# =============================================================================
# JSON-like API Tests
# =============================================================================

class TestJsonApi:
    def test_loads_simple(self):
        data = pywasot.loads(".block\nHello")
        assert data["name"] == "block"
        assert "Hello" in data["text"]

    def test_loads_nested(self):
        data = pywasot.loads("""
.parent
..child
Content
""")
        assert data["name"] == "parent"
        assert len(data["children"]) == 1
        assert data["children"][0]["name"] == "child"

    def test_dumps_simple(self):
        data = {"name": "test", "args": [], "kwargs": {}, "text": "hello", "children": []}
        result = pywasot.dumps(data)
        assert ".test" in result
        assert "hello" in result

    def test_roundtrip(self):
        original = """
.article
..section
...paragraph
Text content
"""
        data = pywasot.loads(original)
        serialized = pywasot.dumps(data)
        reparsed = pywasot.loads(serialized)
        assert data["name"] == reparsed["name"]


# =============================================================================
# Edge Cases and Error Handling
# =============================================================================

class TestEdgeCases:
    def test_empty_content(self):
        block = pywasot.Block.create("empty")
        assert block.text == ""
        assert block.inline_text == ""
        assert list(block.strings) == []

    def test_special_characters_in_text(self):
        block = pywasot.Block.create("test", text="<>&\"'")
        assert block.inline_text == "<>&\"'"

    def test_unicode_content(self):
        block = pywasot.Block.create("unicode", text="Hello")
        assert block.inline_text == "Hello"
        text = pywasot.Text.create("Привет мир 日本語")
        assert "Привет" in text.content

    def test_deeply_nested_navigation(self):
        blocks = [pywasot.Block.create(f"level{i}") for i in range(10)]
        for i in range(len(blocks) - 1):
            blocks[i].append(blocks[i + 1])
        deepest = blocks[-1]
        assert len(list(deepest.parents)) == 9
        assert deepest.root is blocks[0]

    def test_block_with_no_parent(self):
        block = pywasot.Block.create("orphan")
        assert block.parent is None
        assert list(block.parents) == []
        assert block.root is block

    def test_sibling_with_no_parent(self):
        block = pywasot.Block.create("orphan")
        assert block.next_sibling is None
        assert block.previous_sibling is None

    def test_getitem_invalid_type(self):
        block = pywasot.Block.create("test")
        with pytest.raises(TypeError):
            _ = block[3.14]

    def test_setitem_index(self):
        parent = pywasot.Block.create("parent")
        child1 = pywasot.Block.create("child1")
        child2 = pywasot.Block.create("child2")
        parent.append(child1)
        parent[0] = child2
        assert parent[0] is child2

    def test_setitem_string_raises(self):
        block = pywasot.Block.create("test")
        with pytest.raises(TypeError):
            block["name"] = "value"

    def test_delitem_index(self):
        parent = pywasot.Block.create("parent")
        child = pywasot.Block.create("child")
        parent.append(child)
        del parent[0]
        assert len(parent) == 0

    def test_delitem_invalid_type_raises(self):
        block = pywasot.Block.create("test")
        with pytest.raises(TypeError):
            del block["name"]

    def test_keys_values_items(self):
        block = pywasot.Block.create("parent", kwargs={"attr": "val"})
        child = pywasot.Block.create("child")
        block.append(child)
        keys = block.keys()
        assert "child" in keys
        assert "attr" in keys

    def test_clone_does_not_modify_original(self):
        original = pywasot.BlockStarted.create("orig", id="1", args=["a"])
        cloned = original.clone(type_name="cloned", args=["b"])
        assert original.type_name == "orig"
        assert original.args == ["a"]
        assert cloned.type_name == "cloned"
        assert cloned.args == ["b"]


# =============================================================================
# Link and Footnote Tests
# =============================================================================

class TestLink:
    def test_create_simple(self):
        link = pywasot.Link.create("https://example.com")
        assert link.target == "https://example.com"
        assert link.name is None

    def test_create_with_name(self):
        link = pywasot.Link.create("https://example.com", name="Example")
        assert link.name_str == "Example"

    def test_is_uri(self):
        link = pywasot.Link.create("https://example.com")
        assert link.is_uri is True

    def test_clone(self):
        link = pywasot.Link.create("https://old.com", name="Old")
        cloned = link.clone(target="https://new.com")
        assert cloned.target == "https://new.com"
        assert cloned.name_str == "Old"
        assert link.target == "https://old.com"


class TestFootnote:
    def test_create(self):
        fn = pywasot.Footnote.create(1, "https://example.com")
        assert fn.marker == 1
        assert fn.link == "https://example.com"

    def test_clone(self):
        fn = pywasot.Footnote.create(1, "old.com")
        cloned = fn.clone(marker=2)
        assert cloned.marker == 2
        assert cloned.link == "old.com"
