"""
Tests for the pyscript.web module.
"""

import asyncio

import upytest
from pyscript import RUNNING_IN_WORKER, document, web, when
from pyscript.ffi import to_js


def setup():
    container = web.page.find("#test-element-container")[0]
    container.innerHTML = ""


def teardown():
    container = web.page.find("#test-element-container")[0]
    container.innerHTML = ""


def test_getitem_by_id():
    """
    An element with an id in the DOM can be retrieved by id.
    """
    result = web.page["#div-no-classes"]
    # The result is a div.
    assert result.get_tag_name() == "div"
    # The result has the expected id.
    assert result.id == "div-no-classes"
    # Now do the same but without the '#'
    result = web.page["div-no-classes"]
    assert result.get_tag_name() == "div"
    assert result.id == "div-no-classes"


def test_find_item_by_class():
    """
    Elements with a given class in the DOM can be retrieved by class via the
    find method.
    """
    ids = [
        "test_class_selector",
        "test_selector_w_children",
        "test_selector_w_children_child_1",
    ]
    expected_class = "a-test-class"
    result = web.page.find(f".{expected_class}")
    # EXPECT to find exact number of elements with the class in the page (== 3)
    assert len(result) == 3
    # EXPECT that all element ids are in the expected list
    assert [el.id for el in result] == ids


def test_read_n_write_collection_elements():
    """
    Elements with a given class in the DOM can be retrieved by class via the
    find method. They can be bulk updated via the update_all method.
    """
    elements = web.page.find(".multi-elems")
    for element in elements:
        assert element.innerHTML == f"Content {element.id.replace('#', '')}"
    new_content = "New Content"
    elements.update_all(innerHTML=new_content)
    for element in elements:
        assert element.innerHTML == new_content


class TestHelperFunctions:
    """
    Test the internal helper functions.
    """

    def test_wrap_if_not_none_with_element(self):
        """
        Test wrapping a valid DOM element.
        """

        # Create a DOM element.
        dom_div = document.createElement("div")
        dom_div.id = "test-wrap"

        # Wrap it.
        result = web._wrap_if_not_none(dom_div)

        # Should return an Element instance.
        assert isinstance(result, web.Element)
        assert result.id == "test-wrap"

    def test_wrap_if_not_none_with_none(self):
        """
        Test wrapping None returns None.
        """

        # Pass None (as a JS null).
        result = web._wrap_if_not_none(to_js(None))

        # Should return None.
        assert result is None

    def test_find_by_id_without_hash(self):
        """
        Test finding element by id without # prefix.
        """

        # Create and append an element with an id.
        test_div = web.div("Test content", id="helper-test-id")
        web.page.body.append(test_div)

        # Find it using the helper.
        result = web._find_by_id(document, "helper-test-id")

        # Should find the element.
        assert result is not None
        assert result.id == "helper-test-id"
        assert result.innerHTML == "Test content"

    def test_find_by_id_with_hash(self):
        """
        Test finding element by id with # prefix.
        """

        # Create and append an element with an id.
        test_div = web.div("Test content", id="helper-test-id-hash")
        web.page.body.append(test_div)

        # Find it using the helper with # prefix.
        result = web._find_by_id(document, "#helper-test-id-hash")

        # Should find the element.
        assert result is not None
        assert result.id == "helper-test-id-hash"

    def test_find_by_id_not_found(self):
        """
        Test finding non-existent id returns None.
        """

        # Try to find non-existent element.
        result = web._find_by_id(document, "this-id-does-not-exist")

        # Should return None.
        assert result is None

    def test_find_by_id_within_element(self):
        """
        Test finding by id within a specific element.
        """

        # Create a container with a child.
        child = web.p("Child", id="child-in-container")
        container = web.div(child, id="container-for-search")
        web.page.body.append(container)

        # Find the child within the container.
        result = web._find_by_id(container._dom_element, "child-in-container")

        # Should find the child.
        assert result is not None
        assert result.id == "child-in-container"

    def test_find_and_wrap_with_results(self):
        """
        Test finding elements by selector.
        """

        # Create multiple elements with same class.
        container = web.div(
            web.p("Para 1", classes=["test-para"]),
            web.p("Para 2", classes=["test-para"]),
            web.p("Para 3", classes=["test-para"]),
            id="find-wrap-container",
        )
        web.page.body.append(container)

        # Find them using the helper.
        result = web._find_and_wrap(container._dom_element, ".test-para")

        # Should return an ElementCollection.
        assert isinstance(result, web.ElementCollection)
        assert len(result) == 3
        assert result[0].innerHTML == "Para 1"
        assert result[1].innerHTML == "Para 2"
        assert result[2].innerHTML == "Para 3"

    def test_find_and_wrap_no_results(self):
        """
        Test finding with selector that matches nothing.
        """

        # Create a container.
        container = web.div(id="empty-container")
        web.page.body.append(container)

        # Find with selector that matches nothing.
        result = web._find_and_wrap(container._dom_element, ".does-not-exist")

        # Should return empty collection.
        assert isinstance(result, web.ElementCollection)
        assert len(result) == 0

    def test_find_and_wrap_on_document(self):
        """
        Test finding elements in entire document.
        """

        # Create elements in the document.
        web.page.body.append(web.span("Span 1", classes=["doc-span"]))
        web.page.body.append(web.span("Span 2", classes=["doc-span"]))

        # Find them in the entire document.
        result = web._find_and_wrap(document, ".doc-span")

        # Should find both.
        assert isinstance(result, web.ElementCollection)
        assert len(result) >= 2  # May have others from previous tests.


