"""
TDD: Physics Module Tests.

Tests the extracted humanized input functions in isolation,
verifying they produce valid device interactions via the
biomechanical gesture pipeline (BezierGesture → SendEventInjector).

These tests mock the SendEventInjector at the injection boundary to
validate that the humanized functions correctly generate gesture data
and delegate to the injector.
"""

from unittest.mock import MagicMock, patch

import pytest

from GramAddict.core.physics.biomechanics import PhysicsBody


@pytest.fixture(autouse=True)
def reset_singletons():
    """Reset singletons between tests for isolation."""
    PhysicsBody.reset()
    from GramAddict.core.physics.sendevent_injector import SendEventInjector

    SendEventInjector.reset()
    yield
    PhysicsBody.reset()
    SendEventInjector.reset()


@pytest.fixture
def device():
    """Mock Android device."""
    dev = MagicMock()
    dev.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
    dev.cm_to_pixels.return_value = 5
    dev.shell = MagicMock(return_value="")
    return dev


class TestHumanizedScroll:
    """Scroll must produce valid gesture injection calls."""

    @patch("GramAddict.core.physics.humanized_input.SendEventInjector")
    def test_scroll_calls_injector(self, MockInjector, device):
        """Scroll must call the injector's inject_gesture method."""
        mock_injector = MagicMock()
        MockInjector.get_instance.return_value = mock_injector

        from GramAddict.core.physics.humanized_input import humanized_scroll

        humanized_scroll(device)

        mock_injector.inject_gesture.assert_called()
        args = mock_injector.inject_gesture.call_args
        points = args[0][0]
        args[0][1]

        # Validate gesture data structure
        assert len(points) >= 5, f"Expected at least 5 points, got {len(points)}"
        for p in points:
            assert len(p) == 3, f"Each point must be (x, y, pressure), got {p}"

    @patch("GramAddict.core.physics.humanized_input.SendEventInjector")
    def test_scroll_coordinates_within_screen(self, MockInjector, device):
        """All scroll coordinates must be within screen bounds."""
        mock_injector = MagicMock()
        MockInjector.get_instance.return_value = mock_injector

        from GramAddict.core.physics.humanized_input import humanized_scroll

        for _ in range(20):
            mock_injector.inject_gesture.reset_mock()
            humanized_scroll(device)

            args = mock_injector.inject_gesture.call_args
            points = args[0][0]
            for x, y, p in points:
                assert 0 <= x <= 1200, f"x={x} out of bounds"
                assert 0 <= y <= 2500, f"y={y} out of bounds"

    @patch("GramAddict.core.physics.humanized_input.SendEventInjector")
    def test_skip_scroll_calls_injector(self, MockInjector, device):
        """Skip scroll should also use the injector."""
        import random

        random.seed(42)
        mock_injector = MagicMock()
        MockInjector.get_instance.return_value = mock_injector

        from GramAddict.core.physics.humanized_input import humanized_scroll

        humanized_scroll(device, is_skip=True)

        mock_injector.inject_gesture.assert_called()


class TestHumanizedClick:
    """Click must produce valid gesture injection calls."""

    @patch("GramAddict.core.physics.humanized_input.SendEventInjector")
    def test_single_tap_calls_injector_once(self, MockInjector, device):
        """Single tap should call injector exactly once."""
        mock_injector = MagicMock()
        MockInjector.get_instance.return_value = mock_injector

        from GramAddict.core.physics.humanized_input import humanized_click

        humanized_click(device, 500, 1200)

        assert mock_injector.inject_gesture.call_count == 1

    @patch("GramAddict.core.physics.humanized_input.SendEventInjector")
    def test_double_tap_calls_injector_twice(self, MockInjector, device):
        """Double tap uses device.shell for timing-critical sub-300ms double-tap."""
        mock_injector = MagicMock()
        MockInjector.get_instance.return_value = mock_injector

        from GramAddict.core.physics.humanized_input import humanized_click

        humanized_click(device, 500, 1200, double=True)

        # Double-tap bypasses SendEventInjector and uses shell for timing precision
        device.shell.assert_called_once()
        shell_cmd = device.shell.call_args[0][0]
        assert "input tap" in shell_cmd, f"Expected 'input tap' in shell command, got: {shell_cmd}"

    @patch("GramAddict.core.physics.humanized_input.SendEventInjector")
    def test_tap_has_jitter(self, MockInjector, device):
        """Taps should have slight jitter (not exact coordinates)."""
        import random

        random.seed(1)
        mock_injector = MagicMock()
        MockInjector.get_instance.return_value = mock_injector

        from GramAddict.core.physics.humanized_input import humanized_click

        humanized_click(device, 500, 1200)

        args = mock_injector.inject_gesture.call_args
        points = args[0][0]
        # First point should be near but not exactly (500, 1200)
        x, y, p = points[0]
        assert 475 <= x <= 525, f"Tap X {x} too far from target 500"
        assert 1175 <= y <= 1225, f"Tap Y {y} too far from target 1200"


class TestHumanizedHorizontalSwipe:
    """Horizontal swipe must produce valid gesture injection calls."""

    @patch("GramAddict.core.physics.humanized_input.SendEventInjector")
    def test_horizontal_swipe_calls_injector(self, MockInjector, device):
        """Horizontal swipe should call injector once."""
        mock_injector = MagicMock()
        MockInjector.get_instance.return_value = mock_injector

        from GramAddict.core.physics.humanized_input import humanized_horizontal_swipe

        humanized_horizontal_swipe(device, 800, 200, 1200, 250)

        mock_injector.inject_gesture.assert_called_once()

    @patch("GramAddict.core.physics.humanized_input.SendEventInjector")
    def test_horizontal_swipe_has_arc(self, MockInjector, device):
        """Horizontal swipe points should have Y-axis variation (thumb arc)."""
        mock_injector = MagicMock()
        MockInjector.get_instance.return_value = mock_injector

        from GramAddict.core.physics.humanized_input import humanized_horizontal_swipe

        humanized_horizontal_swipe(device, 800, 200, 1200, 250)

        args = mock_injector.inject_gesture.call_args
        points = args[0][0]
        ys = [p[1] for p in points]
        # Y values should NOT all be identical (thumb arc produces variation)
        unique_ys = set(ys)
        assert len(unique_ys) > 1, "Expected Y-axis variation from thumb arc"
