from functools import partial

from observ import reactive
import pytest

try:
    from PySide6 import QtCore, QtGui, QtWidgets
except ImportError:
    pytest.skip(
        "skip test for PySide6 renderer when not available", allow_module_level=True
    )

from collagraph import Collagraph, create_element as h, EventLoopType
from collagraph.renderers import PySideRenderer


def current_window(app):
    """Returns the first instance of a window within the topLevelWidgets."""
    windows = []
    for widget in app.topLevelWidgets():
        if isinstance(widget, QtWidgets.QMainWindow):
            windows.append(widget)

    assert len(windows) <= 1
    return windows[0] if windows else None


def test_simple_structure(qtbot, qapp):
    renderer = PySideRenderer(autoshow=False)
    gui = Collagraph(renderer=renderer, event_loop_type=EventLoopType.QT)

    container = renderer.create_element("Window")

    def Simple(props):
        return h("Widget", {"object_name": "simple"})

    gui.render(h(Simple, {}), container)

    def check_simple_is_child():
        simple = container.findChild(QtWidgets.QWidget, name="simple")
        assert simple
        assert simple.parent() is container

    qtbot.waitUntil(check_simple_is_child, timeout=500)


def test_layouts(qapp, qtbot):
    renderer = PySideRenderer(autoshow=False)
    gui = Collagraph(renderer=renderer, event_loop_type=EventLoopType.QT)

    def LayoutExample(props):
        # Data to fill the box layout
        box = []
        for i in range(1, 5):
            box.append(("Button", {"text": f"Button {i}"}))

        # Data to fill the grid layout
        grid = []
        for i in range(1, 5):
            grid.append(("Label", {"text": f"Line {i}", "grid_index": (i, 0)}))
            grid.append(("LineEdit", {"grid_index": (i, 1)}))
        grid.append(
            (
                "TextEdit",
                {
                    "text": "This widget takes up about two thirds of the grid layout",
                    "grid_index": (1, 2, 4, 1),
                },
            )
        )

        # Data to fill the form layout
        form = []
        for i, widget in enumerate(["LineEdit", "ComboBox", "SpinBox"]):
            text = f"Line {i+1}:"
            if i == 1:
                text = "Line 2, long text:"
            form.append((widget, {"form_label": text, "form_index": i}))

        return h(
            "Window",
            {},
            h(
                "Widget",
                {},
                h(
                    "GroupBox",
                    {
                        "title": "Horizontal layout",
                        "object_name": "box",
                        "layout": {
                            "type": "Box",
                            "direction": "LeftToRight",
                        },
                    },
                    *[h(item[0], item[1]) for item in box],
                ),
                h(
                    "GroupBox",
                    {
                        "title": "Grid layout",
                        "object_name": "grid",
                        "layout": {"type": "Grid"},
                    },
                    *[h(item[0], item[1]) for item in grid],
                ),
                h(
                    "GroupBox",
                    {
                        "title": "Form layout",
                        "object_name": "form",
                        "layout": {"type": "Form"},
                    },
                    *[h(item[0], item[1]) for item in form],
                ),
                h(
                    "TextEdit",
                    {
                        "text": "This widget takes up all the remaining "
                        "space in the top-level layout."
                    },
                ),
                h(
                    "DialogButtonBox",
                    {},
                    h("Button", {"flag": "Ok"}),
                    h("Button", {"flag": "Cancel"}),
                ),
            ),
        )

    element = h(LayoutExample, {})
    gui.render(element, qapp)

    def check_label():
        window = current_window(qapp)
        assert window

        box = window.findChild(QtWidgets.QGroupBox, name="box")
        assert box
        assert isinstance(box.layout(), QtWidgets.QBoxLayout)
        grid = window.findChild(QtWidgets.QGroupBox, name="grid")
        assert grid
        assert isinstance(grid.layout(), QtWidgets.QGridLayout)
        form = window.findChild(QtWidgets.QGroupBox, name="form")
        assert form
        assert isinstance(form.layout(), QtWidgets.QFormLayout)

    qtbot.waitUntil(check_label, timeout=500)


