"""
Unit Test Suite for Client-Side Fog of War & Minimap HUD Architecture (PoE2 Specification).
Covers:
1. Minimap Aspect-Ratio Preserving Projection & Geometry.
2. Fog of War Matrix 3-State Transition & 8-Tile Radius Reveal.
3. Fog State Persistence (LocalStorage key and format).
4. Dynamic Markers: Player blip, Waypoints, Boss Gate, POIs.
5. Mobile Battery Throttling & Game Pause Suppression.
6. Code & DOM Line Budget Compliance (minimap_hud.js <= 280 lines, index.html <= 200 lines).
"""

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


class MockMinimapProjection:
    """Simulates MinimapHUD aspect-ratio preserving coordinate projection."""
    def __init__(self, canvas_w: int = 120, canvas_h: int = 80, pad: int = 2):
        self.canvas_w = canvas_w
        self.canvas_h = canvas_h
        self.pad = pad
        self.map_w = 0
        self.map_h = 0
        self.scale = 1.0
        self.pad_x = pad
        self.pad_y = pad

    def set_map(self, width: int, height: int):
        self.map_w = width
        self.map_h = height
        avail_w = self.canvas_w - self.pad * 2
        avail_h = self.canvas_h - self.pad * 2
        if width > 0 and height > 0:
            self.scale = min(avail_w / width, avail_h / height)
            self.pad_x = self.pad + (avail_w - width * self.scale) / 2
            self.pad_y = self.pad + (avail_h - height * self.scale) / 2
        else:
            self.scale = 1.0
            self.pad_x = self.pad
            self.pad_y = self.pad

    def world_to_minimap(self, wx: float, wy: float) -> Tuple[float, float]:
        return (self.pad_x + wx * self.scale, self.pad_y + wy * self.scale)


class MockFogOfWarMatrix:
    """Simulates client-side Fog of War state machine (0=UNEXPLORED, 1=EXPLORED_FOGGED, 2=VISIBLE)."""
    UNEXPLORED = 0
    EXPLORED_FOGGED = 1
    VISIBLE = 2

    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height
        self.grid = bytearray(width * height)  # Initialized to all UNEXPLORED (0)
        self.is_dirty = True

    def update_player_vision(self, player_wx: float, player_wy: float, radius: float = 8.0) -> bool:
        changed = False
        ptx = int(math.floor(player_wx))
        pty = int(math.floor(player_wy))
        rad_sq = radius * radius

        # Step 1: Any tile that was previously VISIBLE (2) becomes EXPLORED_FOGGED (1)
        for i in range(len(self.grid)):
            if self.grid[i] == self.VISIBLE:
                self.grid[i] = self.EXPLORED_FOGGED
                changed = True

        # Step 2: Reveal all tiles within radius around player
        min_x = max(0, int(ptx - radius - 1))
        max_x = min(self.width - 1, int(ptx + radius + 1))
        min_y = max(0, int(pty - radius - 1))
        max_y = min(self.height - 1, int(pty + radius + 1))

        for ty in range(min_y, max_y + 1):
            row_idx = ty * self.width
            for tx in range(min_x, max_x + 1):
                dx = tx - ptx
                dy = ty - pty
                if (dx * dx + dy * dy) <= rad_sq:
                    idx = row_idx + tx
                    if self.grid[idx] != self.VISIBLE:
                        self.grid[idx] = self.VISIBLE
                        changed = True

        if changed:
            self.is_dirty = True
        return changed

    def get_fog_state(self, tx: int, ty: int) -> int:
        if 0 <= tx < self.width and 0 <= ty < self.height:
            return self.grid[ty * self.width + tx]
        return self.UNEXPLORED


