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

import json

import pytest

# Skip this module if PySide6 is not available
pytest.importorskip('PySide6', reason='PySide6 is required for GUI tests')

from PySide6.QtCore import QByteArray, QMimeData, QModelIndex, Qt

from mafw.steering.models import GroupConfig, ProcessorConfig
from mafw.steering_gui.controllers.steering_controller import SteeringController, SteeringControllerError
from mafw.steering_gui.models.pipeline import PipelineItem, ProcessorPipeline
from mafw.steering_gui.models.steering_tree_model import SteeringTreeModel, SteeringTreeRoles

pytestmark = pytest.mark.gui


@pytest.fixture
def controller() -> SteeringController:
    return SteeringController()


@pytest.fixture
def sample_pipeline() -> ProcessorPipeline:
    p1 = PipelineItem(config=ProcessorConfig(name='Proc1'))

    # Nested hierarchy: GroupA -> GroupB -> Proc3
    group_b_config = GroupConfig(name='GroupB', processors=['Proc3'])
    group_b = PipelineItem(config=group_b_config)
    p3 = PipelineItem(config=ProcessorConfig(name='Proc3'), parent=group_b)
    group_b.children = [p3]

    group_a_config = GroupConfig(name='GroupA', processors=['GroupB'])
    group_a = PipelineItem(config=group_a_config)
    group_b.parent = group_a
    group_a.children = [group_b]

    return ProcessorPipeline(items=[p1, group_a])