def test_lists(qapp, qtbot, qtmodeltester):
    def ListsExample(props):
        def add_item():
            props["items"].append([["NEW", "ITEM"], False])

        def remove_item():
            if len(props["items"]):
                props["items"].pop(0)

        children = []
        for row, (item, _) in enumerate(props["items"]):
            for column, text in enumerate(item):
                children.append(
                    h("QStandardItem", {"text": text, "model_index": (row, column)})
                )

        item_model = h("QStandardItemModel", {"column_count": 2}, *children)

        return h(
            "Window",
            {},
            h(
                "Widget",
                {},
                h(
                    "QSplitter",
                    {},
                    h("QListView", {}, item_model),
                    h("QTableView", {}, item_model),
                    h("QTreeView", {}, item_model),
                ),
                h(
                    "Widget",
                    {
                        "layout": {
                            "type": "Box",
                            "direction": "LeftToRight",
                        },
                        "maximum-height": 50,
                    },
                    h(
                        "Button",
                        {"text": "Add", "on_clicked": add_item, "object_name": "add"},
                    ),
                    h(
                        "Button",
                        {
                            "text": "Remove",
                            "on_clicked": remove_item,
                            "object_name": "remove",
                        },
                    ),
                ),
            ),
        )

    renderer = PySideRenderer(autoshow=False)
    gui = Collagraph(renderer=renderer, event_loop_type=EventLoopType.QT)

    state = reactive(
        {
            "items": [
                [["Foo", "Bar"], False],
            ],
        }
    )

    assert len(qapp.topLevelWidgets()) == 0

    element = h(ListsExample, state)
    gui.render(element, qapp)

    window = None
    model = None

    # Check that Foo in in the model
    def check_model_contains_foo():
        nonlocal window
        nonlocal model

        window = current_window(qapp)
        assert window

        list_view = window.findChild(QtWidgets.QListView)
        model = list_view.model()

        assert model.findItems("Foo")
        assert model.findItems("Bar", column=1)
        qtmodeltester.check(model)

    qtbot.waitUntil(check_model_contains_foo, timeout=500)

    add_button = None
    remove_button = None

    def find_buttons():
        nonlocal add_button
        nonlocal remove_button

        add_button = window.findChild(QtWidgets.QPushButton, name="add")
        assert add_button
        remove_button = window.findChild(QtWidgets.QPushButton, name="remove")
        assert remove_button

    qtbot.waitUntil(find_buttons, timeout=500)
    qtbot.mouseClick(add_button, QtCore.Qt.LeftButton)

    # Check that NEW is added to the model
    def check_model_contains_new():
        assert model.findItems("Foo")
        assert model.findItems("Bar", column=1)
        assert model.findItems("NEW")
        assert model.findItems("ITEM", column=1)
        qtmodeltester.check(model)

    qtbot.waitUntil(check_model_contains_new, timeout=500)
    qtbot.mouseClick(remove_button, QtCore.Qt.LeftButton)

    # Check that Foo is removed from the model
    def check_model_does_not_contain_foo():
        assert not model.findItems("Foo")
        assert not model.findItems("Bar", column=1)
        assert model.findItems("NEW")
        assert model.findItems("ITEM", column=1)
        qtmodeltester.check(model)

    qtbot.waitUntil(check_model_does_not_contain_foo, timeout=500)


def test_menu(qapp, qtbot):
    def MenuExample(props):
        return h(
            "Window",
            {},
            h(
                "QMenuBar",
                {},
                h(
                    "QMenu",
                    {"title": "File"},
                    h(
                        "QAction",
                        {"text": "Open"},
                    ),
                ),
            ),
            h(
                "Widget",
                {},
            ),
        )

    renderer = PySideRenderer(autoshow=False)
    gui = Collagraph(renderer=renderer, event_loop_type=EventLoopType.QT)
    gui.render(h(MenuExample, {}), qapp)

    def check_file_menu():
        windows = [
            widget
            for widget in qapp.topLevelWidgets()
            if isinstance(widget, QtWidgets.QMainWindow)
        ]
        assert len(windows) == 1
        actions = windows[0].findChildren(QtGui.QAction)
        assert any([action.text() == "Open" for action in actions])

    qtbot.waitUntil(check_file_menu, timeout=500)


def test_menu_extensively(qapp, qtbot):
    from tests.data.menubar import MenuBarTest

    state = reactive(
        {
            "show_menubar": True,
            "show_menu": True,
            "show_item": True,
            "show_submenu": True,
            "show_subitem": True,
        }
    )
    renderer = PySideRenderer(autoshow=False)
    gui = Collagraph(renderer=renderer, event_loop_type=EventLoopType.QT)
    gui.render(h(MenuBarTest, state), qapp)

    def check(menubar, menu, item, submenu, subitem):
        windows = [
            widget
            for widget in qapp.topLevelWidgets()
            if isinstance(widget, QtWidgets.QMainWindow)
        ]
        assert len(windows) == 1
        assert menubar is bool(windows[0].findChild(QtWidgets.QMenuBar, "menubar"))
        assert item is bool(windows[0].findChild(QtGui.QAction, "action"))
        assert subitem is bool(windows[0].findChild(QtGui.QAction, "subaction"))

    check_items = partial(check, True, True, True, True, True)
    qtbot.waitUntil(check_items, timeout=500)

    state["show_subitem"] = False

    check_items = partial(check, True, True, True, True, False)
    qtbot.waitUntil(check_items, timeout=500)

    state["show_submenu"] = False
    state["show_subitem"] = True

    check_items = partial(check, True, True, True, False, False)
    qtbot.waitUntil(check_items, timeout=500)

    state["show_menu"] = False

    check_items = partial(check, True, False, False, False, False)
    qtbot.waitUntil(check_items, timeout=500)

    state["show_menu"] = True
    state["show_menubar"] = False

    check_items = partial(check, False, False, False, False, False)
    qtbot.waitUntil(check_items, timeout=500)
