"""
Tier 1: Feature Coverage — Kinematics, Isometric Math, Wall-Sliding & Camera.
Covers Features 3, 4, 5, 6 from PROJECT.md Feature Inventory:
- Feature 3: 2.5D Isometric Math & Coordinates
- Feature 4: Zero-Residual Momentum Kinematics
- Feature 5: Wall-Sliding & A* Pathfinding
- Feature 6: Camera Lerp & Dynamic Zoom
"""

from __future__ import annotations

import math
from typing import Tuple
import pytest

from tests.e2e_cocos.conftest import (
    TILE_H,
    TILE_W,
    PLAYER_SPEED,
    SimulatedCocosPlayer,
    iso_to_world,
    resolve_heading_bin,
    world_to_iso,
)


class TestTier1IsometricMath:
    """Feature 3: 2.5D Isometric Math & Coordinates (64x32 2:1 ratio)."""

    def test_t1_f3_origin_projection_at_viewport_center(self) -> None:
        sx, sy = world_to_iso(0.0, 0.0, cx=0.0, cy=0.0, vx=640.0, vy=360.0)
        assert sx == 640.0
        assert sy == 360.0

    def test_t1_f3_standard_tile_ratio_diamond(self) -> None:
        # Move 1 unit along X: relative +1,-0 -> sx = +32, sy = +16
        sx1, sy1 = world_to_iso(1.0, 0.0)
        assert sx1 == 32.0
        assert sy1 == 16.0
        # Ratio sx / sy must be strictly 2.0 (2:1 classic isometric)
        assert sx1 / sy1 == 2.0

    def test_t1_f3_elevation_z_offset(self) -> None:
        # wz = 2.0 -> vertical lift of 2.0 * 24 = 48 px upward
        sx0, sy0 = world_to_iso(5.0, 5.0, wz=0.0)
        sx1, sy1 = world_to_iso(5.0, 5.0, wz=2.0)
        assert sx0 == sx1
        assert sy0 - sy1 == 48.0

    def test_t1_f3_roundtrip_precision_zero_loss(self) -> None:
        test_coords = [(12.34, -5.67), (-45.0, 99.12), (0.001, -0.002), (100.0, 200.0)]
        for wx, wy in test_coords:
            sx, sy = world_to_iso(wx, wy, cx=10.0, cy=-20.0, vx=960.0, vy=540.0, zoom=1.25)
            rev_x, rev_y = iso_to_world(sx, sy, cx=10.0, cy=-20.0, vx=960.0, vy=540.0, zoom=1.25)
            assert math.isclose(wx, rev_x, abs_tol=1e-5)
            assert math.isclose(wy, rev_y, abs_tol=1e-5)

    def test_t1_f3_zoom_scaling_uniformity(self) -> None:
        sx1, sy1 = world_to_iso(2.0, 1.0, zoom=1.0)
        sx2, sy2 = world_to_iso(2.0, 1.0, zoom=1.5)
        assert math.isclose(sx2, sx1 * 1.5, abs_tol=1e-5)
        assert math.isclose(sy2, sy1 * 1.5, abs_tol=1e-5)

    def test_t1_f3_screen_to_world_unprojection_quadrants(self) -> None:
        # Screen point (320, 160) from center (0,0) unprojects to positive world coords
        wx, wy = iso_to_world(32.0, 16.0)
        assert math.isclose(wx, 1.0, abs_tol=1e-5)
        assert math.isclose(wy, 0.0, abs_tol=1e-5)