class TestElement:
    """
    Test the base Element class functionality.
    """

    def test_query(self):
        # GIVEN an existing element on the page, with at least 1 child element
        id_ = "test_selector_w_children"
        parent_div = web.page[f"#{id_}"]

        # EXPECT it to be able to query for the first child element
        div = parent_div[0]

        # EXPECT the new element to be associated with the parent
        assert (
            div.parent.id == parent_div.id
        ), f"The parent of the new element should be the parent div, but got {div.parent} instead of {parent_div}"
        # EXPECT the new element to be an Element
        assert isinstance(div, web.Element), "The new element should be an Element"
        # EXPECT the div attributes to be == to how they are configured in the page
        assert (
            div.innerHTML == "Child 1"
        ), f"The innerHTML of the div should be 'Child 1', but got {div.innerHTML}"
        assert (
            div.id == "test_selector_w_children_child_1"
        ), f"The id of the div should be 'test_selector_w_children_child_1', but got {div.id}"

    def test_equality(self):
        # GIVEN 2 different Elements pointing to the same underlying element
        id_ = "test_id_selector"
        selector = f"#{id_}"
        div = web.page.find(selector)[0]
        div2 = web.page.find(selector)[0]

        # EXPECT them to be equal
        assert div.id == div2.id
        # EXPECT them to be different objects
        assert div is not div2

        # EXPECT their value to always be equal
        assert div.innerHTML == div2.innerHTML
        div.innerHTML = "some value"

        assert div.innerHTML == div2.innerHTML == "some value"

    def test_append_element(self):
        id_ = "element-append-tests"
        div = web.page[f"#{id_}"]
        len_children_before = len(div.children)
        new_el = web.p("new element")
        div.append(new_el)
        assert len(div.children) == len_children_before + 1
        assert div.children[-1].id == new_el.id

    def test_append_dom_element_element(self):
        id_ = "element-append-tests"
        div = web.page[f"#{id_}"]
        len_children_before = len(div.children)
        new_el = web.p("new element")
        div.append(new_el._dom_element)
        assert len(div.children) == len_children_before + 1
        assert div.children[-1].id == new_el.id

    def test_append_collection(self):
        id_ = "element-append-tests"
        div = web.page[f"#{id_}"]
        len_children_before = len(div.children)
        collection = web.page.find(".collection")
        div.append(collection)
        assert len(div.children) == len_children_before + len(collection)

        for i in range(len(collection)):
            assert div.children[-1 - i].id == collection[-1 - i].id

    """
    Ensure that appending a string adds a text node.
    """
    def test_append_string(self):
        id_ = "element-append-tests"
        div = web.page[f"#{id_}"]
        len_children_before = len(div.children)
        text_to_append = "a simple string"
        div.append(text_to_append)
        assert len(div.children) == len_children_before
        assert div._dom_element.textContent.endswith(text_to_append)

    """
    Test that appending a string does not overwrite existing children.
    """
    def test_append_multiple_mixed_types(self):
        id_ = "element-append-tests"
        div = web.page[f"#{id_}"]
        new_el = web.p("First item", id="first-p")
        div.append(new_el)
        text_to_append = "Second item"
        div.append(text_to_append)
        assert div.children[-1].id == "first-p"
        assert "First item" in div._dom_element.textContent
        assert div._dom_element.textContent.endswith(text_to_append)

    def test_read_classes(self):
        id_ = "test_class_selector"
        expected_class = "a-test-class"
        div = web.page[f"#{id_}"]
        assert div.classes == {expected_class}

    def test_add_remove_class(self):
        id_ = "div-no-classes"
        classname = "tester-class"
        div = web.page[f"#{id_}"]
        assert not div.classes
        div.classes.add(classname)
        assert div.classes == {classname}
        div.classes.remove(classname)
        assert div.classes == set()
        # Handle multiple classes in a single string
        multiple_classes = "class1 class2 class3"
        div.classes.add(multiple_classes)
        assert div.classes == {"class1", "class2", "class3"}
        div.classes.remove("class2 class3")
        assert div.classes == {"class1"}
        # Remove the final class
        div.classes.remove("class1")
        # Removing a non-existent class should not raise an error.
        div.classes.remove("non-existent-class")

    async def test_when_decorator(self):
        called = False

        just_a_button = web.page.find("#a-test-button")[0]
        call_flag = asyncio.Event()

        @when("click", just_a_button)
        def on_click(event):
            nonlocal called
            called = True
            call_flag.set()

        # Now let's simulate a click on the button (using the low level JS API)
        # so we don't risk dom getting in the way
        assert not called
        just_a_button._dom_element.click()
        await call_flag.wait()
        assert called

    async def test_when_decorator_on_event(self):
        called = False

        another_button = web.page.find("#another-test-button")[0]
        call_flag = asyncio.Event()

        assert another_button.on_click is not None
        assert isinstance(another_button.on_click, web.Event)

        @when(another_button.on_click)
        def on_click(event):
            nonlocal called
            called = True
            call_flag.set()

        # Now let's simulate a click on the button (using the low level JS API)
        # so we don't risk dom getting in the way
        assert not called
        another_button._dom_element.click()
        await call_flag.wait()
        assert called

    async def test_on_event_with_default_handler(self):
        called = False
        call_flag = asyncio.Event()

        def handler(event):
            nonlocal called
            called = True
            call_flag.set()

        b = web.button("Click me", on_click=handler)

        # Now let's simulate a click on the button (using the low level JS API)
        # so we don't risk dom getting in the way
        assert not called
        b._dom_element.click()
        await call_flag.wait()
        assert called

    def test_on_event_must_be_actual_event(self):
        """
        Any on_FOO event must relate to an actual FOO event on the element.
        """
        b = web.button("Click me")
        # Non-existent event causes a ValueError
        with upytest.raises(ValueError):
            b.on_chicken
        # Buttons have an underlying "click" event so this will work.
        assert b.on_click

    def test_inner_html_attribute(self):
        # GIVEN an existing element on the page with a known empty text content
        div = web.page.find("#element_attribute_tests")[0]

        # WHEN we set the html attribute
        div.innerHTML = "<b>New Content</b>"

        # EXPECT the element html and underlying JS Element innerHTML property
        # to match what we expect and what
        assert div.innerHTML == div._dom_element.innerHTML == "<b>New Content</b>"
        assert div.textContent == div._dom_element.textContent == "New Content"

    def test_text_attribute(self):
        # GIVEN an existing element on the page with a known empty text content
        div = web.page.find("#element_attribute_tests")[0]

        # WHEN we set the html attribute
        div.textContent = "<b>New Content</b>"

        # EXPECT the element html and underlying JS Element innerHTML property
        # to match what we expect and what
        assert (
            div.innerHTML
            == div._dom_element.innerHTML
            == "&lt;b&gt;New Content&lt;/b&gt;"
        )
        assert div.textContent == div._dom_element.textContent == "<b>New Content</b>"

    def test_update_classes(self):
        """Test updating classes via update()."""
        div = web.div()
        div.update(classes=["foo", "bar"])
        assert "foo" in div.classes
        assert "bar" in div.classes

    def test_update_single_class(self):
        """Test updating single class string via update()."""
        div = web.div()
        div.update(classes="foo")
        assert "foo" in div.classes

    def test_update_style(self):
        """Test updating styles via update()."""
        div = web.div()
        div.update(style={"color": "red", "font-size": "16px"})
        assert div.style["color"] == "red", div.style["color"]
        assert div.style["font-size"] == "16px"

    def test_update_attributes(self):
        """Test updating attributes via update()."""
        div = web.div()
        div.update(id="test-id", title="Test Title")
        assert div.id == "test-id"
        assert div.title == "Test Title"

    def test_update_combined(self):
        """Test updating classes, styles, and attributes together."""
        div = web.div()
        div.update(classes=["foo"], style={"color": "red"}, id="test-id")
        assert "foo" in div.classes
        assert div.style["color"] == "red"
        assert div.id == "test-id"

    def test_getitem_integer_index(self):
        """Test indexing children by integer."""
        parent = web.div(web.p("Child 1"), web.p("Child 2"))
        assert parent[0].innerHTML == "Child 1"
        assert parent[1].innerHTML == "Child 2"

    def test_getitem_slice(self):
        """Test slicing children."""
        parent = web.div(web.p("Child 1"), web.p("Child 2"), web.p("Child 3"))
        sliced = parent[0:2]
        assert len(sliced) == 2
        assert sliced[0].innerHTML == "Child 1"
        assert sliced[1].innerHTML == "Child 2"

    def test_getitem_by_id(self):
        """Test looking up descendant by id."""
        child = web.p("Child", id="child-id")
        parent = web.div(child)
        web.page.body.append(parent)

        result = parent["child-id"]
        assert result is not None
        assert result.id == "child-id"

    def test_getitem_by_id_with_hash(self):
        """Test looking up descendant by id with # prefix."""
        child = web.p("Child", id="child-id-2")
        parent = web.div(child)
        web.page.body.append(parent)

        result = parent["#child-id-2"]
        assert result is not None
        assert result.id == "child-id-2"

    def test_clone_basic(self):
        """Test cloning an element."""
        original = web.div("Content", id="original")
        original.classes.add("test-class")

        clone = original.clone()
        assert clone.innerHTML == original.innerHTML
        assert "test-class" in clone.classes
        assert clone is not original
        assert clone._dom_element is not original._dom_element

    def test_clone_with_id(self):
        """Test cloning with new id."""
        original = web.div("Content", id="original")
        clone = original.clone(clone_id="cloned")
        assert clone.id == "cloned"
        assert original.id == "original"

    def test_for_attribute(self):
        """Test that for_ maps to htmlFor."""
        label = web.label("Test", for_="input-id")
        assert label.for_ == "input-id"
        assert 'for="input-id"' in label.outerHTML

    def test_trailing_underscore_removal(self):
        """Test that trailing underscores are removed."""
        div = web.div()
        div.class_ = "test-class"
        # Setting class_ should set the className, which affects classes
        assert "test-class" in div.classes
        assert div.className == "test-class"


