#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Unit tests for the filter GUI components.

:Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
:Description: Verify filter tree model structure, manager switching, and editor signals.
"""

from __future__ import annotations

import pytest

pytest.importorskip('PySide6', reason='PySide6 is required for GUI tests')
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication

from mafw.steering.models import (
    Condition,
    FieldFilterConfig,
    FieldInfo,
    LogicalOp,
    ModelFilterConfig,
    ModelInfo,
    ProcessorConfig,
)
from mafw.steering_gui.models.filter_tree_model import FilterTreeModel, LogicKey
from mafw.steering_gui.views.filter_editors import ConditionEditor
from mafw.steering_gui.views.filter_manager import FilterManager
from mafw.steering_gui.views.processor_parameter_editor import ProcessorParameterEditor

pytestmark = pytest.mark.gui


@pytest.fixture(scope='session')
def qapp() -> QApplication:
    app = QApplication.instance()
    if app is None:
        app = QApplication([])
    return app


def test_filter_tree_model_structure(qapp: QApplication) -> None:
    config = ProcessorConfig(
        name='TestProcessor',
        filters={
            'ModelA': [
                ModelFilterConfig(name='ModelA', model='ModelA'),
                FieldFilterConfig(name='ModelA.Field1', model='ModelA', field_name='Field1'),
            ],
            'ModelB': [],
        },
    )
    model = FilterTreeModel(config)

    # Root -> Filters of ...
    assert model.rowCount() == 1
    root_idx = model.index(0, 0)
    assert model.data(root_idx) == 'Filters of TestProcessor'

    # Children of root: ModelA, ModelB
    # The order depends on dictionary iteration order, which is insertion order in recent Python
    assert model.rowCount(root_idx) == 2

    # Find ModelA node
    model_a_idx = model.index(0, 0, root_idx)
    model_b_idx = model.index(1, 0, root_idx)

    # If order is swapped
    if 'ModelB' in str(model.data(model_a_idx)):
        model_a_idx, model_b_idx = model_b_idx, model_a_idx

    assert 'Model: ModelA' in str(model.data(model_a_idx))
    assert 'Model: ModelB' in str(model.data(model_b_idx))

    # ModelA children: Field1
    # ModelFilterConfig itself is the node data, logic/conditions are children if present.
    # Here we have no logic/conditions on ModelFilterConfig, just the FieldFilterConfig sibling.
    assert model.rowCount(model_a_idx) == 1
    field_idx = model.index(0, 0, model_a_idx)
    assert 'Field: Field1' in str(model.data(field_idx))


def test_filter_manager_switching(qapp: QApplication) -> None:
    config = ProcessorConfig(name='TestProcessor', filters={'ModelA': []})
    manager = FilterManager(config)

    # Default stack widget (processor editor)
    assert manager._stack.currentIndex() == 0
    assert manager._stack.currentWidget() == manager._processor_editor

    # Select Model node
    root_idx = manager._model.index(0, 0)  # Filters of ...
    model_idx = manager._model.index(0, 0, root_idx)  # ModelA

    manager._tree.setCurrentIndex(model_idx)
    # Check stack switched to ModelFilterEditor
    assert manager._stack.currentWidget() == manager._model_editor


def test_processor_parameter_editor_signal(qapp: QApplication) -> None:
    editor = ProcessorParameterEditor()
    received = []
    editor.edit_filters_requested.connect(lambda: received.append(True))

    editor._edit_filters_button.click()
    assert received == [True]


def test_filter_tree_view_emits_changed_signal(qapp: QApplication) -> None:
    """Verify that FilterTreeView emits filter_changed when modifying the structure."""
    from unittest.mock import patch

    from PySide6.QtWidgets import QDialog

    from mafw.steering_gui.views.filter_tree import FilterTreeView

    config = ProcessorConfig(name='TestProcessor', filters={})
    model = FilterTreeModel(config)
    tree = FilterTreeView()
    tree.set_model(model)

    received = []
    tree.filter_changed.connect(lambda: received.append(True))

    # Mock AddModelDialog to simulate adding a model filter
    with patch('mafw.steering_gui.views.filter_tree.AddModelDialog') as mock_dialog_cls:
        mock_dialog = mock_dialog_cls.return_value
        mock_dialog.exec.return_value = QDialog.DialogCode.Accepted
        mock_dialog.selected_model.return_value = 'ModelX'

        tree._add_model_filter()

    assert 'ModelX' in config.filters
    assert received == [True]


def test_filter_tree_model_logic_nodes(qapp: QApplication) -> None:
    config = ProcessorConfig(
        name='TestProcessor',
        filters={
            'ModelA': [ModelFilterConfig(name='ModelA', model='ModelA', logic_buffer='')],
        },
        logic_buffer='',
    )
    model = FilterTreeModel(config)

    model_logic_index = model.find_logic_index(LogicKey(model_name='ModelA', field_name=None))
    assert model_logic_index.isValid()
    assert model.data(model_logic_index) == 'Logic'

    root_logic_index = model.find_logic_index(LogicKey(model_name=None, field_name=None))
    assert root_logic_index.isValid()
    assert model.data(root_logic_index) == 'Logic'


def test_filter_tree_model_logic_warning_icon(qapp: QApplication) -> None:
    config = ProcessorConfig(
        name='TestProcessor',
        filters={
            'ModelA': [ModelFilterConfig(name='ModelA', model='ModelA', logic_buffer='')],
        },
    )
    model = FilterTreeModel(config)

    logic_key = LogicKey(model_name='ModelA', field_name=None)
    model.set_logic_warning(logic_key, True)
    index = model.find_logic_index(logic_key)
    icon = model.data(index, role=Qt.ItemDataRole.DecorationRole)
    assert icon is not None


def test_logic_warning_survives_refresh(qapp: QApplication) -> None:
    config = ProcessorConfig(name='TestProcessor', filters={})
    config.logic_str_original = 'ModelX AND'
    config.logic_ast = None
    config.logic_is_valid = False

    model = FilterTreeModel(config)
    logic_key = LogicKey(model_name=None, field_name=None)
    index = model.find_logic_index(logic_key)
    assert index.isValid()
    assert model.data(index, role=Qt.ItemDataRole.DecorationRole) is not None

    model.refresh()
    index = model.find_logic_index(logic_key)
    assert index.isValid()
    assert model.data(index, role=Qt.ItemDataRole.DecorationRole) is not None


def test_invalid_condition_warning_survives_refresh(qapp: QApplication) -> None:
    config = ProcessorConfig(name='TestProcessor', filters={})
    model_config = ModelFilterConfig(name='ModelX', model='ModelX')
    config.filters.setdefault('ModelX', []).append(model_config)
    condition = Condition(operator=LogicalOp.EQ.value, value=None, is_valid=False)
    model_config.conditions['missing'] = condition

    model = FilterTreeModel(config)
    key = model.condition_key_for_parent(model_config, 'missing')
    assert key is not None

    index = model.find_condition_index(key)
    assert index.isValid()
    assert model.data(index, role=Qt.ItemDataRole.DecorationRole) is not None

    model.refresh()
    index = model.find_condition_index(key)
    assert index.isValid()
    assert model.data(index, role=Qt.ItemDataRole.DecorationRole) is not None


def test_condition_editor_defaults_numeric_values(qapp: QApplication) -> None:
    condition = Condition(operator=LogicalOp.EQ.value, value=None, is_valid=False)
    parent = ModelFilterConfig(name='Numeric', model='Numeric')
    parent.model_info = ModelInfo(field_info={'number': FieldInfo(name='number', type=int)})
    parent.conditions['number'] = condition

    editor = ConditionEditor()
    editor.set_condition(condition, parent, 'number')

    assert condition.value == 0
    assert condition.is_valid
