"""
TDD Tests for BezierGesture — Curve Mathematics.

Validates that Bézier curves produce non-linear, biomechanically
plausible touch paths with correct pressure profiles and timing.
"""

import pytest

from GramAddict.core.physics.biomechanics import BezierGesture, PhysicsBody


@pytest.fixture
def body_right():
    """Right-handed PhysicsBody with standard display."""
    PhysicsBody.reset()
    return PhysicsBody(handedness="right", device_info={"displayWidth": 1080, "displayHeight": 2400})


@pytest.fixture
def body_left():
    """Left-handed PhysicsBody."""
    PhysicsBody.reset()
    return PhysicsBody(handedness="left", device_info={"displayWidth": 1080, "displayHeight": 2400})


class TestScrollCurve:
    """Tests for BezierGesture.scroll_curve()."""

    def test_returns_correct_number_of_points(self, body_right):
        points = BezierGesture.scroll_curve((500, 1800), (500, 800), body_right, n_points=15)
        # n_points + 1 (inclusive of start and end)
        assert len(points) == 16

    def test_start_and_end_are_near_requested_positions(self, body_right):
        """Start/end points should be close to the requested coordinates (with micro-noise)."""
        start = (540, 1800)
        end = (540, 600)
        points = BezierGesture.scroll_curve(start, end, body_right, n_points=12)

        # First point near start (within noise tolerance)
        assert abs(points[0][0] - start[0]) < 20
        assert abs(points[0][1] - start[1]) < 20

        # Last point near end
        assert abs(points[-1][0] - end[0]) < 20
        assert abs(points[-1][1] - end[1]) < 20

    def test_path_is_non_linear(self, body_right):
        """The Bézier path should NOT be a straight line (thumb arc)."""
        start = (540, 1800)
        end = (540, 600)
        points = BezierGesture.scroll_curve(start, end, body_right, n_points=20)

        # Extract X coordinates of middle points
        mid_xs = [p[0] for p in points[5:15]]

        # A straight line would have all X ≈ 540. Bézier arc should deviate.
        max_deviation = max(abs(x - start[0]) for x in mid_xs)
        assert max_deviation > 3, (
            f"Expected non-linear path (arc deviation > 3px), got max deviation {max_deviation}px. "
            f"The curve is too straight — Bézier control points aren't applying."
        )

    def test_right_hander_arcs_right(self, body_right):
        """Right-handers should produce a rightward arc (positive X deviation)."""
        start = (540, 1800)
        end = (540, 600)

        # Run multiple times to get statistical average
        total_deviation = 0
        n_runs = 20
        for _ in range(n_runs):
            points = BezierGesture.scroll_curve(start, end, body_right, n_points=15)
            mid_xs = [p[0] for p in points[4:12]]
            avg_x = sum(mid_xs) / len(mid_xs)
            total_deviation += avg_x - start[0]

        avg_deviation = total_deviation / n_runs
        assert avg_deviation > 0, f"Right-hander should arc RIGHT (positive X), got avg deviation {avg_deviation:.1f}px"

    def test_left_hander_arcs_left(self, body_left):
        """Left-handers should produce a leftward arc (negative X deviation)."""
        start = (540, 1800)
        end = (540, 600)

        total_deviation = 0
        n_runs = 20
        for _ in range(n_runs):
            points = BezierGesture.scroll_curve(start, end, body_left, n_points=15)
            mid_xs = [p[0] for p in points[4:12]]
            avg_x = sum(mid_xs) / len(mid_xs)
            total_deviation += avg_x - start[0]

        avg_deviation = total_deviation / n_runs
        assert avg_deviation < 0, f"Left-hander should arc LEFT (negative X), got avg deviation {avg_deviation:.1f}px"

    def test_pressure_has_gaussian_peak(self, body_right):
        """Pressure should peak in the middle of the gesture (Gaussian profile)."""
        points = BezierGesture.scroll_curve((540, 1800), (540, 600), body_right, n_points=20)
        pressures = [p[2] for p in points]

        # Find the peak pressure index
        peak_idx = pressures.index(max(pressures))
        n = len(pressures)

        # Peak should be in the first half (around t=0.4 of the gesture)
        assert 2 <= peak_idx <= n * 0.7, (
            f"Pressure peak should be in the first 40-70% of the gesture, " f"but peaked at index {peak_idx}/{n}"
        )

    def test_pressure_within_valid_range(self, body_right):
        """All pressure values should be in [0.08, 0.92]."""
        points = BezierGesture.scroll_curve((540, 1800), (540, 600), body_right, n_points=20)
        for x, y, p in points:
            assert 0.08 <= p <= 0.92, f"Pressure {p} out of range at ({x}, {y})"

    def test_all_points_have_three_components(self, body_right):
        """Each point must be (x, y, pressure)."""
        points = BezierGesture.scroll_curve((540, 1800), (540, 600), body_right)
        for point in points:
            assert len(point) == 3, f"Expected (x, y, pressure), got {point}"