class TestContainerElement:
    """Test ContainerElement specific functionality."""

    def test_container_iteration(self):
        """Test iterating over container's children."""
        parent = web.div(web.p("1"), web.p("2"), web.p("3"))
        children = list(parent)
        assert len(children) == 3
        assert children[0].innerHTML == "1"

    def test_container_children_kwarg(self):
        """Test creating container with children kwarg."""
        parent = web.div(children=[web.p("1"), web.p("2")])
        assert len(parent.children) == 2

    def test_container_html_string(self):
        """Test inserting HTML string as child."""
        parent = web.div("<b>Bold text</b>")
        assert "Bold text" in parent.innerHTML
        assert "<b>" in parent.innerHTML


class TestCollection:

    def test_iter_eq_children(self):
        elements = web.page.find(".multi-elems")
        assert list(elements) == list(elements.elements)
        assert len(elements) == 3

    def test_slices(self):
        elements = web.page.find(".multi-elems")
        assert elements[0]
        _slice = elements[:2]
        assert len(_slice) == 2
        for i, el in enumerate(_slice):
            assert el == elements[i]
        assert elements[:] == elements

    @upytest.skip(
        "Flakey in Pyodide on Worker",
        skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
    )
    async def test_when_decorator(self):
        called = False
        call_flag = asyncio.Event()

        buttons_collection = web.page.find("button")

        @when("click", buttons_collection)
        def on_click(event):
            nonlocal called
            called = True
            call_flag.set()

        # Now let's simulate a click on the button (using the low level JS API)
        # so we don't risk dom getting in the way
        assert not called
        for button in buttons_collection:
            button._dom_element.click()
            await call_flag.wait()
            assert called
            called = False
            call_flag.clear()

    def test_update_all_single_attribute(self):
        """Test updating single attribute on all elements."""
        div1 = web.div("Content 1")
        div2 = web.div("Content 2")
        collection = web.ElementCollection([div1, div2])

        collection.update_all(className="updated")
        assert div1.className == "updated"
        assert div2.className == "updated"

    def test_update_all_multiple_attributes(self):
        """Test updating multiple attributes on all elements."""
        div1 = web.div()
        div2 = web.div()
        collection = web.ElementCollection([div1, div2])

        collection.update_all(innerHTML="Hello", title="Test")
        assert div1.innerHTML == "Hello"
        assert div1.title == "Test"
        assert div2.innerHTML == "Hello"
        assert div2.title == "Test"

    def test_update_all_classes_parameter(self):
        """Test updating the class list of all elements."""
        div1 = web.div()
        div2 = web.div()
        collection = web.ElementCollection([div1, div2])

        collection.update_all(classes="active")

        assert "active" in div1.className
        assert "active" in div2.className

    def test_update_all_style_parameter(self):
        """Test updating the style of all elements."""
        div1 = web.div()
        div2 = web.div()
        collection = web.ElementCollection([div1, div2])

        collection.update_all(style={"color": "red"})

        assert div1.style["color"] == "red"
        assert div2.style["color"] == "red"

    def test_update_all_combined(self):
        """Test updating classes, styles, and content together."""
        div1 = web.div()
        div2 = web.div()
        collection = web.ElementCollection([div1, div2])

        collection.update_all(
            classes="active",
            style={"color": "blue"},
            innerHTML="Hi"
        )

        assert "active" in div1.className
        assert div1.innerHTML == "Hi"
        assert div1.style["color"] == "blue"

        assert "active" in div2.className
        assert div2.innerHTML == "Hi"
        assert div2.style["color"] == "blue"

    def test_collection_getitem_by_id(self):
        """Test looking up element by id in collection."""
        div1 = web.div(web.p("Child", id="find-me"))
        div2 = web.div(web.p("Child 2"), id="other-div")
        collection = web.ElementCollection([div1, div2])

        web.page.body.append(div1)
        web.page.body.append(div2)

        # Find a child element by id.
        result = collection["find-me"]
        assert result is not None
        assert result.id == "find-me"
        # Find an element contained within the collection by id.
        result = collection["other-div"]
        assert result is not None
        assert result.id == "other-div"
        # Check with # prefix as well.
        result = collection["#find-me"]
        assert result is not None
        assert result.id == "find-me"
        # And for the other one too.
        result = collection["#other-div"]
        assert result is not None
        assert result.id == "other-div"