def test_model_parent_deep_hierarchy(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    group_a_idx = model.index(1, 0, processors_idx)
    group_b_idx = model.index(0, 0, group_a_idx)
    proc3_idx = model.index(0, 0, group_b_idx)

    # Parent of Proc3 is GroupB
    assert model.parent(proc3_idx) == group_b_idx
    # Parent of GroupB is GroupA (hits line 172)
    assert model.parent(group_b_idx) == group_a_idx


@pytest.fixture
def model(controller: SteeringController, sample_pipeline: ProcessorPipeline) -> SteeringTreeModel:
    return SteeringTreeModel(controller, sample_pipeline)


def test_model_initialization(model: SteeringTreeModel) -> None:
    assert model.rowCount() == 1  # Only root
    root_idx = model.index(0, 0)
    assert model.rowCount(root_idx) == 4  # Globals, Database, UI, Processors


def test_model_index_and_parent(model: SteeringTreeModel) -> None:
    # Root
    root_idx = model.index(0, 0)
    assert root_idx.isValid()
    assert not model.parent(root_idx).isValid()

    # Sections
    globals_idx = model.index(0, 0, root_idx)
    assert globals_idx.isValid()
    assert model.data(globals_idx, Qt.ItemDataRole.DisplayRole) == 'Globals'
    assert model.parent(globals_idx) == root_idx

    # Processors section
    processors_idx = model.index(3, 0, root_idx)
    assert model.data(processors_idx, Qt.ItemDataRole.DisplayRole) == 'Processors'

    # Pipeline items
    proc1_idx = model.index(0, 0, processors_idx)
    assert proc1_idx.isValid()
    assert model.data(proc1_idx, Qt.ItemDataRole.DisplayRole) == 'Proc1'
    assert model.parent(proc1_idx) == processors_idx

    group_idx = model.index(1, 0, processors_idx)
    child_idx = model.index(0, 0, group_idx)
    assert child_idx.isValid()
    assert model.parent(child_idx) == group_idx


def test_model_data_roles(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    proc1_idx = model.index(0, 0, processors_idx)
    group_idx = model.index(1, 0, processors_idx)

    # Section node
    assert model.data(processors_idx, SteeringTreeRoles.SECTION_KEY) == 'processors'

    # Pipeline item
    assert model.data(proc1_idx, SteeringTreeRoles.PIPELINE_ITEM).name() == 'Proc1'
    assert model.data(proc1_idx, SteeringTreeRoles.IS_GROUP) is False
    assert model.data(group_idx, SteeringTreeRoles.IS_GROUP) is True

    # Header data
    assert model.headerData(0, Qt.Orientation.Horizontal) == 'Steering File'
    assert model.headerData(1, Qt.Orientation.Horizontal) is None


def test_flags(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    globals_idx = model.index(0, 0, root_idx)
    processors_idx = model.index(3, 0, root_idx)
    proc1_idx = model.index(0, 0, processors_idx)

    assert model.flags(globals_idx) & Qt.ItemFlag.ItemIsSelectable
    assert model.flags(processors_idx) & Qt.ItemFlag.ItemIsDropEnabled
    assert model.flags(proc1_idx) & Qt.ItemFlag.ItemIsDragEnabled


def test_mime_data(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    proc1_idx = model.index(0, 0, processors_idx)

    mime_data = model.mimeData([proc1_idx])
    assert mime_data.hasFormat('application/x-mafw-pipeline-path')

    # Verify content
    raw = mime_data.data('application/x-mafw-pipeline-path')
    payload = json.loads(bytes(raw.data()).decode('utf-8'))
    assert payload == [[0]]


def test_drop_mime_data(model: SteeringTreeModel, controller: SteeringController) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)

    # Mock some data to drop
    mime_data = QMimeData()
    payload = json.dumps([[0]]).encode('utf-8')
    mime_data.setData('application/x-mafw-pipeline-path', QByteArray(payload))

    # We need a real controller state to move things
    controller.add_processor_to_run_list('Proc1')
    controller.add_processor_to_run_list('Proc2')
    model.set_pipeline(controller.build_pipeline())

    # Drop Proc1 after Proc2
    # In a list of 2, moving index 0 to index 1 effectively swaps them.
    success = model.dropMimeData(mime_data, Qt.DropAction.MoveAction, 1, 0, processors_idx)
    assert success
    assert controller.get_processors_to_run() == ['Proc2', 'Proc1']


def test_drop_mime_data_invalid(model: SteeringTreeModel) -> None:
    mime_data = QMimeData()
    # Not MoveAction
    assert not model.dropMimeData(mime_data, Qt.DropAction.CopyAction, 0, 0, QModelIndex())
    # No format
    assert not model.dropMimeData(mime_data, Qt.DropAction.MoveAction, 0, 0, QModelIndex())


def test_pipeline_item_from_index(model: SteeringTreeModel) -> None:
    assert model.pipeline_item_from_index(QModelIndex()) is None

    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    proc1_idx = model.index(0, 0, processors_idx)

    item = model.pipeline_item_from_index(proc1_idx)
    assert item is not None
    assert item.name() == 'Proc1'


def test_section_key_from_index(model: SteeringTreeModel) -> None:
    assert model.section_key_from_index(QModelIndex()) is None

    root_idx = model.index(0, 0)
    globals_idx = model.index(0, 0, root_idx)
    assert model.section_key_from_index(globals_idx) == 'globals'

    processors_idx = model.index(3, 0, root_idx)
    proc1_idx = model.index(0, 0, processors_idx)
    assert model.section_key_from_index(proc1_idx) == 'processors'


def test_expand_signals_connected(model: SteeringTreeModel) -> None:
    assert model.expand_signals_connected is False
    model.expand_signals_connected = True
    assert model.expand_signals_connected is True


def test_item_from_path_edge_cases(model: SteeringTreeModel) -> None:
    assert model._item_from_path([]) is None
    assert model._item_from_path([999]) is None


def test_path_for_item_edge_cases(model: SteeringTreeModel) -> None:
    lonely = PipelineItem(config=ProcessorConfig(name='Lonely'))
    assert model._path_for_item(lonely) is None


def test_model_index_invalid(model: SteeringTreeModel) -> None:
    # Invalid row/column
    assert not model.index(-1, 0).isValid()
    assert not model.index(0, 1).isValid()
    # Invalid row for root
    assert not model.index(1, 0).isValid()

    root_idx = model.index(0, 0)
    # Invalid row for sections
    assert not model.index(4, 0, root_idx).isValid()

    processors_idx = model.index(3, 0, root_idx)
    # Invalid row for processors (sample_pipeline has 2 items)
    assert not model.index(2, 0, processors_idx).isValid()

    # Index for a leaf section (like Globals)
    globals_idx = model.index(0, 0, root_idx)
    assert not model.index(0, 0, globals_idx).isValid()


def test_model_parent_complex(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    group_idx = model.index(1, 0, processors_idx)
    child_idx = model.index(0, 0, group_idx)

    # Parent of child in group
    assert model.parent(child_idx) == group_idx
    # Parent of group at root of pipeline
    assert model.parent(group_idx) == processors_idx

    # Test with an item that has a parent but is not in its children (edge case for coverage)
    PipelineItem(config=ProcessorConfig(name='Rogue'), parent=group_idx.internalPointer())
    # Note: createIndex is internal, but we can try to trigger the logic if we had such item in model
    # But since _item_from_index checks internalPointer, it's hard without a real index.


def test_model_data_edge_cases(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    # Invalid index
    assert model.data(QModelIndex()) is None
    # Section node with invalid role
    assert model.data(root_idx, Qt.ItemDataRole.DecorationRole) is None

    processors_idx = model.index(3, 0, root_idx)
    proc1_idx = model.index(0, 0, processors_idx)
    # Pipeline item with invalid role
    assert model.data(proc1_idx, Qt.ItemDataRole.SizeHintRole) is None


def test_drop_mime_data_more_invalid_cases(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    globals_idx = model.index(0, 0, root_idx)

    mime_data = QMimeData()
    payload = json.dumps([[0]]).encode('utf-8')
    mime_data.setData('application/x-mafw-pipeline-path', QByteArray(payload))

    # Drop on invalid column
    assert not model.dropMimeData(mime_data, Qt.DropAction.MoveAction, 0, 1, processors_idx)
    # Drop on invalid parent
    assert not model.dropMimeData(mime_data, Qt.DropAction.MoveAction, 0, 0, QModelIndex())
    # Drop on non-processors section
    assert not model.dropMimeData(mime_data, Qt.DropAction.MoveAction, 0, 0, globals_idx)

    # Drop on a non-group pipeline item
    proc1_idx = model.index(0, 0, processors_idx)
    assert not model.dropMimeData(mime_data, Qt.DropAction.MoveAction, 0, 0, proc1_idx)

    # Invalid JSON in mime data
    bad_mime = QMimeData()
    bad_mime.setData('application/x-mafw-pipeline-path', QByteArray(b'invalid json'))
    assert not model.dropMimeData(bad_mime, Qt.DropAction.MoveAction, 0, 0, processors_idx)

    # Valid JSON but not a list
    not_list_mime = QMimeData()
    not_list_mime.setData('application/x-mafw-pipeline-path', QByteArray(b'{}'))
    assert not model.dropMimeData(not_list_mime, Qt.DropAction.MoveAction, 0, 0, processors_idx)


def test_drop_mime_data_into_group(model: SteeringTreeModel, controller: SteeringController) -> None:
    root_idx = model.index(0, 0)
    model.index(3, 0, root_idx)

    controller.add_processor_to_run_list('Proc1')
    controller.add_group('GroupA', [])
    controller.add_processor_to_run_list('GroupA')
    model.set_pipeline(controller.build_pipeline())

    # Index of GroupA might have changed after set_pipeline
    processors_idx_new = model.index(3, 0, root_idx)
    group_a_idx = model.index(1, 0, processors_idx_new)

    mime_data = QMimeData()
    payload = json.dumps([[0]]).encode('utf-8')  # Proc1
    mime_data.setData('application/x-mafw-pipeline-path', QByteArray(payload))

    # Drop Proc1 into GroupA
    success = model.dropMimeData(mime_data, Qt.DropAction.MoveAction, -1, 0, group_a_idx)
    assert success
    assert controller.get_group_snapshot('GroupA').processors == ['Proc1']
    assert controller.get_processors_to_run() == ['GroupA']


def test_supported_drop_actions(model: SteeringTreeModel) -> None:
    assert model.supportedDropActions() == Qt.DropAction.MoveAction


def test_row_count_and_parent_edge_cases(model: SteeringTreeModel) -> None:
    # rowCount for an invalid parent (root) should be 1
    assert model.rowCount(QModelIndex()) == 1

    # parent of root node should be invalid
    root_idx = model.index(0, 0)
    assert not model.parent(root_idx).isValid()

    # rowCount for section nodes
    globals_idx = model.index(0, 0, root_idx)
    assert model.rowCount(globals_idx) == 0

    processors_idx = model.index(3, 0, root_idx)
    assert model.rowCount(processors_idx) == 2  # sample_pipeline has 2 items

    # parent of section node should be root
    assert model.parent(globals_idx) == root_idx


def test_mime_data_seen_ids(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    proc1_idx = model.index(0, 0, processors_idx)

    # Duplicate indices should be ignored in mimeData
    mime_data = model.mimeData([proc1_idx, proc1_idx])
    payload = json.loads(bytes(mime_data.data('application/x-mafw-pipeline-path').data()).decode('utf-8'))
    assert len(payload) == 1


def test_drop_mime_data_seen_ids(model: SteeringTreeModel, controller: SteeringController) -> None:
    root_idx = model.index(0, 0)
    model.index(3, 0, root_idx)

    controller.add_processor_to_run_list('P1')
    model.set_pipeline(controller.build_pipeline())

    # Duplicate paths in payload should be ignored
    mime_data = QMimeData()
    payload = json.dumps([[0], [0]]).encode('utf-8')
    mime_data.setData('application/x-mafw-pipeline-path', QByteArray(payload))

    # Drop on TargetGroup
    controller.add_group('G', [])
    controller.add_processor_to_run_list('G')
    model.set_pipeline(controller.build_pipeline())

    processors_idx_new = model.index(3, 0, root_idx)
    group_idx = model.index(1, 0, processors_idx_new)

    success = model.dropMimeData(mime_data, Qt.DropAction.MoveAction, -1, 0, group_idx)
    assert success
    assert len(controller.get_group_snapshot('G').processors) == 1


def test_drop_mime_data_more_errors(model: SteeringTreeModel, controller: SteeringController) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)

    # Valid mime data but item doesn't exist in model
    mime_data = QMimeData()
    payload = json.dumps([[999]]).encode('utf-8')
    mime_data.setData('application/x-mafw-pipeline-path', QByteArray(payload))
    assert not model.dropMimeData(mime_data, Qt.DropAction.MoveAction, 0, 0, processors_idx)


def test_drop_mime_data_multiple_paths(model: SteeringTreeModel, controller: SteeringController) -> None:
    root_idx = model.index(0, 0)
    model.index(3, 0, root_idx)

    controller.add_processor_to_run_list('P1')
    controller.add_processor_to_run_list('P2')
    controller.add_processor_to_run_list('P3')
    controller.add_group('TargetGroup', [])
    controller.add_processor_to_run_list('TargetGroup')
    model.set_pipeline(controller.build_pipeline())

    # New index for TargetGroup
    processors_idx_new = model.index(3, 0, root_idx)
    target_group_idx = model.index(3, 0, processors_idx_new)

    mime_data = QMimeData()
    payload = json.dumps([[0], [1]]).encode('utf-8')  # P1, P2
    mime_data.setData('application/x-mafw-pipeline-path', QByteArray(payload))

    # Drop P1, P2 into TargetGroup
    success = model.dropMimeData(mime_data, Qt.DropAction.MoveAction, -1, 0, target_group_idx)
    assert success
    assert controller.get_group_snapshot('TargetGroup').processors == ['P1', 'P2']
    assert controller.get_processors_to_run() == ['P3', 'TargetGroup']


def test_data_extra_roles(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    proc1_idx = model.index(0, 0, processors_idx)
    model.index(1, 0, processors_idx)

    # UserRole for section node
    assert model.data(processors_idx, Qt.ItemDataRole.UserRole) == 'processors'
    # UserRole for pipeline item
    assert model.data(proc1_idx, Qt.ItemDataRole.UserRole) == 'processors'

    # PipelineRoles.NAME role
    assert model.data(proc1_idx, 258) == 'Proc1'  # 258 is PipelineRoles.NAME (UserRole + 2)

    # DecorationRole for non-group pipeline item
    icon = model.data(proc1_idx, Qt.ItemDataRole.DecorationRole)
    assert isinstance(icon, (Qt.ItemDataRole.DecorationRole.__class__, object))  # It's a QIcon

    # Section node with invalid role
    assert model.data(processors_idx, Qt.ItemDataRole.SizeHintRole) is None
    # Pipeline item with invalid role
    assert model.data(proc1_idx, Qt.ItemDataRole.SizeHintRole) is None


def test_model_index_out_of_range(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    group_idx = model.index(1, 0, processors_idx)

    # PipelineItem row out of range
    assert not model.index(99, 0, group_idx).isValid()


def test_model_parent_null_and_unknown(model: SteeringTreeModel) -> None:
    # parent(None)
    assert model.parent(None) is None
    # parent(invalid)
    assert not model.parent(QModelIndex()).isValid()


def test_row_count_pipeline_item_and_default(model: SteeringTreeModel) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    group_idx = model.index(1, 0, processors_idx)

    # PipelineItem row count
    assert model.rowCount(group_idx) == 1  # GroupA has Proc3


def test_column_count_coverage(model: SteeringTreeModel) -> None:
    assert model.columnCount() == 1


def test_flags_coverage(model: SteeringTreeModel) -> None:
    # Invalid index
    assert model.flags(QModelIndex()) == Qt.ItemFlag.ItemIsEnabled

    # Group drop enabled flag
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    group_idx = model.index(1, 0, processors_idx)
    assert model.flags(group_idx) & Qt.ItemFlag.ItemIsDropEnabled


def test_mime_types_coverage(model: SteeringTreeModel) -> None:
    assert 'application/x-mafw-pipeline-path' in model.mimeTypes()


def test_mime_data_null_item(model: SteeringTreeModel) -> None:
    # Passing an index that pipeline_item_from_index returns None for
    root_idx = model.index(0, 0)
    mime = model.mimeData([root_idx])
    assert not mime.hasFormat('application/x-mafw-pipeline-path')


def test_drop_mime_data_unknown_parent(model: SteeringTreeModel) -> None:
    mime = QMimeData()
    mime.setData('application/x-mafw-pipeline-path', QByteArray(b'[[0]]'))
    # A leaf index that shouldn't accept drops
    root_idx = model.index(0, 0)
    globals_idx = model.index(0, 0, root_idx)
    assert not model.dropMimeData(mime, Qt.DropAction.MoveAction, 0, 0, globals_idx)


def test_drop_mime_data_type_error(model: SteeringTreeModel) -> None:
    # To hit line 295-296, we need raw.data() to return something that bytes() doesn't like
    from unittest.mock import MagicMock

    mock_mime = MagicMock(spec=QMimeData)
    mock_mime.hasFormat.return_value = True

    mock_raw = MagicMock()
    # bytes(None) raises TypeError
    mock_raw.data.return_value = None
    mock_mime.data.return_value = mock_raw

    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)
    assert not model.dropMimeData(mock_mime, Qt.DropAction.MoveAction, 0, 0, processors_idx)


def test_drop_mime_data_controller_error(model: SteeringTreeModel, controller: SteeringController) -> None:
    root_idx = model.index(0, 0)
    processors_idx = model.index(3, 0, root_idx)

    controller.add_processor_to_run_list('P1')
    model.set_pipeline(controller.build_pipeline())

    mime = QMimeData()
    mime.setData('application/x-mafw-pipeline-path', QByteArray(b'[[0]]'))

    from unittest.mock import patch

    with patch.object(controller, 'move_pipeline_entry', side_effect=SteeringControllerError('error')):
        assert not model.dropMimeData(mime, Qt.DropAction.MoveAction, 0, 0, processors_idx)


def test_path_for_item_value_error(model: SteeringTreeModel) -> None:
    # Triggering the ValueError in _path_for_item
    item = PipelineItem(config=ProcessorConfig(name='Rogue'))
    # Since it's not in the pipeline, root_items().index(current) will raise ValueError
    assert model._path_for_item(item) is None


def test_target_append_index_none(model: SteeringTreeModel) -> None:
    assert model._target_append_index(None) == len(model._pipeline_model.root_items())