class TestTapCurve:
    """Tests for BezierGesture.tap_curve()."""

    def test_returns_three_points(self, body_right):
        """Tap curve should have exactly 3 points (down, contact, up)."""
        points = BezierGesture.tap_curve(500, 1000, body_right)
        assert len(points) == 3

    def test_micro_drift_is_small(self, body_right):
        """Tap drift should be tiny (< 15px from target)."""
        target_x, target_y = 500, 1000
        points = BezierGesture.tap_curve(target_x, target_y, body_right)

        for x, y, p in points:
            assert abs(x - target_x) < 20, f"X drift too large: {abs(x - target_x)}px"
            assert abs(y - target_y) < 20, f"Y drift too large: {abs(y - target_y)}px"

    def test_pressure_sequence_is_down_peak_up(self, body_right):
        """Pressure should follow: light → firm → light."""
        points = BezierGesture.tap_curve(500, 1000, body_right)
        p_down, p_full, p_up = [p[2] for p in points]

        assert p_full > p_down, "Full contact pressure should exceed touch-down"
        assert p_full > p_up, "Full contact pressure should exceed touch-up"


class TestHorizontalSwipeCurve:
    """Tests for BezierGesture.horizontal_swipe_curve()."""

    def test_returns_reasonable_point_count(self, body_right):
        points = BezierGesture.horizontal_swipe_curve((900, 1200), (200, 1200), body_right, n_points=10)
        assert len(points) == 11

    def test_horizontal_distance_is_correct_direction(self, body_right):
        """Swiping left should end with lower X than start."""
        points = BezierGesture.horizontal_swipe_curve((900, 1200), (200, 1200), body_right)
        assert points[-1][0] < points[0][0], "Horizontal swipe left should decrease X"

    def test_vertical_arc_exists(self, body_right):
        """Horizontal swipe should have a vertical arc (thumb drops during swipe)."""
        start_y = 1200
        total_y_deviation = 0
        n_runs = 15

        for _ in range(n_runs):
            points = BezierGesture.horizontal_swipe_curve((900, start_y), (200, start_y), body_right, n_points=12)
            mid_ys = [p[1] for p in points[3:9]]
            avg_y = sum(mid_ys) / len(mid_ys)
            total_y_deviation += abs(avg_y - start_y)

        avg_deviation = total_y_deviation / n_runs
        assert avg_deviation > 5, f"Expected vertical arc (deviation > 5px), got {avg_deviation:.1f}px"


class TestSigmoidTiming:
    """Tests for BezierGesture.compute_sigmoid_timing()."""

    def test_total_duration_matches(self):
        """Sum of intervals should approximately equal total duration."""
        total_ms = 300
        intervals = BezierGesture.compute_sigmoid_timing(15, total_ms)

        total_computed = sum(intervals) * 1000
        assert (
            abs(total_computed - total_ms) < total_ms * 0.15
        ), f"Total computed {total_computed:.0f}ms differs too much from {total_ms}ms"

    def test_edges_are_slower_than_middle(self):
        """Start and end intervals should be longer than middle intervals."""
        intervals = BezierGesture.compute_sigmoid_timing(20, 400)

        # Average of first 3 and last 3
        edge_avg = (sum(intervals[:3]) + sum(intervals[-3:])) / 6
        # Average of middle 6
        mid_start = len(intervals) // 2 - 3
        mid_avg = sum(intervals[mid_start : mid_start + 6]) / 6

        # Edge intervals should be slower (larger) — this validates the U-shape
        assert edge_avg > mid_avg * 0.8, (
            f"Expected U-shaped timing (edges slower), " f"edge_avg={edge_avg:.4f}, mid_avg={mid_avg:.4f}"
        )

    def test_single_point_returns_single_interval(self):
        intervals = BezierGesture.compute_sigmoid_timing(1, 100)
        assert len(intervals) == 1
        assert abs(intervals[0] - 0.1) < 0.02

    def test_no_negative_intervals(self):
        """All intervals must be positive."""
        intervals = BezierGesture.compute_sigmoid_timing(25, 200)
        for i, interval in enumerate(intervals):
            assert interval > 0, f"Interval {i} is non-positive: {interval}"
