import pytest

from textual.app import App, ComposeResult
from textual.color import Color
from textual.containers import Container
from textual.css.query import (
    DeclarationError,
    InvalidQueryFormat,
    NoMatches,
    TooManyMatches,
    WrongType,
)
from textual.widget import Widget
from textual.widgets import Input, Label


async def test_query_errors():
    app = App()
    async with app.run_test():
        with pytest.raises(InvalidQueryFormat):
            app.query_one("foo_bar")

        with pytest.raises(InvalidQueryFormat):
            app.query("foo_bar")

        with pytest.raises(InvalidQueryFormat):
            app.query("1")

        with pytest.raises(InvalidQueryFormat):
            app.query_one("1")


async def test_query():
    class View(Widget):
        pass

    class View2(View):
        pass

    class App(Widget):
        pass

    app = App()
    main_view = View(id="main")
    help_view = View2(id="help")
    app._add_child(main_view)
    app._add_child(help_view)

    widget1 = Widget(id="widget1")
    widget2 = Widget(id="widget2")
    sidebar = Widget(id="sidebar")
    sidebar.add_class("float")

    helpbar = Widget(id="helpbar")
    helpbar.add_class("float")

    main_view._add_child(widget1)
    main_view._add_child(widget2)
    main_view._add_child(sidebar)

    sub_view = View(id="sub")
    sub_view.add_class("-subview")
    main_view._add_child(sub_view)

    tooltip = Widget(id="tooltip")
    tooltip.add_class("float", "transient")
    sub_view._add_child(tooltip)

    help = Widget(id="markdown")
    help_view._add_child(help)
    help_view._add_child(helpbar)

    # repeat tests to account for caching
    for repeat in range(3):
        assert list(app.query_children()) == [main_view, help_view]
        assert list(app.query_children("*")) == [main_view, help_view]
        assert list(app.query_children("#help")) == [help_view]
        assert list(main_view.query_children(".float")) == [sidebar]

        assert list(app.query("Frob")) == []
        assert list(app.query(".frob")) == []
        assert list(app.query("#frob")) == []

        assert not app.query("NotAnApp")

        assert list(app.query("App")) == []  # Root is not included in queries
        assert list(app.query("#main")) == [main_view]
        assert list(app.query("View#main")) == [main_view]
        assert list(app.query("View2#help")) == [help_view]
        assert list(app.query("#widget1")) == [widget1]
        assert list(app.query("#Widget1")) == []  # Note case.
        assert list(app.query("#widget2")) == [widget2]
        assert list(app.query("#Widget2")) == []  # Note case.

        assert list(app.query("Widget.float")) == [sidebar, tooltip, helpbar]
        assert list(app.query(Widget).filter(".float")) == [sidebar, tooltip, helpbar]
        assert list(
            app.query(Widget)
            .exclude("App")
            .exclude("#sub")
            .exclude("#markdown")
            .exclude("#main")
            .exclude("#help")
            .exclude("#widget1")
            .exclude("#widget2")
        ) == [sidebar, tooltip, helpbar]
        assert list(reversed(app.query("Widget.float"))) == [helpbar, tooltip, sidebar]
        assert list(app.query("Widget.float").results(Widget)) == [
            sidebar,
            tooltip,
            helpbar,
        ]
        assert list(app.query("Widget.float").results()) == [
            sidebar,
            tooltip,
            helpbar,
        ]
        assert list(app.query("Widget.float").results(View)) == []
        assert app.query_one("#widget1") == widget1
        assert app.query_one("#widget1", Widget) == widget1
        with pytest.raises(TooManyMatches):
            _ = app.query_exactly_one(Widget)

        assert app.query("Widget.float")[0] == sidebar
        assert app.query("Widget.float")[0:2] == [sidebar, tooltip]

        assert list(app.query("Widget.float.transient")) == [tooltip]

        assert list(app.query("App > View")) == [main_view, help_view]
        assert list(app.query("App > View#help")) == [help_view]
        assert list(app.query("App > View#main .float ")) == [sidebar, tooltip]
        assert list(app.query("View > View")) == [sub_view]

        assert list(app.query("#help *")) == [help, helpbar]
        assert list(app.query("#main *")) == [
            widget1,
            widget2,
            sidebar,
            sub_view,
            tooltip,
        ]

        assert list(app.query("View")) == [main_view, sub_view, help_view]
        assert list(app.query("#widget1, #widget2")) == [widget1, widget2]
        assert list(app.query("#widget1 , #widget2")) == [widget1, widget2]
        assert list(app.query("#widget1, #widget2, App")) == [widget1, widget2]

        assert app.query(".float").first() == sidebar
        assert app.query(".float").last() == helpbar

        with pytest.raises(NoMatches):
            _ = app.query(".no_such_class").first()
        with pytest.raises(NoMatches):
            _ = app.query(".no_such_class").last()

        with pytest.raises(WrongType):
            _ = app.query(".float").first(View)
        with pytest.raises(WrongType):
            _ = app.query(".float").last(View)