# -----------------------------------------------------------------------------
# 1. Minimap Aspect-Ratio Preserving Projection Tests
# -----------------------------------------------------------------------------
class TestMinimapProjection:
    def test_aspect_ratio_preservation_standard_wilderness(self):
        """Verifies 60x45 wilderness map scales uniformly into 120x80 canvas without distortion."""
        proj = MockMinimapProjection(120, 80, pad=2)
        proj.set_map(60, 45)

        # Available space: 116 x 76
        # 116 / 60 = 1.9333; 76 / 45 = 1.6889 -> scale should be 1.6889
        expected_scale = 76.0 / 45.0
        assert math.isclose(proj.scale, expected_scale, rel_tol=1e-4)

        # Height should tightly fit 76 px, width should be centered
        pixel_w = 60 * proj.scale
        pixel_h = 45 * proj.scale
        assert pixel_w <= 116.0
        assert math.isclose(pixel_h, 76.0, rel_tol=1e-4)

        # Offset X should provide horizontal centering
        expected_pad_x = 2.0 + (116.0 - pixel_w) / 2.0
        assert math.isclose(proj.pad_x, expected_pad_x, rel_tol=1e-4)
        assert proj.pad_y == 2.0

    def test_aspect_ratio_preservation_large_zone(self):
        """Verifies 120x90 endgame zone fits inside 120x80 without boundary clipping."""
        proj = MockMinimapProjection(120, 80, pad=2)
        proj.set_map(120, 90)

        # Available space: 116 x 76
        # 116 / 120 = 0.9667; 76 / 90 = 0.8444 -> scale should be 0.8444
        expected_scale = 76.0 / 90.0
        assert math.isclose(proj.scale, expected_scale, rel_tol=1e-4)

        # Corner (119, 89) must map within (120, 80) canvas
        mx, my = proj.world_to_minimap(119, 89)
        assert 0 <= mx < 120
        assert 0 <= my < 80

    def test_coordinate_mapping_accuracy(self):
        """Origin (0,0) maps to (pad_x, pad_y) and coordinates scale linearly."""
        proj = MockMinimapProjection(120, 80, pad=2)
        proj.set_map(60, 45)

        mx0, my0 = proj.world_to_minimap(0, 0)
        assert math.isclose(mx0, proj.pad_x)
        assert math.isclose(my0, proj.pad_y)

        # Center tile (30, 22.5) should map to canvas center
        cx, cy = proj.world_to_minimap(30, 22.5)
        assert math.isclose(cx, 60.0, abs_tol=1.0)
        assert math.isclose(cy, 40.0, abs_tol=1.0)


# -----------------------------------------------------------------------------
# 2. Fog of War Matrix & State Transition Tests
# -----------------------------------------------------------------------------
class TestFogOfWarStateMachine:
    def test_initial_state_unexplored(self):
        """Newly created fog matrix must be 100% UNEXPLORED (0)."""
        fog = MockFogOfWarMatrix(60, 45)
        assert all(b == MockFogOfWarMatrix.UNEXPLORED for b in fog.grid)

    def test_reveal_radius_8_tiles(self):
        """Tiles within 8.0 distance of player become VISIBLE (2), rest remain UNEXPLORED."""
        fog = MockFogOfWarMatrix(60, 45)
        fog.update_player_vision(player_wx=20.0, player_wy=20.0, radius=8.0)

        # Tile directly at player
        assert fog.get_fog_state(20, 20) == MockFogOfWarMatrix.VISIBLE

        # Tile at distance 7
        assert fog.get_fog_state(27, 20) == MockFogOfWarMatrix.VISIBLE

        # Tile at distance 8
        assert fog.get_fog_state(28, 20) == MockFogOfWarMatrix.VISIBLE

        # Tile at distance 9 must be UNEXPLORED
        assert fog.get_fog_state(29, 20) == MockFogOfWarMatrix.UNEXPLORED

        # Far tile must remain UNEXPLORED
        assert fog.get_fog_state(0, 0) == MockFogOfWarMatrix.UNEXPLORED

    def test_visible_to_explored_fogged_transition(self):
        """When player moves far away, previous VISIBLE tiles become EXPLORED_FOGGED (1), never UNEXPLORED."""
        fog = MockFogOfWarMatrix(60, 45)
        fog.update_player_vision(player_wx=10.0, player_wy=10.0, radius=8.0)
        assert fog.get_fog_state(10, 10) == MockFogOfWarMatrix.VISIBLE

        # Move player to far position (40, 30)
        fog.update_player_vision(player_wx=40.0, player_wy=30.0, radius=8.0)

        # Previous position (10, 10) must now be EXPLORED_FOGGED (1)
        assert fog.get_fog_state(10, 10) == MockFogOfWarMatrix.EXPLORED_FOGGED

        # New position (40, 30) must be VISIBLE (2)
        assert fog.get_fog_state(40, 30) == MockFogOfWarMatrix.VISIBLE

    def test_persistence_key_and_integrity(self):
        """Validates standard localStorage persistence key format."""
        zone_id = "zone_tang_kiem_nhai"
        seed = 882914
        expected_key = f"fe_fog_{zone_id}_{seed}"
        assert expected_key == "fe_fog_zone_tang_kiem_nhai_882914"


