# Unit tests for ax_utilities_component.py methods.
#
# Copyright 2026 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.

# pylint: disable=import-outside-toplevel
# pylint: disable=too-many-arguments
# pylint: disable=too-many-positional-arguments
# pylint: disable=protected-access

"""Unit tests for ax_utilities_component.py methods."""

from __future__ import annotations

from typing import TYPE_CHECKING

import gi
import pytest

gi.require_version("Atspi", "2.0")
from gi.repository import Atspi

if TYPE_CHECKING:
    from unittest.mock import MagicMock

    from .orca_test_context import OrcaTestContext


@pytest.mark.unit
class TestAXUtilitiesComponent:
    """Test AXUtilitiesComponent class methods."""

    def _setup_dependencies(self, test_context: OrcaTestContext) -> dict[str, MagicMock]:
        """Set up mocks for ax_utilities_component dependencies."""

        additional_modules = ["orca.ax_utilities_role", "orca.ax_component"]
        essential_modules = test_context.setup_shared_dependencies(additional_modules)

        debug_mock = essential_modules["orca.debug"]
        debug_mock.print_message = test_context.Mock()
        debug_mock.print_tokens = test_context.Mock()
        debug_mock.LEVEL_INFO = 800

        ax_object_class_mock = test_context.Mock()
        ax_object_class_mock.supports_component = test_context.Mock(return_value=True)
        ax_object_class_mock.is_bogus = test_context.Mock(return_value=False)
        ax_object_class_mock.get_child_count = test_context.Mock(return_value=0)
        ax_object_class_mock.get_parent = test_context.Mock(return_value=None)
        ax_object_class_mock.get_index_in_parent = test_context.Mock(return_value=0)
        ax_object_class_mock.clear_cache = test_context.Mock()
        essential_modules["orca.ax_object"].AXObject = ax_object_class_mock

        role_class_mock = test_context.Mock()
        role_class_mock.is_menu = test_context.Mock(return_value=False)
        essential_modules["orca.ax_utilities_role"].AXUtilitiesRole = role_class_mock

        ax_component_class_mock = test_context.Mock()
        ax_component_class_mock.get_rect = test_context.Mock()
        ax_component_class_mock.object_contains_point = test_context.Mock(return_value=False)
        ax_component_class_mock.get_object_at_point = test_context.Mock(return_value=None)
        essential_modules["orca.ax_component"].AXComponent = ax_component_class_mock

        return essential_modules

    def test_get_center_point(self, test_context: OrcaTestContext) -> None:
        """Test AXUtilitiesComponent.get_center_point."""

        mock_accessible = test_context.Mock(spec=Atspi.Accessible)
        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        mock_rect = Atspi.Rect()
        mock_rect.x = 10
        mock_rect.y = 20
        mock_rect.width = 100
        mock_rect.height = 60
        essential_modules["orca.ax_component"].AXComponent.get_rect.return_value = mock_rect
        result = AXUtilitiesComponent.get_center_point(mock_accessible)
        assert result == (60.0, 50.0)

    @pytest.mark.parametrize(
        "width, height, expected_result",
        [
            pytest.param(0, 0, True, id="zero_width_and_height"),
            pytest.param(100, 0, False, id="zero_height_only"),
            pytest.param(0, 50, False, id="zero_width_only"),
            pytest.param(100, 50, False, id="non_zero_dimensions"),
        ],
    )
    def test_has_no_size(
        self,
        test_context: OrcaTestContext,
        width: int,
        height: int,
        expected_result: bool,
    ) -> None:
        """Test AXUtilitiesComponent.has_no_size."""

        mock_accessible = test_context.Mock(spec=Atspi.Accessible)
        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        mock_rect = Atspi.Rect()
        mock_rect.width = width
        mock_rect.height = height
        essential_modules["orca.ax_component"].AXComponent.get_rect.return_value = mock_rect
        result = AXUtilitiesComponent.has_no_size(mock_accessible)
        assert result is expected_result

    @pytest.mark.parametrize(
        "x, y, width, height, expected_result",
        [
            pytest.param(0, 0, 0, 0, True, id="all_zero_dimensions"),
            pytest.param(10, 20, 0, 0, True, id="zero_size_non_zero_position"),
            pytest.param(-1, -1, -1, -1, True, id="all_negative_one"),
            pytest.param(10, 20, 100, 50, False, id="valid_dimensions"),
            pytest.param(10, 20, -2, 50, True, id="invalid_negative_width"),
            pytest.param(10, 20, 100, -5, True, id="invalid_negative_height"),
        ],
    )
    def test_has_no_size_or_invalid_rect(
        self,
        test_context: OrcaTestContext,
        x: int,
        y: int,
        width: int,
        height: int,
        expected_result: bool,
    ) -> None:
        """Test AXUtilitiesComponent.has_no_size_or_invalid_rect."""

        mock_accessible = test_context.Mock(spec=Atspi.Accessible)
        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        mock_rect = Atspi.Rect()
        mock_rect.x = x
        mock_rect.y = y
        mock_rect.width = width
        mock_rect.height = height
        essential_modules["orca.ax_component"].AXComponent.get_rect.return_value = mock_rect
        result = AXUtilitiesComponent.has_no_size_or_invalid_rect(mock_accessible)
        assert result is expected_result

    @pytest.mark.parametrize(
        "x, y, width, height, expected_result",
        [
            pytest.param(0, 0, 0, 0, True, id="all_zeros"),
            pytest.param(0, 0, 100, 50, False, id="zero_position_valid_size"),
            pytest.param(10, 20, 0, 0, False, id="valid_position_zero_size"),
            pytest.param(10, 20, 100, 50, False, id="all_non_zero"),
        ],
    )
    def test_is_empty_rect(
        self,
        test_context: OrcaTestContext,
        x: int,
        y: int,
        width: int,
        height: int,
        expected_result: bool,
    ) -> None:
        """Test AXUtilitiesComponent.is_empty_rect."""

        self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        rect = Atspi.Rect()
        rect.x = x
        rect.y = y
        rect.width = width
        rect.height = height
        result = AXUtilitiesComponent.is_empty_rect(rect)
        assert result is expected_result

    def test_is_same_rect(self, test_context: OrcaTestContext) -> None:
        """Test AXUtilitiesComponent.is_same_rect."""

        self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        rect1 = Atspi.Rect()
        rect1.x = 10
        rect1.y = 20
        rect1.width = 100
        rect1.height = 50

        rect2 = Atspi.Rect()
        rect2.x = 10
        rect2.y = 20
        rect2.width = 100
        rect2.height = 50
        assert AXUtilitiesComponent.is_same_rect(rect1, rect2) is True

        rect2.x = 15
        assert AXUtilitiesComponent.is_same_rect(rect1, rect2) is False

        rect2.x = 10
        rect2.width = 200
        assert AXUtilitiesComponent.is_same_rect(rect1, rect2) is False

    @pytest.mark.parametrize(
        "rect1_coords, rect2_coords, pixel_delta, expected",
        [
            pytest.param((10, 20, 100, 30), (10, 20, 100, 30), 5, True, id="same_rects"),
            pytest.param(
                (50, 35, 0, 0),
                (10, 20, 100, 30),
                5,
                True,
                id="point_in_rect_vertical_range",
            ),
            pytest.param((50, 10, 0, 0), (10, 20, 100, 30), 5, False, id="point_above_rect"),
            pytest.param((10, 20, 100, 30), (50, 35, 0, 0), 5, True, id="rect_contains_point"),
            pytest.param((10, 20, 100, 30), (10, 100, 100, 30), 5, False, id="no_vertical_overlap"),
            pytest.param((10, 20, 100, 30), (10, 22, 100, 30), 5, True, id="overlapping_same_line"),
            pytest.param((10, 20, 100, 30), (10, 40, 100, 30), 5, False, id="midpoints_too_far"),
            pytest.param(
                (10, 20, 100, 30),
                (10, 40, 100, 30),
                20,
                True,
                id="midpoints_within_larger_delta",
            ),
        ],
    )
    def test_rects_are_on_same_line(
        self,
        test_context: OrcaTestContext,
        rect1_coords: tuple[int, int, int, int],
        rect2_coords: tuple[int, int, int, int],
        pixel_delta: int,
        expected: bool,
    ) -> None:
        """Test AXUtilitiesComponent.rects_are_on_same_line."""

        self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        rect1 = Atspi.Rect()
        rect1.x, rect1.y, rect1.width, rect1.height = rect1_coords
        rect2 = Atspi.Rect()
        rect2.x, rect2.y, rect2.width, rect2.height = rect2_coords

        result = AXUtilitiesComponent.rects_are_on_same_line(rect1, rect2, pixel_delta)
        assert result is expected

    def test_get_rect_intersection(self, test_context: OrcaTestContext) -> None:
        """Test AXUtilitiesComponent.get_rect_intersection."""

        self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        rect1 = Atspi.Rect()
        rect1.x = 10
        rect1.y = 20
        rect1.width = 100
        rect1.height = 50
        rect2 = Atspi.Rect()
        rect2.x = 50
        rect2.y = 30
        rect2.width = 80
        rect2.height = 60
        result = AXUtilitiesComponent.get_rect_intersection(rect1, rect2)
        assert result.x == 50
        assert result.y == 30
        assert result.width == 60
        assert result.height == 40

    @pytest.mark.parametrize(
        "case",
        [
            {
                "id": "no_overlap",
                "rect1_coords": (10, 20, 50, 30),
                "rect2_coords": (100, 200, 50, 30),
                "expected_result": (0, 0, 0, 0),
            },
            {
                "id": "huge_rect",
                "rect1_coords": (100, 100, 200, 150),
                "rect2_coords": (50, 50, 2147483647, 2147483647),
                "expected_result": (100, 100, 200, 150),
            },
        ],
        ids=lambda case: case["id"],
    )
    def test_get_rect_intersection_scenarios(
        self,
        test_context: OrcaTestContext,
        case: dict,
    ) -> None:
        """Test AXUtilitiesComponent.get_rect_intersection with different rectangle scenarios."""

        self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        rect1 = Atspi.Rect()
        rect1.x, rect1.y, rect1.width, rect1.height = case["rect1_coords"]
        rect2 = Atspi.Rect()
        rect2.x, rect2.y, rect2.width, rect2.height = case["rect2_coords"]

        result = AXUtilitiesComponent.get_rect_intersection(rect1, rect2)
        expected_x, expected_y, expected_width, expected_height = case["expected_result"]

        assert result.x == expected_x
        assert result.y == expected_y
        assert result.width == expected_width
        assert result.height == expected_height

    @pytest.mark.parametrize(
        "obj_coords,test_coords,expected_result",
        [
            pytest.param((10, 20, 100, 50), (50, 30, 80, 60), True, id="with_intersection"),
            pytest.param((10, 20, 50, 30), (100, 200, 50, 30), False, id="no_intersection"),
        ],
    )
    def test_object_intersects_rect(
        self,
        test_context: OrcaTestContext,
        obj_coords,
        test_coords,
        expected_result,
    ) -> None:
        """Test AXUtilitiesComponent.object_intersects_rect intersection scenarios."""

        mock_accessible = test_context.Mock(spec=Atspi.Accessible)
        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        obj_rect = Atspi.Rect()
        obj_rect.x, obj_rect.y, obj_rect.width, obj_rect.height = obj_coords
        test_rect = Atspi.Rect()
        test_rect.x, test_rect.y, test_rect.width, test_rect.height = test_coords

        essential_modules["orca.ax_component"].AXComponent.get_rect.return_value = obj_rect
        result = AXUtilitiesComponent.object_intersects_rect(mock_accessible, test_rect)
        assert result is expected_result

    @pytest.mark.parametrize(
        "x, y, width, height, child_count, is_menu, expected_result",
        [
            pytest.param(15000, 20, 100, 50, 0, False, True, id="extreme_x_position"),
            pytest.param(20, 15000, 100, 50, 0, False, True, id="extreme_y_position"),
            pytest.param(20, 30, 0, 0, 0, False, True, id="zero_size_no_children"),
            pytest.param(20, 30, 0, 0, 2, False, False, id="zero_size_has_children"),
            pytest.param(20, 30, 0, 0, 0, True, True, id="zero_size_menu_role"),
            pytest.param(-50, -60, 40, 30, 0, False, True, id="completely_offscreen"),
            pytest.param(20, 30, 100, 50, 0, False, False, id="normal_onscreen_object"),
        ],
    )
    def test_object_is_off_screen(
        self,
        test_context: OrcaTestContext,
        x: int,
        y: int,
        width: int,
        height: int,
        child_count: int,
        is_menu: bool,
        expected_result: bool,
    ) -> None:
        """Test AXUtilitiesComponent.object_is_off_screen."""

        mock_accessible = test_context.Mock(spec=Atspi.Accessible)
        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)

        essential_modules["orca.ax_object"].AXObject.get_child_count = test_context.Mock(
            return_value=child_count,
        )
        essential_modules["orca.ax_utilities_role"].AXUtilitiesRole.is_menu = test_context.Mock(
            return_value=is_menu,
        )
        from orca.ax_utilities_component import AXUtilitiesComponent

        mock_rect = Atspi.Rect()
        mock_rect.x = x
        mock_rect.y = y
        mock_rect.width = width
        mock_rect.height = height
        essential_modules["orca.ax_component"].AXComponent.get_rect.return_value = mock_rect
        result = AXUtilitiesComponent.object_is_off_screen(mock_accessible)
        assert result is expected_result

    def test_objects_have_same_rect(self, test_context: OrcaTestContext) -> None:
        """Test AXUtilitiesComponent.objects_have_same_rect."""

        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        obj1 = test_context.Mock(spec=Atspi.Accessible)
        obj2 = test_context.Mock(spec=Atspi.Accessible)
        rect1 = Atspi.Rect()
        rect1.x = 10
        rect1.y = 20
        rect1.width = 100
        rect1.height = 50
        rect2 = Atspi.Rect()
        rect2.x = 10
        rect2.y = 20
        rect2.width = 100
        rect2.height = 50

        def get_rect_for_obj(obj):
            return rect1 if obj == obj1 else rect2

        essential_modules["orca.ax_component"].AXComponent.get_rect.side_effect = get_rect_for_obj

        result = AXUtilitiesComponent.objects_have_same_rect(obj1, obj2)
        assert result is True

        rect2.width = 200
        result = AXUtilitiesComponent.objects_have_same_rect(obj1, obj2)
        assert result is False

    def test_objects_overlap(self, test_context: OrcaTestContext) -> None:
        """Test AXUtilitiesComponent.objects_overlap."""

        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        obj1 = test_context.Mock(spec=Atspi.Accessible)
        obj2 = test_context.Mock(spec=Atspi.Accessible)
        rect1 = Atspi.Rect()
        rect1.x = 10
        rect1.y = 20
        rect1.width = 100
        rect1.height = 50
        rect2 = Atspi.Rect()
        rect2.x = 50
        rect2.y = 30
        rect2.width = 80
        rect2.height = 60

        def get_rect_for_obj(obj):
            return rect1 if obj == obj1 else rect2

        essential_modules["orca.ax_component"].AXComponent.get_rect.side_effect = get_rect_for_obj
        result = AXUtilitiesComponent.objects_overlap(obj1, obj2)
        assert result is True

    def test_on_same_line(self, test_context: OrcaTestContext) -> None:
        """Test AXUtilitiesComponent.on_same_line."""

        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        obj1 = test_context.Mock(spec=Atspi.Accessible)
        obj2 = test_context.Mock(spec=Atspi.Accessible)

        rect1 = Atspi.Rect()
        rect1.x = 10
        rect1.y = 20
        rect1.width = 50
        rect1.height = 30
        rect2 = Atspi.Rect()
        rect2.x = 100
        rect2.y = 22
        rect2.width = 60
        rect2.height = 26

        def get_rect_for_obj(obj):
            return rect1 if obj == obj1 else rect2

        essential_modules["orca.ax_component"].AXComponent.get_rect.side_effect = get_rect_for_obj
        result = AXUtilitiesComponent.on_same_line(obj1, obj2)
        assert result is True

        rect2.height = 200
        result = AXUtilitiesComponent.on_same_line(obj1, obj2)
        assert result is False

    def test_on_same_line_with_delta(self, test_context: OrcaTestContext) -> None:
        """Test AXUtilitiesComponent.on_same_line with delta parameter."""

        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        obj1 = test_context.Mock(spec=Atspi.Accessible)
        obj2 = test_context.Mock(spec=Atspi.Accessible)
        rect1 = Atspi.Rect()
        rect1.x = 10
        rect1.y = 20
        rect1.width = 50
        rect1.height = 20
        rect2 = Atspi.Rect()
        rect2.x = 100
        rect2.y = 30
        rect2.width = 60
        rect2.height = 20

        def get_rect_for_obj(obj):
            return rect1 if obj == obj1 else rect2

        essential_modules["orca.ax_component"].AXComponent.get_rect.side_effect = get_rect_for_obj

        result = AXUtilitiesComponent.on_same_line(obj1, obj2, delta=5)
        assert result is False

        result = AXUtilitiesComponent.on_same_line(obj1, obj2, delta=15)
        assert result is True

    def test_sort_objects_by_size(self, test_context: OrcaTestContext) -> None:
        """Test AXUtilitiesComponent.sort_objects_by_size."""

        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        obj_small = test_context.Mock(spec=Atspi.Accessible)
        obj_medium = test_context.Mock(spec=Atspi.Accessible)
        obj_large = test_context.Mock(spec=Atspi.Accessible)

        rect_small = Atspi.Rect()
        rect_small.width = 10
        rect_small.height = 10
        rect_medium = Atspi.Rect()
        rect_medium.width = 20
        rect_medium.height = 15
        rect_large = Atspi.Rect()
        rect_large.width = 30
        rect_large.height = 20

        def get_rect_for_obj(obj):
            if obj == obj_small:
                return rect_small
            if obj == obj_medium:
                return rect_medium
            return rect_large

        essential_modules["orca.ax_component"].AXComponent.get_rect.side_effect = get_rect_for_obj
        objects = [obj_large, obj_small, obj_medium]
        result = AXUtilitiesComponent.sort_objects_by_size(objects)
        assert result == [obj_small, obj_medium, obj_large]

    def test_sort_objects_by_position(self, test_context: OrcaTestContext) -> None:
        """Test AXUtilitiesComponent.sort_objects_by_position."""

        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
        from orca.ax_utilities_component import AXUtilitiesComponent

        obj_top_left = test_context.Mock(spec=Atspi.Accessible)
        obj_top_right = test_context.Mock(spec=Atspi.Accessible)
        obj_bottom_left = test_context.Mock(spec=Atspi.Accessible)
        rect_top_left = Atspi.Rect()
        rect_top_left.x = 10
        rect_top_left.y = 10
        rect_top_right = Atspi.Rect()
        rect_top_right.x = 100
        rect_top_right.y = 10
        rect_bottom_left = Atspi.Rect()
        rect_bottom_left.x = 10
        rect_bottom_left.y = 100

        def get_rect_for_obj(obj):
            if obj == obj_top_left:
                return rect_top_left
            if obj == obj_top_right:
                return rect_top_right
            return rect_bottom_left

        essential_modules["orca.ax_component"].AXComponent.get_rect.side_effect = get_rect_for_obj
        objects = [obj_bottom_left, obj_top_right, obj_top_left]
        result = AXUtilitiesComponent.sort_objects_by_position(objects)
        assert result == [obj_top_left, obj_top_right, obj_bottom_left]

    def test_sort_objects_by_position_same_coordinates(self, test_context: OrcaTestContext) -> None:
        """Test AXUtilitiesComponent.sort_objects_by_position when objects have same coordinates."""

        essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)

        obj1 = test_context.Mock(spec=Atspi.Accessible)
        obj2 = test_context.Mock(spec=Atspi.Accessible)
        parent = test_context.Mock(spec=Atspi.Accessible)

        essential_modules["orca.ax_object"].AXObject.get_parent = test_context.Mock(
            return_value=parent,
        )
        essential_modules["orca.ax_object"].AXObject.get_index_in_parent = test_context.Mock(
            side_effect=lambda obj: 0 if obj == obj1 else 1,
        )
        from orca.ax_utilities_component import AXUtilitiesComponent

        same_rect = Atspi.Rect()
        same_rect.x = 50
        same_rect.y = 50
        essential_modules["orca.ax_component"].AXComponent.get_rect.return_value = same_rect
        objects = [obj2, obj1]
        result = AXUtilitiesComponent.sort_objects_by_position(objects)
        assert result == [obj1, obj2]