async def test_query_classes():
    class App(Widget):
        pass

    class ClassTest(Widget):
        pass

    CHILD_COUNT = 100

    # Create a fake app to hold everything else.
    app = App()

    # Now spin up a bunch of children.
    for n in range(CHILD_COUNT):
        app._add_child(ClassTest(id=f"child{n}"))

    # Let's just be 100% sure everything was created fine.
    assert len(app.query(ClassTest)) == CHILD_COUNT

    # Now, let's check there are *no* children with the test class.
    assert len(app.query(".test")) == 0

    # Add classes via set_classes
    app.query(ClassTest).set_classes("foo bar")
    assert (len(app.query(".foo"))) == CHILD_COUNT
    assert (len(app.query(".bar"))) == CHILD_COUNT

    # Reset classes
    app.query(ClassTest).set_classes("")
    assert (len(app.query(".foo"))) == 0
    assert (len(app.query(".bar"))) == 0

    # Repeat, to check setting empty iterable
    app.query(ClassTest).set_classes("foo bar")
    assert (len(app.query(".foo"))) == CHILD_COUNT
    assert (len(app.query(".bar"))) == CHILD_COUNT

    app.query(ClassTest).set_classes([])
    assert (len(app.query(".foo"))) == 0
    assert (len(app.query(".bar"))) == 0

    # Add the test class to everything and then check again.
    app.query(ClassTest).add_class("test")
    assert len(app.query(".test")) == CHILD_COUNT

    # Remove the test class from everything then try again.
    app.query(ClassTest).remove_class("test")
    assert len(app.query(".test")) == 0

    # Add the test class to everything using set_class.
    app.query(ClassTest).set_class(True, "test")
    assert len(app.query(".test")) == CHILD_COUNT

    # Remove the test class from everything using set_class.
    app.query(ClassTest).set_class(False, "test")
    assert len(app.query(".test")) == 0

    # Add the test class to everything using toggle_class.
    app.query(ClassTest).toggle_class("test")
    assert len(app.query(".test")) == CHILD_COUNT

    # Remove the test class from everything using toggle_class.
    app.query(ClassTest).toggle_class("test")
    assert len(app.query(".test")) == 0


async def test_invalid_query():
    class App(Widget):
        pass

    app = App()

    with pytest.raises(InvalidQueryFormat):
        app.query("#3")

    with pytest.raises(InvalidQueryFormat):
        app.query("#foo").exclude("#2")


async def test_universal_selector_doesnt_select_self():
    class ExampleApp(App):
        def compose(self) -> ComposeResult:
            yield Container(
                Widget(
                    Widget(),
                    Widget(
                        Widget(),
                    ),
                ),
                Widget(),
                id="root-container",
            )

    app = ExampleApp()
    async with app.run_test():
        container = app.query_one("#root-container", Container)
        query = container.query("*")
        results = list(query.results())
        assert len(results) == 5
        assert not any(node.id == "root-container" for node in results)


async def test_query_set_styles_invalid_css_raises_error():
    app = App()
    async with app.run_test():
        with pytest.raises(DeclarationError):
            app.query(Widget).set_styles(css="random-rule: 1fr;")