# -----------------------------------------------------------------------------
# 3. Dynamic Indicators Tests
# -----------------------------------------------------------------------------
class TestDynamicIndicators:
    def test_player_heading_vector_calculation(self):
        """Directional notch points in facing angle direction."""
        angle = math.pi / 4  # 45 degrees (South-East in screen space)
        length = 5.0
        hx = math.cos(angle) * length
        hy = math.sin(angle) * length
        assert math.isclose(hx, length * math.sqrt(2) / 2)
        assert math.isclose(hy, length * math.sqrt(2) / 2)

    def test_boss_gate_marker_color_states(self):
        """Locked Boss Gate is Red (#ef4444); Unlocked/Breached is Emerald (#10b981)."""
        def get_gate_color(is_breached: bool) -> str:
            return "#10b981" if is_breached else "#ef4444"

        assert get_gate_color(False) == "#ef4444"
        assert get_gate_color(True) == "#10b981"

    def test_poi_marker_fog_masking(self):
        """POI markers are masked when tile is UNEXPLORED, and visible once explored."""
        fog = MockFogOfWarMatrix(60, 45)
        poi_x, poi_y = 35, 25

        # Unexplored: POI is hidden
        assert fog.get_fog_state(poi_x, poi_y) == MockFogOfWarMatrix.UNEXPLORED
        should_render = fog.get_fog_state(poi_x, poi_y) > 0
        assert should_render is False

        # Player visits POI
        fog.update_player_vision(poi_x, poi_y, radius=8.0)
        assert fog.get_fog_state(poi_x, poi_y) == MockFogOfWarMatrix.VISIBLE
        assert (fog.get_fog_state(poi_x, poi_y) > 0) is True

        # Player leaves POI
        fog.update_player_vision(0, 0, radius=8.0)
        assert fog.get_fog_state(poi_x, poi_y) == MockFogOfWarMatrix.EXPLORED_FOGGED
        assert (fog.get_fog_state(poi_x, poi_y) > 0) is True


# -----------------------------------------------------------------------------
# 4. Mobile Battery Throttling Tests
# -----------------------------------------------------------------------------
class TestBatteryThrottling:
    def test_30hz_throttling_filters_120fps(self):
        """Simulates 120 FPS game loop over 1 second (120 frames at 8.33ms dt)."""
        render_count = 0
        last_render_time = -100.0
        throttle_ms = 33.33  # ~30 Hz

        for frame in range(120):
            now = frame * 8.3333  # dt = 8.33 ms
            if now - last_render_time >= throttle_ms:
                last_render_time = now
                render_count += 1

        # At 30 Hz in 1 second, render count should be ~30 (75% battery savings over 120 FPS)
        assert 29 <= render_count <= 31

    def test_pause_game_suppression(self):
        """When isGamePaused is True, minimap updates are completely suppressed."""
        is_paused = True
        did_render = False

        if not is_paused:
            did_render = True

        assert did_render is False


# -----------------------------------------------------------------------------
# 5. Code Hygiene Budget Tests
# -----------------------------------------------------------------------------
class TestCodeHygieneBudget:
    def test_proposed_minimap_hud_line_cap(self):
        """Verifies proposed_minimap_hud.js stays <= 280 lines."""
        path = "c:/Projects/FreeExile/.agents/teamwork/explorer_m4_3/proposed_minimap_hud.js"
        with open(path, "r", encoding="utf-8") as f:
            lines = f.readlines()
        assert len(lines) <= 280, f"proposed_minimap_hud.js exceeded 280 lines ({len(lines)} lines)"

    def test_index_html_current_line_cap(self):
        """Verifies client/webapp/index.html is currently <= 200 lines."""
        path = "c:/Projects/FreeExile/client/webapp/index.html"
        with open(path, "r", encoding="utf-8") as f:
            lines = f.readlines()
        assert len(lines) <= 200, f"index.html exceeded 200 lines ({len(lines)} lines)"
