"""
Tier 2: Boundary & Corner Cases — Extreme Limits, Stress Faults & Desync Guards.
Covers Tier 2 requirements from DISPATCH.md and Survey specifications:
- Empty/Null/Zero-length input vectors & NaN guards
- Maximum speed clamping & anti-speedhack thresholds
- Wall collisions & corner pinching
- Packet drops, sequence desync & reconnect timeouts
- Backpack inventory slot 39 vs 40 boundaries
- Dead entity, poise break & invulnerability boundary handling
"""

from __future__ import annotations

import math
from typing import Any, Dict, List, Optional, Tuple
import pytest

from server.proto import combat_pb2, network_pb2
from tests.e2e_cocos.conftest import (
    BACKPACK_CAPACITY,
    DODGE_BOOST,
    PLAYER_SPEED,
    SimulatedCocosPlayer,
    decode_opcode_frame,
)


class TestTier2InputAndVectorBoundaries:
    """Tier 2: Empty, Null & Zero-Length Input Vectors."""

    def test_t2_zero_length_vector_produces_zero_displacement(self, simulated_player: SimulatedCocosPlayer) -> None:
        simulated_player.apply_input(0.0, 0.0, dt=0.016)
        assert simulated_player.vx == 0.0 and simulated_player.vy == 0.0
        assert simulated_player.anim_state == "idle"

    def test_t2_sub_threshold_epsilon_vector_treated_as_zero(self, simulated_player: SimulatedCocosPlayer) -> None:
        # Vector with magnitude 0.0001 < 0.05 deadzone
        simulated_player.apply_input(0.0001, 0.0001, dt=0.016)
        assert simulated_player.vx == 0.0 and simulated_player.vy == 0.0
        assert simulated_player.anim_state == "idle"

    def test_t2_nan_coordinate_input_guard(self) -> None:
        raw_x = float("nan")
        # Guard function: if NaN or Inf, sanitize to 0.0
        safe_x = 0.0 if math.isnan(raw_x) or math.isinf(raw_x) else raw_x
        assert safe_x == 0.0

    def test_t2_zero_delta_time_produces_zero_movement(self, simulated_player: SimulatedCocosPlayer) -> None:
        init_x = simulated_player.wx
        simulated_player.apply_input(1.0, 0.0, dt=0.0)
        assert simulated_player.wx == init_x

    def test_t2_infinite_coordinate_guard(self) -> None:
        raw_y = float("inf")
        safe_y = 0.0 if math.isinf(raw_y) else raw_y
        assert safe_y == 0.0


class TestTier2SpeedClampingAndAntiSpeedhack:
    """Tier 2: Maximum Speed Clamping & Anti-Speedhack Bounds."""

    def test_t2_input_magnitude_greater_than_one_normalized(self, simulated_player: SimulatedCocosPlayer) -> None:
        # Oversized joystick vector (5.0, 5.0) must normalize to PLAYER_SPEED
        simulated_player.apply_input(5.0, 5.0, dt=0.1)
        actual_speed = math.hypot(simulated_player.vx, simulated_player.vy)
        assert math.isclose(actual_speed, PLAYER_SPEED, abs_tol=1e-4)

    def test_t2_claimed_speed_exceeding_server_tolerance_flagged(self) -> None:
        dt = 0.1
        max_allowed_speed = PLAYER_SPEED * 1.15  # 15% network tolerance
        max_allowed_dist = max_allowed_speed * dt
        claimed_dist = (PLAYER_SPEED * 1.50) * dt  # 50% faster (speedhack)
        is_suspicious = claimed_dist > max_allowed_dist
        assert is_suspicious is True

    def test_t2_overspeed_rubberband_clamped_to_server_position(self) -> None:
        auth_pos = 10.0
        claimed_pos = 18.0  # Excessive displacement
        tolerance = 2.0
        reconciled_pos = auth_pos if abs(claimed_pos - auth_pos) > tolerance else claimed_pos
        assert reconciled_pos == auth_pos

    def test_t2_dodge_boost_speed_capped_strictly_at_1_75(self, simulated_player: SimulatedCocosPlayer) -> None:
        simulated_player.trigger_dodge()
        simulated_player.apply_input(1.0, 0.0, dt=0.1)
        expected_boosted_speed = PLAYER_SPEED * DODGE_BOOST
        assert math.isclose(simulated_player.vx, expected_boosted_speed, abs_tol=1e-4)

    def test_t2_negative_delta_time_rejected(self, simulated_player: SimulatedCocosPlayer) -> None:
        init_x = simulated_player.wx
        dt = -0.05
        safe_dt = max(0.0, dt)
        simulated_player.apply_input(1.0, 0.0, dt=safe_dt)
        assert simulated_player.wx == init_x


class TestTier2WallCollisionAndGeometryBoundaries:
    """Tier 2: Wall Collisions & Corner Pinching."""

    def test_t2_corner_pinch_stops_both_axes(self) -> None:
        # Trapped in a 90-degree corner: both X and Y directions are blocked
        def can_move(dx: float, dy: float) -> Tuple[float, float]:
            return (0.0 if dx > 0 else dx, 0.0 if dy > 0 else dy)
        res_x, res_y = can_move(0.5, 0.5)
        assert res_x == 0.0 and res_y == 0.0

    def test_t2_continuous_collision_prevents_tunneling(self) -> None:
        # High speed tunneling test: moving 10 units through 1-unit wall at x=2
        start_x, end_x = 0.0, 10.0
        wall_x = 2.0
        # Ray-cast intersection stops player before wall
        stopped_x = min(end_x, wall_x - 0.35)  # radius clearance
        assert stopped_x == 1.65 < wall_x

    def test_t2_zone_boundary_box_clamp(self) -> None:
        bounds = 800.0
        raw_x = 950.0
        clamped_x = max(-bounds, min(raw_x, bounds))
        assert clamped_x == 800.0

    def test_t2_degenerate_normal_fallback_halts(self) -> None:
        normal_x, normal_y = 0.0, 0.0
        norm_mag = math.hypot(normal_x, normal_y)
        # If normal is degenerate (0,0), safe fallback halts movement
        slide_v = (0.0, 0.0) if norm_mag == 0 else (1.0, 1.0)
        assert slide_v == (0.0, 0.0)

    def test_t2_chasm_passability_during_dodge_only(self) -> None:
        is_chasm = True
        walkable_normal = not is_chasm
        walkable_during_dodge = True  # Chasm leap mechanic
        assert walkable_normal is False
        assert walkable_during_dodge is True