async def test_query_set_styles_kwds():
    class LabelApp(App):
        def compose(self):
            yield Label("Some text")

    app = LabelApp()
    async with app.run_test():
        # Sanity check.
        assert app.query_one(Label).styles.color != Color(255, 0, 0)
        app.query(Label).set_styles(color="red")
        assert app.query_one(Label).styles.color == Color(255, 0, 0)


async def test_query_set_styles_css_and_kwds():
    class LabelApp(App):
        def compose(self):
            yield Label("Some text")

    app = LabelApp()
    async with app.run_test():
        # Sanity checks.
        lbl = app.query_one(Label)
        assert lbl.styles.color != Color(255, 0, 0)
        assert lbl.styles.background != Color(255, 0, 0)

        app.query(Label).set_styles(css="background: red;", color="red")
        assert app.query_one(Label).styles.color == Color(255, 0, 0)
        assert app.query_one(Label).styles.background == Color(255, 0, 0)


async def test_query_set_styles_css():
    class LabelApp(App):
        def compose(self):
            yield Label("Some text")

    app = LabelApp()
    async with app.run_test():
        # Sanity checks.
        lbl = app.query_one(Label)
        assert lbl.styles.color != Color(255, 0, 0)
        assert lbl.styles.background != Color(255, 0, 0)

        app.query(Label).set_styles(css="background: red; color: red;")
        assert app.query_one(Label).styles.color == Color(255, 0, 0)
        assert app.query_one(Label).styles.background == Color(255, 0, 0)


@pytest.mark.parametrize(
    "args", [(False, False), (True, False), (True, True), (False, True)]
)
async def test_query_refresh(args):
    refreshes = []

    class MyWidget(Widget):
        def refresh(self, *, repaint=None, layout=None, recompose=None):
            super().refresh(repaint=repaint, layout=layout)
            refreshes.append((repaint, layout))

    class MyApp(App):
        def compose(self):
            yield MyWidget()

    app = MyApp()
    async with app.run_test() as pilot:
        app.query(MyWidget).refresh(repaint=args[0], layout=args[1])
        assert refreshes[-1] == args


async def test_query_focus_blur():
    class FocusApp(App):
        AUTO_FOCUS = None

        def compose(self) -> ComposeResult:
            yield Input(id="foo")
            yield Input(id="bar")
            yield Input(id="baz")

    app = FocusApp()
    async with app.run_test() as pilot:
        # Nothing focused
        assert app.focused is None
        # Focus first input
        app.query(Input).focus()
        await pilot.pause()
        assert app.focused.id == "foo"
        # Blur inputs
        app.query(Input).blur()
        await pilot.pause()
        assert app.focused is None
        # Focus another
        app.query("#bar").focus()
        await pilot.pause()
        assert app.focused.id == "bar"
        # Focus non existing
        app.query("#egg").focus()
        assert app.focused.id == "bar"


async def test_query_error():
    class QueryApp(App):
        def compose(self) -> ComposeResult:
            yield Input(id="foo")

    app = QueryApp()
    async with app.run_test():
        with pytest.raises(WrongType):
            # Asking for a Label, but the widget is an Input
            app.query_one("#foo", Label)

        # Widget is an Input so this works
        foo = app.query_one("#foo", Input)
        assert isinstance(foo, Input)


async def test_query_one_optional():
    class QueryApp(App):
        AUTO_FOCUS = None

        def compose(self) -> ComposeResult:
            yield Input(id="foo")
            yield Input(classes="bar")

    app = QueryApp()
    async with app.run_test():
        assert app.query_one_optional("TextArea") is None
        assert app.query_one_optional("Input#bar") is None

        assert isinstance(app.query_one_optional("Input"), Input)
        assert isinstance(app.query_one_optional(".bar"), Input)

        # Verify that WrongType exceptions still propagate
        with pytest.raises(WrongType):
            # Asking for a Label, but the widget is an Input
            app.query_one_optional("#foo", Label)