class TestCreation:

    def test_create_document_element(self):
        # TODO: This test should probably be removed since it's testing the elements
        # module.
        new_el = web.div("new element")
        new_el.id = "new_el_id"
        assert isinstance(new_el, web.Element)
        assert new_el._dom_element.tagName == "DIV"
        # EXPECT the new element to be associated with the document
        assert new_el.parent is None
        web.page.body.append(new_el)

        assert web.page.find("#new_el_id")[0].parent.tagName == web.page.body.tagName

    def test_create_element_child(self):
        selector = "#element-creation-test"
        parent_div = web.page.find(selector)[0]

        # Creating an element from another element automatically creates that element
        # as a child of the original element
        new_el = web.p(
            "a div",
            classes=["code-description"],
            innerHTML="Ciao PyScripters!",
            id="test-new-el",
        )
        parent_div.append(new_el)

        assert isinstance(new_el, web.Element)
        assert new_el._dom_element.tagName == "P"

        # EXPECT the new element to be associated with the document
        assert new_el.parent.id == parent_div.id
        assert web.page.find(selector)[0].children[0].id == new_el.id


class TestInput:

    input_ids = [
        "test_rr_input_text",
        "test_rr_input_button",
        "test_rr_input_email",
        "test_rr_input_password",
    ]

    def test_value(self):
        for id_ in self.input_ids:
            expected_type = id_.split("_")[-1]
            input_el = web.page[f"#{id_}"]
            assert input_el._dom_element.type == expected_type
            assert (
                input_el.value == f"Content {id_}" == input_el._dom_element.value
            ), f"Expected '{input_el.value}' to be 'Content {id_}' to be '{input_el._dom_element.value}'"

            # Check that we can set the value
            new_value = f"New Value {expected_type}"
            input_el.value = new_value
            assert input_el.value == new_value


