"""
Shared Fixtures and Mock Models for FreeExile Cocos Creator 3.x E2E Test Suite.
Provides authoritative geometric math, protobuf framing, simulated state entities,
and game design matrix constants for Tiers 1-4.
"""

from __future__ import annotations

import math
import struct
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

import pytest

# Configure Python path for FreeExile server and compiled protobuf packages
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))
SERVER_PATH = PROJECT_ROOT / "server"
if str(SERVER_PATH) not in sys.path:
    sys.path.insert(0, str(SERVER_PATH))
PROTO_PATH = SERVER_PATH / "proto"
if str(PROTO_PATH) not in sys.path:
    sys.path.insert(0, str(PROTO_PATH))

# Import compiled Protobuf messages
from server.proto import (  # noqa: E402
    auth_pb2,
    combat_pb2,
    map_zone_pb2,
    network_pb2,
    npc_pb2,
    portal_pb2,
)

# Canonical constants from specifications
TILE_W = 64.0
TILE_H = 32.0
PLAYER_SPEED = 5.5
DODGE_BOOST = 1.75
DODGE_IFRAME_DURATION = 0.25
SAFE_RADIUS_WAYPOINT = 8.0
HEALING_RATE_PER_SEC = 0.02
BACKPACK_CAPACITY = 40

CANONICAL_CLASSES = [
    "char_sword_master",
    "char_sword_maiden",
    "char_feral_berserker",
    "char_wild_archer",
    "char_glacial_lancer",
    "char_shadow_assassin",
]

CANONICAL_MARTIAL_NAMES = [
    "Độc Cô Bại",
    "Huyết Vô Ảnh",
    "Cố Dạ Ly",
    "Phong Tiêu Dạ",
    "Mạc Vấn Thiên",
    "Lôi Cuồng",
    "Diệp Tố Tâm",
    "Thương Vô Địch",
    "Hàn Băng Thần",
    "Lãnh Nguyệt",
    "Dạ Khắc",
    "Sát Cốt Ma",
]

SUPPORTED_LOCALES = ["vi", "en", "zh", "ja", "ko", "th", "de", "ru", "es"]

OPCODES = {
    "PING": 0x0001,
    "PONG": 0x0002,
    "PLAYER_MOVE": 0x0010,
    "WORLD_SYNC": 0x0011,
    "ENTITY_SNAPSHOT": 0x0012,
    "CAST_SKILL": 0x0020,
    "PHANTOM_EVASION": 0x0021,
    "COMBAT_DAMAGE": 0x0022,
    "ENTER_ZONE_REQ": 0x0030,
    "ENTER_ZONE_RESP": 0x0031,
    "WAYPOINT_TELEPORT_REQ": 0x0032,
    "WAYPOINT_TELEPORT_RESP": 0x0033,
    "INTERACT_NPC_REQ": 0x0040,
    "INTERACT_NPC_RESP": 0x0041,
}


def world_to_iso(
    wx: float, wy: float, wz: float = 0.0,
    cx: float = 0.0, cy: float = 0.0,
    vx: float = 0.0, vy: float = 0.0, zoom: float = 1.0,
) -> Tuple[float, float]:
    """Projects 3D world coordinates to 2.5D isometric screen coordinates (2:1 ratio)."""
    rel_x = wx - cx
    rel_y = wy - cy
    sx = (rel_x - rel_y) * (TILE_W / 2.0) * zoom + vx
    sy = (rel_x + rel_y) * (TILE_H / 2.0) * zoom + vy - (wz * 24.0 * zoom)
    return sx, sy


def iso_to_world(
    sx: float, sy: float,
    cx: float = 0.0, cy: float = 0.0,
    vx: float = 0.0, vy: float = 0.0, zoom: float = 1.0,
) -> Tuple[float, float]:
    """Unprojects 2D screen coordinates back to continuous 2.5D ground world coordinates."""
    screen_x = (sx - vx) / zoom
    screen_y = (sy - vy) / zoom
    rel_x = ((screen_x / (TILE_W / 2.0)) + (screen_y / (TILE_H / 2.0))) / 2.0
    rel_y = ((screen_y / (TILE_H / 2.0)) - (screen_x / (TILE_W / 2.0))) / 2.0
    return rel_x + cx, rel_y + cy