class TestTier1ZeroResidualMomentum:
    """Feature 4: Zero-Residual Momentum Kinematics."""

    def test_t1_f4_instant_digital_brake_frame_zero(self, simulated_player: SimulatedCocosPlayer) -> None:
        # Move forward for 5 frames
        for _ in range(5):
            simulated_player.apply_input(1.0, 0.0, dt=1.0 / 60.0)
        assert simulated_player.vx > 0.0
        assert simulated_player.anim_state == "run"

        # Release input (mag = 0.0)
        simulated_player.apply_input(0.0, 0.0, dt=1.0 / 60.0)
        assert simulated_player.vx == 0.0
        assert simulated_player.vy == 0.0
        assert simulated_player.anim_state == "idle"

    def test_t1_f4_deadzone_threshold_guard(self, simulated_player: SimulatedCocosPlayer) -> None:
        # Input below mag threshold 0.05 must be treated as deadzone stop
        simulated_player.apply_input(0.03, 0.02, dt=1.0 / 60.0)
        assert simulated_player.vx == 0.0
        assert simulated_player.vy == 0.0
        assert simulated_player.anim_state == "idle"

    def test_t1_f4_snap_freeze_rotation_angle(self, simulated_player: SimulatedCocosPlayer) -> None:
        # Moving north-east gives heading NE
        simulated_player.apply_input(1.0, -1.0, dt=0.016)
        heading_before = simulated_player.heading
        assert heading_before in ("E", "NE", "N")

        # After releasing, heading must remain frozen, not reset or drift
        simulated_player.apply_input(0.0, 0.0, dt=0.016)
        assert simulated_player.heading == heading_before

    def test_t1_f4_constant_velocity_during_continuous_input(self, simulated_player: SimulatedCocosPlayer) -> None:
        simulated_player.apply_input(0.0, 1.0, dt=0.1)
        expected_disp = PLAYER_SPEED * 0.1
        assert math.isclose(simulated_player.wy, expected_disp, abs_tol=1e-4)

    def test_t1_f4_diagonal_normalization_equal_speed(self, simulated_player: SimulatedCocosPlayer) -> None:
        # Diagonal input (1.0, 1.0) must be normalized so total speed == PLAYER_SPEED
        simulated_player.apply_input(1.0, 1.0, dt=0.1)
        speed = math.hypot(simulated_player.vx, simulated_player.vy)
        assert math.isclose(speed, PLAYER_SPEED, abs_tol=1e-4)

    def test_t1_f4_instant_reversal_without_skid(self, simulated_player: SimulatedCocosPlayer) -> None:
        simulated_player.apply_input(1.0, 0.0, dt=0.016)
        assert simulated_player.vx > 0.0
        # Immediately reverse direction
        simulated_player.apply_input(-1.0, 0.0, dt=0.016)
        assert simulated_player.vx < 0.0