class TestSelect:

    def test_select_options_iter(self):
        select = web.page.find("#test_select_element_w_options")[0]

        for i, option in enumerate(select.options, 1):
            assert option.value == f"{i}"
            assert option.innerHTML == f"Option {i}"

    def test_select_options_len(self):
        select = web.page.find("#test_select_element_w_options")[0]
        assert len(select.options) == 2

    def test_select_options_clear(self):
        select = web.page.find("#test_select_element_to_clear")[0]
        assert len(select.options) == 3

        select.options.clear()

        assert len(select.options) == 0

    def test_select_element_add(self):
        # GIVEN the existing select element with no options
        select = web.page.find("#test_select_element")[0]

        # EXPECT the select element to have no options
        assert len(select.options) == 0

        # WHEN we add an option
        select.options.add(value="1", html="Option 1")

        # EXPECT the select element to have 1 option matching the attributes
        # we passed in
        assert len(select.options) == 1
        assert select.options[0].value == "1"
        assert select.options[0].innerHTML == "Option 1"

        # WHEN we add another option (blank this time)
        select.options.add("")

        # EXPECT the select element to have 2 options
        assert len(select.options) == 2

        # EXPECT the last option to have an empty value and html
        assert select.options[1].value == ""
        assert select.options[1].innerHTML == ""

        # WHEN we add another option (this time adding it in between the other 2
        # options by using an integer index)
        select.options.add(value="2", html="Option 2", before=1)

        # EXPECT the select element to have 3 options
        assert len(select.options) == 3

        # EXPECT the middle option to have the value and html we passed in
        assert select.options[0].value == "1"
        assert select.options[0].innerHTML == "Option 1"
        assert select.options[1].value == "2"
        assert select.options[1].innerHTML == "Option 2"
        assert select.options[2].value == ""
        assert select.options[2].innerHTML == ""

        # WHEN we add another option (this time adding it in between the other 2
        # options but using the option itself)
        select.options.add(
            value="3", html="Option 3", before=select.options[2], selected=True
        )

        # EXPECT the select element to have 3 options
        assert len(select.options) == 4

        # EXPECT the middle option to have the value and html we passed in
        assert select.options[0].value == "1"
        assert select.options[0].innerHTML == "Option 1"
        assert select.options[0].selected == select.options[0]._dom_element.selected
        assert select.options[0].selected is False
        assert select.options[1].value == "2"
        assert select.options[1].innerHTML == "Option 2"
        assert select.options[2].value == "3"
        assert select.options[2].innerHTML == "Option 3"
        assert select.options[2].selected == select.options[2]._dom_element.selected
        assert select.options[2].selected is True
        assert select.options[3].value == ""
        assert select.options[3].innerHTML == ""

        # WHEN we add another option (this time adding it in between the other 2
        # options but using the JS element of the option itself)
        select.options.add(
            value="2a", html="Option 2a", before=select.options[2]._dom_element
        )

        # EXPECT the select element to have 3 options
        assert len(select.options) == 5

        # EXPECT the middle option to have the value and html we passed in
        assert select.options[0].value == "1"
        assert select.options[0].innerHTML == "Option 1"
        assert select.options[1].value == "2"
        assert select.options[1].innerHTML == "Option 2"
        assert select.options[2].value == "2a"
        assert select.options[2].innerHTML == "Option 2a"
        assert select.options[3].value == "3"
        assert select.options[3].innerHTML == "Option 3"
        assert select.options[4].value == ""
        assert select.options[4].innerHTML == ""

    def test_select_options_remove(self):
        # GIVEN the existing select element with 3 options
        select = web.page["#test_select_element_to_remove"]

        # EXPECT the select element to have 3 options
        assert len(select.options) == 4
        # EXPECT the options to have the values originally set
        assert select.options[0].value == "1"
        assert select.options[1].value == "2"
        assert select.options[2].value == "3"
        assert select.options[3].value == "4"

        # WHEN we remove the second option (index starts at 0)
        select.options.remove(1)

        # EXPECT the select element to have 2 options
        assert len(select.options) == 3
        # EXPECT the options to have the values originally set but the second
        assert select.options[0].value == "1"
        assert select.options[1].value == "3"
        assert select.options[2].value == "4"

    def test_select_get_selected_option(self):
        # GIVEN the existing select element with one selected option
        select = web.page.find("#test_select_element_w_options")[0]

        # WHEN we get the selected option
        selected_option = select.options.selected

        # EXPECT the selected option to be correct
        assert selected_option.value == "2"
        assert selected_option.innerHTML == "Option 2"
        assert selected_option.selected == selected_option._dom_element.selected
        assert selected_option.selected is True