def resolve_heading_bin(dx: float, dy: float) -> str:
    """Calculates isometric 8-directional heading bin from movement vector."""
    if abs(dx) < 1e-6 and abs(dy) < 1e-6:
        return "S"
    screen_dir_x = (dx - dy) * 0.5
    screen_dir_y = (dx + dy) * 0.25
    target_rad = math.atan2(screen_dir_y, screen_dir_x)
    deg = (math.degrees(target_rad) + 360.0) % 360.0

    bins = [
        ("E", (337.5, 360.0)), ("E", (0.0, 22.5)),
        ("SE", (22.5, 67.5)), ("S", (67.5, 112.5)),
        ("SW", (112.5, 157.5)), ("W", (157.5, 202.5)),
        ("NW", (202.5, 247.5)), ("N", (247.5, 292.5)),
        ("NE", (292.5, 337.5)),
    ]
    for name, (low, high) in bins:
        if low <= deg < high or (low == 337.5 and deg >= 337.5):
            return name
    return "S"


def encode_opcode_frame(opcode: int, payload: bytes) -> bytes:
    """Encodes a binary WebSocket frame with 2-byte big-endian opcode."""
    return struct.pack(">H", opcode) + payload


def decode_opcode_frame(frame: bytes) -> Tuple[int, bytes]:
    """Decodes a binary WebSocket frame into (opcode, protobuf_bytes)."""
    if len(frame) < 2:
        raise ValueError("Frame length underflow (< 2 bytes)")
    opcode = struct.unpack(">H", frame[:2])[0]
    return opcode, frame[2:]


class SimulatedCocosPlayer:
    """Simulates Cocos Creator 3.x client player entity and kinematics."""

    def __init__(self, entity_id: int = 1, x: float = 0.0, y: float = 0.0):
        self.entity_id = entity_id
        self.wx = x
        self.wy = y
        self.vx = 0.0
        self.vy = 0.0
        self.heading = "S"
        self.anim_state = "idle"
        self.input_seq = 0
        self.is_iframe = False
        self.iframe_timer = 0.0
        self.current_hp = 100.0
        self.max_hp = 100.0
        self.current_mana = 50.0
        self.max_mana = 50.0
        self.inventory: List[Optional[Dict[str, Any]]] = [None] * BACKPACK_CAPACITY
        self.zone_id = "zone_boundless_sanctuary"

    def apply_input(self, mx: float, my: float, dt: float) -> None:
        """Applies digital or touch input with frame 0 zero-residual momentum."""
        mag = math.hypot(mx, my)
        if mag <= 0.05:
            # Immediate digital brake
            self.vx = 0.0
            self.vy = 0.0
            self.anim_state = "idle"
            return

        self.input_seq += 1
        speed = PLAYER_SPEED * (DODGE_BOOST if self.is_iframe else 1.0)
        norm_x = mx / mag
        norm_y = my / mag
        self.vx = norm_x * speed
        self.vy = norm_y * speed
        self.wx += self.vx * dt
        self.wy += self.vy * dt
        self.heading = resolve_heading_bin(norm_x, norm_y)
        self.anim_state = "run"

    def trigger_dodge(self) -> bool:
        """Triggers 0.25s Phantom Evasion dodge roll."""
        self.is_iframe = True
        self.iframe_timer = DODGE_IFRAME_DURATION
        self.anim_state = "dodge"
        return True

    def update_timers(self, dt: float) -> None:
        """Advances iframe and hitstop cooldowns."""
        if self.is_iframe:
            self.iframe_timer -= dt
            if self.iframe_timer <= 0.0:
                self.is_iframe = False
                self.iframe_timer = 0.0
                if self.anim_state == "dodge":
                    self.anim_state = "idle"

    def add_to_inventory(self, item: Dict[str, Any]) -> Tuple[bool, Optional[int]]:
        """Inserts item into first free slot in the 40-slot backpack."""
        for idx in range(BACKPACK_CAPACITY):
            if self.inventory[idx] is None:
                self.inventory[idx] = item
                return True, idx
        return False, None


@pytest.fixture
def simulated_player() -> SimulatedCocosPlayer:
    return SimulatedCocosPlayer(entity_id=101, x=0.0, y=0.0)


@pytest.fixture
def sanctuary_zone_data() -> map_zone_pb2.ZoneInfoPayload:
    zone = map_zone_pb2.ZoneInfoPayload()
    zone.zone_id = "zone_boundless_sanctuary"
    zone.name = "Doanh Trại Bến Lưu Đày"
    zone.zone_type = map_zone_pb2.ZoneType.ZONE_TYPE_SANCTUARY
    zone.bounds_width = 800.0
    zone.bounds_height = 800.0
    wp = zone.waypoints.add()
    wp.waypoint_id = "wp_sanctuary_central"
    wp.name = "Bia Cổ Bến Lưu Đày"
    wp.x = 0.0
    wp.y = 0.0
    wp.is_unlocked = True
    return zone