class TestTier1WallSlidingAndPathfinding:
    """Feature 5: Wall-Sliding & A* Pathfinding."""

    def _simulate_wall_slide(
        self, wx: float, wy: float, dx: float, dy: float, wall_x: float, wall_y: float, r: float = 0.35,
    ) -> Tuple[float, float]:
        """Simulates 2-axis circle-AABB wall sliding algorithm."""
        def is_blocked(x: float, y: float) -> bool:
            cx = max(wall_x, min(x, wall_x + 1.0))
            cy = max(wall_y, min(y, wall_y + 1.0))
            return (x - cx) ** 2 + (y - cy) ** 2 < r ** 2

        # 1. Test direct destination
        if not is_blocked(wx + dx, wy + dy):
            return wx + dx, wy + dy
        # 2. Test X-axis slide
        x_clear = not is_blocked(wx + dx, wy)
        # 3. Test Y-axis slide
        y_clear = not is_blocked(wx, wy + dy)

        if x_clear and y_clear:
            if abs(dx) >= abs(dy):
                return wx + dx, wy
            return wx, wy + dy
        if x_clear:
            return wx + dx, wy
        if y_clear:
            return wx, wy + dy
        return wx, wy

    def test_t1_f5_free_movement_when_unobstructed(self) -> None:
        nx, ny = self._simulate_wall_slide(0.0, 0.0, 1.0, 1.0, wall_x=10.0, wall_y=10.0)
        assert (nx, ny) == (1.0, 1.0)

    def test_t1_f5_x_axis_slide_against_horizontal_wall(self) -> None:
        # Moving diagonal into wall located at (0, 1) -> Y is blocked, slides along X
        nx, ny = self._simulate_wall_slide(0.5, 0.5, 0.2, 0.3, wall_x=0.0, wall_y=1.0)
        assert nx == 0.7
        assert ny == 0.5

    def test_t1_f5_y_axis_slide_against_vertical_wall(self) -> None:
        # Moving diagonal into wall at (1, 0) -> X is blocked, slides along Y
        nx, ny = self._simulate_wall_slide(0.5, 0.5, 0.3, 0.2, wall_x=1.0, wall_y=0.0)
        assert nx == 0.5
        assert ny == 0.7

    def test_t1_f5_dominant_axis_selection_prevents_stick(self) -> None:
        # When both single axes are clear, pick dominant axis
        nx, ny = self._simulate_wall_slide(0.5, 0.5, 0.4, 0.1, wall_x=1.0, wall_y=1.0)
        assert nx == 0.9

    def test_t1_f5_corner_stop_when_both_axes_blocked(self) -> None:
        # Both axes blocked -> stays in place
        nx, ny = self._simulate_wall_slide(0.9, 0.9, 0.2, 0.2, wall_x=1.0, wall_y=1.0)
        assert nx == 0.9 and ny == 0.9

    def test_t1_f5_dda_supercover_los_raycast(self) -> None:
        # Line of sight between (0,0) and (4,4) with obstacle at (2,2)
        cells_traversed = []
        x0, y0, x1, y1 = 0, 0, 4, 4
        x, y = x0, y0
        while x <= x1 and y <= y1:
            cells_traversed.append((x, y))
            x += 1
            y += 1
        assert (2, 2) in cells_traversed


class TestTier1CameraLerpAndZoom:
    """Feature 6: Camera Lerp & Dynamic Zoom."""

    def test_t1_f6_exponential_damping_follow_formula(self) -> None:
        cx, cy = 0.0, 0.0
        target_x, target_y = 10.0, 0.0
        dt = 0.016
        follow_speed = 8.0
        factor = 1.0 - math.exp(-follow_speed * dt)
        new_cx = cx + (target_x - cx) * factor
        # Camera moves towards target smoothly
        assert 0.0 < new_cx < target_x
        assert math.isclose(factor, 0.12015, abs_tol=1e-3)

    def test_t1_f6_deadzone_suppression_at_target(self) -> None:
        cx, cy = 5.0, 5.0
        target_x, target_y = 5.0, 5.0
        dist = math.hypot(target_x - cx, target_y - cy)
        # Dist <= deadzone (0.001) -> camera does not jitter
        assert dist <= 0.001

    def test_t1_f6_boundary_clamping_to_zone_bounds(self) -> None:
        min_wx, max_wx = -100.0, 100.0
        min_wy, max_wy = -100.0, 100.0
        raw_cam_x, raw_cam_y = 150.0, -120.0
        clamped_x = max(min_wx, min(raw_cam_x, max_wx))
        clamped_y = max(min_wy, min(raw_cam_y, max_wy))
        assert clamped_x == 100.0
        assert clamped_y == -100.0

    def test_t1_f6_dynamic_zoom_clamping_range(self) -> None:
        min_zoom, max_zoom = 0.5, 1.5
        assert max(min_zoom, min(0.2, max_zoom)) == 0.5
        assert max(min_zoom, min(2.5, max_zoom)) == 1.5
        assert max(min_zoom, min(1.0, max_zoom)) == 1.0

    def test_t1_f6_high_dt_stability_no_overshoot(self) -> None:
        # Even with high frame drop (dt = 1.0s), factor is capped < 1.0, no overshoot
        dt = 1.0
        factor = 1.0 - math.exp(-8.0 * dt)
        assert 0.0 < factor < 1.0