class TestTier2PacketFaultsAndSequenceDesync:
    """Tier 2: Packet Drops, Sequence Desync & Reconnect Timeouts."""

    def test_t2_future_ack_sequence_clamp(self) -> None:
        client_max_seq = 20
        server_ack_seq = 25  # Future seq due to desync
        safe_ack = min(client_max_seq, server_ack_seq)
        assert safe_ack == 20

    def test_t2_stale_out_of_order_packet_discarded(self) -> None:
        last_processed_seq = 10
        incoming_seq = 7  # Older packet arriving late
        is_stale = incoming_seq <= last_processed_seq
        assert is_stale is True

    def test_t2_duplicate_packet_sequence_deduplicated(self) -> None:
        processed_packets = set()
        packet_seq = 15
        is_first_time = packet_seq not in processed_packets
        processed_packets.add(packet_seq)
        is_second_time = packet_seq not in processed_packets
        assert is_first_time is True
        assert is_second_time is False

    def test_t2_ping_heartbeat_timeout_detection(self) -> None:
        last_pong_time = 100.0
        current_time = 107.0  # 7 seconds later
        timeout_threshold = 6.0
        is_timed_out = (current_time - last_pong_time) > timeout_threshold
        assert is_timed_out is True

    def test_t2_corrupted_frame_underflow_rejection(self) -> None:
        corrupted_frame = b"\x01"  # < 2 bytes
        with pytest.raises(ValueError, match="Frame length underflow"):
            decode_opcode_frame(corrupted_frame)


class TestTier2BackpackSlotBoundaries:
    """Tier 2: Backpack Inventory Slot 39 vs 40 Boundaries."""

    def test_t2_slot_index_39_is_valid_final_slot(self, simulated_player: SimulatedCocosPlayer) -> None:
        # Fill first 39 slots
        for i in range(39):
            simulated_player.add_to_inventory({"id": f"item_{i}"})
        # Add 40th item into index 39
        success, slot_idx = simulated_player.add_to_inventory({"id": "item_39"})
        assert success is True
        assert slot_idx == 39

    def test_t2_slot_index_40_is_strictly_out_of_bounds(self, simulated_player: SimulatedCocosPlayer) -> None:
        # Inventory array is of length 40, index 40 must raise IndexError
        with pytest.raises(IndexError):
            _ = simulated_player.inventory[40]

    def test_t2_middle_slot_removal_allows_gap_refill(self, simulated_player: SimulatedCocosPlayer) -> None:
        simulated_player.add_to_inventory({"id": "item_0"})
        simulated_player.add_to_inventory({"id": "item_1"})
        simulated_player.add_to_inventory({"id": "item_2"})
        # Remove item_1 at index 1
        simulated_player.inventory[1] = None
        # Next item must fill slot 1
        success, slot_idx = simulated_player.add_to_inventory({"id": "item_new"})
        assert success is True
        assert slot_idx == 1

    def test_t2_multi_slot_boundary_overflow_check(self) -> None:
        grid_width = 8
        grid_height = 5
        # Item 2x3 at x=7 (only 1 column remaining, width requires 2)
        item_w = 2
        start_x = 7
        fits_horizontally = (start_x + item_w) <= grid_width
        assert fits_horizontally is False

    def test_t2_backpack_full_does_not_mutate_existing_slots(self, simulated_player: SimulatedCocosPlayer) -> None:
        for i in range(BACKPACK_CAPACITY):
            simulated_player.add_to_inventory({"id": f"item_{i}"})
        # Attempt overflow
        simulated_player.add_to_inventory({"id": "bad_item"})
        # Verify slot 0 and 39 are still original
        assert simulated_player.inventory[0]["id"] == "item_0"
        assert simulated_player.inventory[39]["id"] == "item_39"


class TestTier2DeadEntityAndInvulnerability:
    """Tier 2: Dead Entity, Poise Break & Invulnerability Boundary Handling."""

    def test_t2_skill_cast_on_dead_entity_rejected(self) -> None:
        target_hp = 0
        can_target = target_hp > 0
        assert can_target is False

    def test_t2_damage_during_iframe_strictly_zero(self, simulated_player: SimulatedCocosPlayer) -> None:
        simulated_player.trigger_dodge()
        damage_applied = 0.0 if simulated_player.is_iframe else 100.0
        assert damage_applied == 0.0

    def test_t2_lethal_damage_clamped_to_zero_hp(self) -> None:
        curr_hp = 20
        raw_damage = 500
        new_hp = max(0, curr_hp - raw_damage)
        assert new_hp == 0  # No negative overflow

    def test_t2_poise_break_stagger_state_trigger(self) -> None:
        poise = 0
        is_staggered = (poise <= 0)
        assert is_staggered is True

    def test_t2_telegraph_cancelled_on_boss_death(self) -> None:
        boss_hp = 0
        active_telegraph = {"active": True}
        if boss_hp <= 0:
            active_telegraph["active"] = False
        assert active_telegraph["active"] is False