class TestElements:
    """
    This class tests all elements in the pyweb.ui.elements module. It creates
    an element of each type, both executing in the main thread and in a worker.
    It runs each test for each interpreter defined in `INTERPRETERS`

    Each individual element test looks for the element properties, sets a value
    on each the supported properties and checks if the element was created correctly
    and all it's properties were set correctly.
    """

    def __init__(self):
        # This module's setup function ensures self.container is empty.
        self.container = web.page.find("#test-element-container")[0]

    def _create_el_and_basic_asserts(
        self,
        el_type,
        el_text=None,
        properties=None,
        additional_selector_rules=None,
    ):
        """
        Create an element with all its properties set then check if the
        element was created correctly and all its properties were set correctly.
        """
        if not properties:
            properties = {}

        def parse_value(v):
            if isinstance(v, bool):
                return str(v)

            return f"{v}"

        args = []
        kwargs = {}
        if el_text:
            args.append(el_text)

        if properties:
            kwargs = {k: parse_value(v) for k, v in properties.items()}

        # Let's make sure the target div to contain the element is empty.
        container = web.page["#test-element-container"]
        container.innerHTML = ""
        assert container.innerHTML == "", container.innerHTML

        # Let's create the element
        try:
            klass = getattr(web, el_type)
            el = klass(*args, **kwargs)
            container.append(el)
        except Exception as e:
            msg = f"Failed to create element {el_type}: {e}"
            raise AssertionError(msg)

        # Let's keep the tag in 2 variables, one for the selector and another to
        # check the return tag from the selector
        locator_type = el_tag = el_type[:-1] if el_type.endswith("_") else el_type
        if additional_selector_rules:
            locator_type += f"{additional_selector_rules}"

        el = container.find(locator_type)[0]
        el.tagName == el_tag.upper()
        if el_text:
            assert (
                el.innerHTML == el_text
            ), f"In {el.tagName}, expected {el_text} but got {el.innerHTML}"
            assert el.textContent == el_text

        if properties:
            for k, v in properties.items():
                assert v == getattr(el, k), f"{k} should be {v} but is {getattr(el, k)}"
        return el

    def test_a(self):
        a = self._create_el_and_basic_asserts("a", "click me")
        assert a.textContent == "click me"

    def test_abbr(self):
        abbr = self._create_el_and_basic_asserts("abbr", "some text")
        assert abbr.textContent == "some text"

    def test_address(self):
        address = self._create_el_and_basic_asserts("address", "some text")
        assert address.textContent == "some text"

    def test_area(self):
        properties = {
            "shape": "poly",
            "coords": "129,0,260,95,129,138",
            "href": "https://developer.mozilla.org/docs/Web/HTTP",
            "target": "_blank",
            "alt": "HTTP",
        }
        # TODO: Check why click times out
        self._create_el_and_basic_asserts("area", properties=properties)

    def test_article(self):
        self._create_el_and_basic_asserts("article", "some text")

    def test_aside(self):
        self._create_el_and_basic_asserts("aside", "some text")

    def test_audio(self):
        self._create_el_and_basic_asserts(
            "audio",
            properties={
                "src": "http://localhost:8080/somefile.ogg",
                "controls": True,
            },
        )

    def test_b(self):
        self._create_el_and_basic_asserts("b", "some text")

    def test_blockquote(self):
        self._create_el_and_basic_asserts("blockquote", "some text")

    def test_br(self):
        self._create_el_and_basic_asserts("br")

    def test_element_button(self):
        button = self._create_el_and_basic_asserts("button", "click me")
        assert button.innerHTML == "click me"

    def test_element_button_attributes(self):
        button = self._create_el_and_basic_asserts("button", "click me", None)
        assert button.innerHTML == "click me"

    def test_canvas(self):
        properties = {
            "height": 100,
            "width": 120,
        }
        # TODO: Check why click times out
        self._create_el_and_basic_asserts(
            "canvas", "alt text for canvas", properties=properties
        )

    def test_caption(self):
        self._create_el_and_basic_asserts("caption", "some text")

    def test_cite(self):
        self._create_el_and_basic_asserts("cite", "some text")

    def test_code(self):
        self._create_el_and_basic_asserts("code", "import pyweb")

    def test_data(self):
        self._create_el_and_basic_asserts(
            "data", "some text", properties={"value": "123"}
        )

    def test_datalist(self):
        self._create_el_and_basic_asserts("datalist", "some items")

    def test_dd(self):
        self._create_el_and_basic_asserts("dd", "some text")

    def test_del_(self):
        self._create_el_and_basic_asserts(
            "del_", "some text", properties={"cite": "http://example.com/"}
        )

    def test_details(self):
        self._create_el_and_basic_asserts(
            "details", "some text", properties={"open": True}
        )

    def test_dialog(self):
        self._create_el_and_basic_asserts(
            "dialog", "some text", properties={"open": True}
        )

    def test_div(self):
        div = self._create_el_and_basic_asserts("div", "click me")
        assert div.innerHTML == "click me"

    def test_dl(self):
        self._create_el_and_basic_asserts("dl", "some text")

    def test_dt(self):
        self._create_el_and_basic_asserts("dt", "some text")

    def test_em(self):
        self._create_el_and_basic_asserts("em", "some text")

    def test_embed(self):
        # NOTE: Types actually matter and embed expects a string for height and width
        #       while other elements expect an int

        # TODO: It's important that we add typing soon to help with the user experience
        properties = {
            "src": "http://localhost:8080/somefile.ogg",
            "type": "video/ogg",
            "width": "250",
            "height": "200",
        }
        self._create_el_and_basic_asserts("embed", properties=properties)

    def test_fieldset(self):
        self._create_el_and_basic_asserts(
            "fieldset", "some text", properties={"name": "some name"}
        )

    def test_figcaption(self):
        self._create_el_and_basic_asserts("figcaption", "some text")

    def test_figure(self):
        self._create_el_and_basic_asserts("figure", "some text")

    def test_footer(self):
        self._create_el_and_basic_asserts("footer", "some text")

    def test_form(self):
        properties = {
            "action": "https://example.com/submit",
            "method": "post",
            "name": "some name",
            "autocomplete": "on",
            "rel": "external",
        }
        self._create_el_and_basic_asserts("form", "some text", properties=properties)

    def test_h1(self):
        self._create_el_and_basic_asserts("h1", "some text")

    def test_h2(self):
        self._create_el_and_basic_asserts("h2", "some text")

    def test_h3(self):
        self._create_el_and_basic_asserts("h3", "some text")

    def test_h4(self):
        self._create_el_and_basic_asserts("h4", "some text")

    def test_h5(self):
        self._create_el_and_basic_asserts("h5", "some text")

    def test_h6(self):
        self._create_el_and_basic_asserts("h6", "some text")

    def test_header(self):
        self._create_el_and_basic_asserts("header", "some text")

    def test_hgroup(self):
        self._create_el_and_basic_asserts("hgroup", "some text")

    def test_hr(self):
        self._create_el_and_basic_asserts("hr")

    def test_i(self):
        self._create_el_and_basic_asserts("i", "some text")

    def test_iframe(self):
        # TODO: same comment about defining the right types
        properties = {
            "src": "http://localhost:8080/somefile.html",
            "width": "250",
            "height": "200",
        }
        self._create_el_and_basic_asserts("iframe", properties=properties)

    @upytest.skip(
        "Flakey in worker.",
        skip_when=RUNNING_IN_WORKER,
    )
    async def test_img(self):
        """
        This test, thanks to downloading an image from the internet, is flakey
        when run in a worker. It's skipped when running in a worker.
        """
        properties = {
            "src": "https://picsum.photos/600/400",
            "alt": "some image",
            "width": 250,
            "height": 200,
        }
        self._create_el_and_basic_asserts("img", properties=properties)

    def test_input(self):
        # TODO: we need multiple input tests
        properties = {
            "type": "text",
            "value": "some value",
            "name": "some name",
            "autofocus": True,
            "pattern": "[A-Za-z]{3}",
            "placeholder": "some placeholder",
            "required": True,
            "size": 20,
        }
        self._create_el_and_basic_asserts("input_", properties=properties)

    def test_ins(self):
        self._create_el_and_basic_asserts(
            "ins", "some text", properties={"cite": "http://example.com/"}
        )

    def test_kbd(self):
        self._create_el_and_basic_asserts("kbd", "some text")

    def test_label(self):
        label_text = "Luke, I am your father"
        label_for = "some-id"
        # Let's create the element
        el = web.label(label_text, for_=label_for)
        # Let's check the element was configured correctly.
        assert isinstance(el, web.label), "The new element should be a label."
        assert el.textContent == label_text, "The label text should match."
        assert el._dom_element.tagName == "LABEL"
        assert el.for_ == label_for, "The label should have the correct for attribute."
        # Ensure the label element is rendered with the correct "for" attribute
        assert (
            f'for="{label_for}"' in el.outerHTML
        ), "The label should have the correct 'for' attribute in its HTML."

    def test_legend(self):
        self._create_el_and_basic_asserts("legend", "some text")

    def test_li(self):
        self._create_el_and_basic_asserts("li", "some text")

    def test_link(self):
        properties = {
            "href": "http://localhost:8080/somefile.css",
            "rel": "stylesheet",
            "type": "text/css",
        }
        self._create_el_and_basic_asserts(
            "link",
            properties=properties,
            additional_selector_rules="[href='http://localhost:8080/somefile.css']",
        )

    def test_main(self):
        self._create_el_and_basic_asserts("main", "some text")

    def test_map(self):
        self._create_el_and_basic_asserts(
            "map_", "some text", properties={"name": "somemap"}
        )

    def test_mark(self):
        self._create_el_and_basic_asserts("mark", "some text")

    def test_menu(self):
        self._create_el_and_basic_asserts("menu", "some text")

    def test_meter(self):
        properties = {
            "value": 50,
            "min": 0,
            "max": 100,
            "low": 30,
            "high": 80,
            "optimum": 50,
        }
        self._create_el_and_basic_asserts("meter", "some text", properties=properties)

    def test_nav(self):
        self._create_el_and_basic_asserts("nav", "some text")

    def test_object(self):
        properties = {
            "data": "http://localhost:8080/somefile.swf",
            "type": "application/x-shockwave-flash",
            "width": "250",
            "height": "200",
        }
        self._create_el_and_basic_asserts(
            "object_",
            properties=properties,
        )

    def test_ol(self):
        self._create_el_and_basic_asserts("ol", "some text")

    def test_optgroup(self):
        self._create_el_and_basic_asserts(
            "optgroup", "some text", properties={"label": "some label"}
        )

    def test_option(self):
        self._create_el_and_basic_asserts(
            "option", "some text", properties={"value": "some value"}
        )

    def test_output(self):
        self._create_el_and_basic_asserts("output", "some text")

    def test_p(self):
        self._create_el_and_basic_asserts("p", "some text")

    def test_picture(self):
        self._create_el_and_basic_asserts("picture", "some text")

    def test_pre(self):
        self._create_el_and_basic_asserts("pre", "some text")

    def test_progress(self):
        properties = {
            "value": 50.0,
            "max": 100.0,
        }
        self._create_el_and_basic_asserts(
            "progress", "some text", properties=properties
        )

    def test_q(self):
        self._create_el_and_basic_asserts(
            "q", "some text", properties={"cite": "http://example.com/"}
        )

    def test_s(self):
        self._create_el_and_basic_asserts("s", "some text")

    # def test_script(self):
    #     self._create_el_and_basic_asserts("script", "some text")

    def test_section(self):
        self._create_el_and_basic_asserts("section", "some text")

    def test_select(self):
        self._create_el_and_basic_asserts("select", "some text")

    def test_small(self):
        self._create_el_and_basic_asserts("small", "some text")

    def test_source(self):
        properties = {
            "src": "http://localhost:8080/somefile.ogg",
            "type": "audio/ogg",
        }
        self._create_el_and_basic_asserts(
            "source",
            properties=properties,
        )

    def test_span(self):
        self._create_el_and_basic_asserts("span", "some text")

    def test_strong(self):
        self._create_el_and_basic_asserts("strong", "some text")

    def test_style(self):
        self._create_el_and_basic_asserts(
            "style",
            "body {background-color: red;}",
        )

    def test_sub(self):
        self._create_el_and_basic_asserts("sub", "some text")

    def test_summary(self):
        self._create_el_and_basic_asserts("summary", "some text")

    def test_sup(self):
        self._create_el_and_basic_asserts("sup", "some text")

    def test_table(self):
        self._create_el_and_basic_asserts("table", "some text")

    def test_tbody(self):
        self._create_el_and_basic_asserts("tbody", "some text")

    def test_td(self):
        self._create_el_and_basic_asserts("td", "some text")

    def test_template(self):
        # We are not checking the content of template since it's sort of
        # special element
        self._create_el_and_basic_asserts("template")

    def test_textarea(self):
        self._create_el_and_basic_asserts("textarea", "some text")

    def test_tfoot(self):
        self._create_el_and_basic_asserts("tfoot", "some text")

    def test_th(self):
        self._create_el_and_basic_asserts("th", "some text")

    def test_thead(self):
        self._create_el_and_basic_asserts("thead", "some text")

    def test_time(self):
        self._create_el_and_basic_asserts("time", "some text")

    def test_title(self):
        self._create_el_and_basic_asserts("title", "some text")

    def test_tr(self):
        self._create_el_and_basic_asserts("tr", "some text")

    def test_track(self):
        properties = {
            "src": "http://localhost:8080/somefile.vtt",
            "kind": "subtitles",
            "srclang": "en",
            "label": "English",
        }
        self._create_el_and_basic_asserts(
            "track",
            properties=properties,
        )

    def test_u(self):
        self._create_el_and_basic_asserts("u", "some text")

    def test_ul(self):
        self._create_el_and_basic_asserts("ul", "some text")

    def test_var(self):
        self._create_el_and_basic_asserts("var", "some text")

    def test_video(self):
        properties = {
            "src": "http://localhost:8080/somefile.ogg",
            "controls": True,
            "width": 250,
            "height": 200,
        }
        self._create_el_and_basic_asserts("video", properties=properties)

    def test_append_py_element(self):
        div_text_content = "Luke, I am your father"
        p_text_content = "noooooooooo!"
        # Let's create the element
        el = web.div(div_text_content)
        child = web.p(p_text_content)
        el.append(child)
        self.container.append(el)
        # Check the expected content exists.
        result = self.container.find("div")
        assert len(result) == 1
        el = result[0]
        tag = el.tagName
        assert tag == "DIV", tag
        assert el.textContent == f"{div_text_content}{p_text_content}"
        assert len(el.children) == 1, "There should be only 1 child"
        assert el.children[0].tagName == "P"
        assert (
            el.children[0].parentNode.textContent
            == f"{div_text_content}{p_text_content}"
        )
        assert el.children[0].textContent == p_text_content

    def test_append_proxy_element(self):
        div_text_content = "Luke, I am your father"
        p_text_content = "noooooooooo!"
        # Let's create the element
        el = web.div(div_text_content)
        child = document.createElement("P")
        child.textContent = p_text_content
        el.append(child)
        self.container.append(el)
        # Check the expected content exists.
        result = self.container.find("div")
        assert len(result) == 1
        el = result[0]
        tag = el.tagName
        assert tag == "DIV", tag
        assert el.textContent == f"{div_text_content}{p_text_content}", el.textContent
        assert len(el.children) == 1, "There should be only 1 child"
        assert el.children[0].tagName == "P"
        assert (
            el.children[0].parentNode.textContent
            == f"{div_text_content}{p_text_content}"
        )
        assert el.children[0].textContent == p_text_content

    def test_append_py_elementcollection(self):
        div_text_content = "Luke, I am your father"
        p_text_content = "noooooooooo!"
        p2_text_content = "not me!"
        # Let's create the elements
        el = web.div(div_text_content)
        child1 = web.p(p_text_content)
        child2 = web.p(p2_text_content, id="child2")
        collection = web.ElementCollection([child1, child2])
        el.append(collection)
        self.container.append(el)
        # Check the expected content exists.
        result = self.container.find("div")
        assert len(result) == 1
        el = result[0]
        tag = el.tagName
        assert tag == "DIV", tag
        parent_full_content = f"{div_text_content}{p_text_content}{p2_text_content}"
        assert el.textContent == parent_full_content
        assert len(el.children) == 2, "There should be only 2 children"
        assert el.children[0].tagName == "P"
        assert el.children[0].parentNode.textContent == parent_full_content
        assert el.children[0].textContent == p_text_content
        assert el.children[1].tagName == "P"
        assert el.children[1].id == "child2"
        assert el.children[1].parentNode.textContent == parent_full_content
        assert el.children[1].textContent == p2_text_content

    def test_append_js_element_nodelist(self):
        div_text_content = "Luke, I am your father"
        p_text_content = "noooooooooo!"
        p2_text_content = "not me!"
        # Let's create the elements
        el = web.div(div_text_content)
        child1 = web.p(p_text_content)
        child2 = web.p(p2_text_content, id="child2")
        self.container.append(child1)
        self.container.append(child2)
        nodes = self.container._dom_element.querySelectorAll("p")
        el.append(nodes)
        self.container.append(el)
        # Check the expected content exists.
        result = self.container.find("div")
        assert len(result) == 1
        el = result[0]
        tag = el.tagName
        assert tag == "DIV", tag
        parent_full_content = f"{div_text_content}{p_text_content}{p2_text_content}"
        assert el.textContent == parent_full_content, el.innerHTML
        assert len(el.children) == 2, "There should be only 2 children"
        assert el.children[0].tagName == "P"
        assert el.children[0].parentNode.textContent == parent_full_content
        assert el.children[0].textContent == p_text_content
        assert el.children[1].tagName == "P"
        assert el.children[1].id == "child2"
        assert el.children[1].parentNode.textContent == parent_full_content
        assert el.children[1].textContent == p2_text_content


class TestPageObject:
    """Test the Page object."""

    def test_page_getitem_with_id(self):
        """Test looking up element by id using page[id]."""
        result = web.page["test_id_selector"]
        assert result is not None
        assert result.id == "test_id_selector"

    def test_page_getitem_with_hash(self):
        """Test looking up element by id with # prefix."""
        result = web.page["#test_id_selector"]
        assert result is not None
        assert result.id == "test_id_selector"

    def test_page_getitem_nonexistent(self):
        """Test looking up nonexistent id returns None."""
        result = web.page["nonexistent-id"]
        assert result is None

    def test_page_title_get(self):
        """Test getting page title."""
        original_title = web.page.title
        assert isinstance(original_title, str)

    def test_page_title_set(self):
        """Test setting page title."""
        original = web.page.title
        web.page.title = "Test Title"
        assert web.page.title == "Test Title"
        web.page.title = original  # Restore


class TestErrorCases:
    """Test error handling."""

    def test_invalid_event_name(self):
        """Test that invalid event names raise ValueError."""
        div = web.div()
        with upytest.raises(ValueError):
            div.on_nonexistent_event

    def test_append_number(self):
        """Test that appending numbers is supported and renders as text."""
        div = web.div()
        div.append(12345)
        assert div._dom_element.textContent == "12345"

    def test_event_name_without_on_prefix(self):
        """Test that get_event requires on_ prefix."""
        div = web.div()
        with upytest.raises(ValueError):
            div.get_event("click")  # Should be "on_click"
