"""
Unit Test Suite for FreeExile Map Data, Server-Authoritative War-Fog,
Procedural Generation, and Anti-Bot Dynamic Obstacle Architecture.

Tests:
1. Map Biome Catalog and Tile Types (Savage Grimdark themes, no Xianxia).
2. Procedural Map Generator (BSP + Caves, 100% connectivity spawn to boss).
3. Anti-Bot Dynamic Obstacles (Destructibles, Debris, Mud pools, Spikes).
4. Real-time Obstacle Destruction and Walkable Grid Mutation.
5. Anti-Bot Path Complexity (Sinuosity ratio >= 1.25, non-linear corridors).
6. Server-Authoritative War-Fog Engine (FOV Line of Sight, Wall obstruction).
7. Anti-Maphack Entity Visibility Filtering (Hidden entities in fog never sent).
8. Fog Delta Compression (Run-Length Encoding / Bitmask efficiency).
9. Deceptive Dead-End and Ambush Pocket generation for bot trapping.
"""

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

from server.world.map_data_types import (
    TileType,
    FogState,
    ObstacleType,
    TileCell,
    ObstacleInstance,
    MapGridData,
    PlayerVisibilityState,
)
from server.world.map_biome_catalog import (
    MAP_BIOMES,
    get_biome_definition,
    list_available_biomes,
)
from server.world.war_fog_engine import (
    WarFogEngine,
    compress_fog_rle,
    decompress_fog_rle,
)
from server.world.procedural_map_engine import (
    ProceduralMapEngine,
    verify_path_connectivity,
    calculate_path_sinuosity,
)


class TestMapBiomeCatalog:
    def test_canonical_grimdark_biomes_registered(self):
        """Verifies 5 canonical Savage Grimdark Biomes exist with correct attributes."""
        biomes = list_available_biomes()
        assert len(biomes) == 30
        expected_biomes = [
            "SAVAGE_MANGROVE_SWAMP",
            "CRIMSON_BLOOD_FOREST",
            "BLEACHED_BONE_CANYON",
            "OUTCAST_MINE_SHAFTS",
            "CORRUPTED_FIEND_RUINS",
        ]
        for b in expected_biomes:
            assert b in biomes
            defn = get_biome_definition(b)
            assert defn.biome_id == b
            assert defn.hazard_slow_factor >= 1.0
            assert len(defn.obstacle_palette) >= 2
            # Verify strictly Grimdark naming, zero Tu Tien / Xianxia
            assert "Tiên" not in defn.name
            assert "Linh Thạch" not in defn.description

    def test_tile_cell_attributes(self):
        """Verifies tile cell attributes for floors, walls, and hazards."""
        floor = TileCell(x=1, y=2, tile_type=TileType.FLOOR, walkable=True, blocks_vision=False)
        assert floor.walkable is True
        assert floor.blocks_vision is False

        wall = TileCell(x=1, y=3, tile_type=TileType.WALL, walkable=False, blocks_vision=True)
        assert wall.walkable is False
        assert wall.blocks_vision is True

        mud = TileCell(
            x=2, y=2, tile_type=TileType.MUD_POOL, walkable=True, blocks_vision=False, movement_cost=2.0
        )
        assert mud.movement_cost == 2.0


class TestProceduralMapGeneration:
    def test_deterministic_generation_with_seed(self):
        """Same seed must produce identical dungeon layout."""
        engine = ProceduralMapEngine(width=48, height=48)
        map_a = engine.generate_map(seed=12345, biome_id="BLEACHED_BONE_CANYON")
        map_b = engine.generate_map(seed=12345, biome_id="BLEACHED_BONE_CANYON")

        assert map_a.width == map_b.width == 48
        assert map_a.height == map_b.height == 48
        assert map_a.spawn_point == map_b.spawn_point
        assert map_a.boss_point == map_b.boss_point
        assert len(map_a.obstacles) == len(map_b.obstacles)

    def test_guaranteed_connectivity_spawn_to_boss(self):
        """Verifies 100% path connectivity from spawn point to boss arena."""
        engine = ProceduralMapEngine(width=48, height=48)
        for seed in [111, 222, 333, 444]:
            map_data = engine.generate_map(seed=seed, biome_id="CRIMSON_BLOOD_FOREST")
            connected, path = verify_path_connectivity(
                map_data, map_data.spawn_point, map_data.boss_point
            )
            assert connected is True
            assert len(path) > 5


class TestAntiBotDynamicObstacles:
    def test_obstacle_injection_along_corridors(self):
        """Verifies that dynamic anti-bot obstacles are placed to break linear corridors."""
        engine = ProceduralMapEngine(width=48, height=48)
        map_data = engine.generate_map(seed=98765, biome_id="OUTCAST_MINE_SHAFTS")

        assert len(map_data.obstacles) > 0
        has_destructible = any(
            obs.obstacle_type == ObstacleType.BARRICADE_WOOD_BONE
            for obs in map_data.obstacles.values()
        )
        has_debris_or_mud = any(
            obs.obstacle_type in (ObstacleType.DEBRIS_COLLAPSE, ObstacleType.MUD_QUAGMIRE, ObstacleType.BONE_CLUSTER)
            for obs in map_data.obstacles.values()
        )
        assert has_destructible is True
        assert has_debris_or_mud is True

    def test_attack_and_destroy_barricade(self):
        """Verifies attacking a destructible barricade reduces HP and updates walkable grid."""
        engine = ProceduralMapEngine(width=32, height=32)
        map_data = engine.generate_map(seed=42, biome_id="OUTCAST_MINE_SHAFTS")

        # Find a destructible barricade
        barricade_coord = None
        for coord, obs in map_data.obstacles.items():
            if obs.obstacle_type == ObstacleType.BARRICADE_WOOD_BONE:
                barricade_coord = coord
                break

        assert barricade_coord is not None
        bx, by = barricade_coord
        assert map_data.is_walkable(bx, by) is False

        # Attack with partial damage
        destroyed, remaining_hp = engine.damage_obstacle(map_data, bx, by, damage=40)
        assert destroyed is False
        assert remaining_hp > 0
        assert map_data.is_walkable(bx, by) is False

        # Attack with lethal damage
        destroyed, remaining_hp = engine.damage_obstacle(map_data, bx, by, damage=100)
        assert destroyed is True
        assert remaining_hp == 0
        assert map_data.is_walkable(bx, by) is True

    def test_anti_bot_sinuosity_path_complexity(self):
        """Verifies path between spawn and boss is non-linear (sinuosity ratio >= 1.25)."""
        engine = ProceduralMapEngine(width=48, height=48)
        map_data = engine.generate_map(seed=777, biome_id="SAVAGE_MANGROVE_SWAMP")
        _, path = verify_path_connectivity(map_data, map_data.spawn_point, map_data.boss_point)
        sinuosity = calculate_path_sinuosity(path, map_data.spawn_point, map_data.boss_point)
        assert sinuosity >= 1.25

    def test_deceptive_dead_ends_generated(self):
        """Verifies deceptive branch dead-ends are created to deter naive pathing bots."""
        engine = ProceduralMapEngine(width=48, height=48)
        map_data = engine.generate_map(seed=555, biome_id="CORRUPTED_FIEND_RUINS")
        assert len(map_data.dead_end_points) >= 2


class TestServerAuthoritativeWarFog:
    def test_initial_fog_state_all_unexplored(self):
        """New visibility state begins with 100% unexplored tiles."""
        fog_engine = WarFogEngine()
        vis = fog_engine.create_player_visibility(player_id="p_tester", width=32, height=32)
        assert vis.explored_tiles_count == 0
        assert vis.visible_tiles_count == 0
        assert vis.fog_matrix[10][10] == FogState.UNEXPLORED.value

    def test_fov_computation_and_wall_blocking(self):
        """Field of view expands around player and is blocked by solid walls."""
        engine = ProceduralMapEngine(width=32, height=32)
        map_data = engine.generate_map(seed=999, biome_id="BLEACHED_BONE_CANYON")
        fog_engine = WarFogEngine()
        vis = fog_engine.create_player_visibility(player_id="p_hero", width=32, height=32)

        sx, sy = map_data.spawn_point
        fog_engine.update_visibility(vis, map_data, player_x=sx, player_y=sy, view_radius=10)

        # Spawn point itself must be VISIBLE
        assert vis.fog_matrix[sy][sx] == FogState.VISIBLE.value
        assert vis.visible_tiles_count > 0
        assert vis.explored_tiles_count >= vis.visible_tiles_count

        # When moving away, old visible cells become EXPLORED_FOGGED
        old_visible_cells = [
            (x, y)
            for y in range(32)
            for x in range(32)
            if vis.fog_matrix[y][x] == FogState.VISIBLE.value
        ]
        bx, by = map_data.boss_point
        fog_engine.update_visibility(vis, map_data, player_x=bx, player_y=by, view_radius=10)

        # Cells around spawn that are outside boss radius should now be EXPLORED_FOGGED
        demoted_to_fogged = sum(
            1 for (x, y) in old_visible_cells if vis.fog_matrix[y][x] == FogState.EXPLORED_FOGGED.value
        )
        assert demoted_to_fogged > 0

    def test_anti_maphack_entity_filtering(self):
        """Server strictly filters out entities outside player's VISIBLE field of view."""
        fog_engine = WarFogEngine()
        vis = fog_engine.create_player_visibility(player_id="p_stealth", width=32, height=32)

        # Mark cell (5, 5) as VISIBLE, (15, 15) as EXPLORED_FOGGED, (25, 25) as UNEXPLORED
        vis.fog_matrix[5][5] = FogState.VISIBLE.value
        vis.fog_matrix[15][15] = FogState.EXPLORED_FOGGED.value
        vis.fog_matrix[25][25] = FogState.UNEXPLORED.value

        all_entities = [
            {"id": "mob_near", "x": 5, "y": 5, "type": "MONSTER"},
            {"id": "chest_in_fog", "x": 15, "y": 15, "type": "CHEST"},
            {"id": "boss_unexplored", "x": 25, "y": 25, "type": "BOSS"},
        ]

        filtered = fog_engine.filter_visible_entities(vis, all_entities)
        # ONLY mob_near should be visible to client!
        assert len(filtered) == 1
        assert filtered[0]["id"] == "mob_near"

    def test_fog_rle_compression(self):
        """Fog matrix can be compressed with RLE and decompressed losslessly."""
        matrix = [[0] * 16 for _ in range(16)]
        for y in range(4, 8):
            for x in range(4, 8):
                matrix[y][x] = 2  # VISIBLE
        for y in range(8, 12):
            for x in range(4, 8):
                matrix[y][x] = 1  # EXPLORED_FOGGED

        compressed = compress_fog_rle(matrix)
        assert isinstance(compressed, str)
        decompressed = decompress_fog_rle(compressed, width=16, height=16)
        assert decompressed == matrix

    def test_fog_rle_decompression_error_handling(self):
        """Malformed RLE input or dimension mismatch gracefully returns safe default matrix."""
        # 1. Empty string
        assert decompress_fog_rle("", width=8, height=8) == [[0] * 8 for _ in range(8)]
        # 2. Corrupted token without 'x'
        assert decompress_fog_rle("bad_token", width=8, height=8) == [[0] * 8 for _ in range(8)]
        # 3. Non-integer values
        assert decompress_fog_rle("axb", width=8, height=8) == [[0] * 8 for _ in range(8)]
        # 4. Total cell count mismatch (e.g. 10 cells instead of 64)
        assert decompress_fog_rle("0x10", width=8, height=8) == [[0] * 8 for _ in range(8)]

    def test_carve_line_bresenham(self):
        """_carve_line carves clean corridor without creating accidental rectangular rooms."""
        engine = ProceduralMapEngine(width=16, height=16)
        tiles = [
            [TileCell(x=x, y=y, tile_type=TileType.WALL, walkable=False, blocks_vision=True) for x in range(16)]
            for y in range(16)
        ]
        # Carve diagonal line from (2, 2) to (10, 10)
        engine._carve_line(tiles, 2, 2, 10, 10, width=1)
        # Verify diagonal points are walkable
        for i in range(2, 11):
            assert tiles[i][i].walkable is True
        # Verify opposite corners of bounding box are NOT carved (not a rectangle!)
        assert tiles[2][10].walkable is False
        assert tiles[10][2].walkable is False


class TestGrandBossArenaAndMapExpansion:
    def test_scalable_map_dimensions_optimization(self):
        """Verifies procedural map engine dynamically scales for 32x32, 48x48, 64x64, 80x80."""
        for dim in [32, 48, 64, 80]:
            engine = ProceduralMapEngine(width=dim, height=dim)
            map_data = engine.generate_map(seed=42 + dim, biome_id="BLEACHED_BONE_CANYON")
            assert map_data.width == dim
            assert map_data.height == dim
            assert len(map_data.rooms) >= 4
            assert map_data.boss_room is not None
            assert map_data.boss_room.is_boss_room is True
            # Verify 100% path connectivity even on large 80x80 maps
            connected, path = verify_path_connectivity(map_data, map_data.spawn_point, map_data.boss_point)
            assert connected is True
            assert len(path) > 5

    def test_grand_boss_room_arena_architecture(self):
        """Verifies end-of-map boss room has altar, runic floor, braziers, and boss gate barrier."""
        engine = ProceduralMapEngine(width=48, height=48)
        map_data = engine.generate_map(seed=999, biome_id="CORRUPTED_FIEND_RUINS")

        br = map_data.boss_room
        assert br is not None
        assert br.room_type == "boss"
        assert br.is_boss_room is True
        assert br.w >= 8 and br.h >= 8

        # Center tile is BOSS_ALTAR
        bcx, bcy = br.center
        assert map_data.tiles[bcy][bcx].tile_type == TileType.BOSS_ALTAR

        # Surrounding tiles include RUNIC_FLOOR
        runic_found = any(
            map_data.tiles[bcy + dy][bcx + dx].tile_type == TileType.RUNIC_FLOOR
            for dy in [-1, 0, 1] for dx in [-1, 0, 1] if dx != 0 or dy != 0
        )
        assert runic_found is True

        # Boss Gate (BOSS_SEAL_BARRIER) exists
        assert map_data.boss_gate is not None
        gx, gy = map_data.boss_gate
        assert map_data.tiles[gy][gx].tile_type == TileType.BOSS_GATE
        gate_obs = map_data.obstacles.get((gx, gy))
        assert gate_obs is not None
        assert gate_obs.obstacle_type == ObstacleType.BOSS_SEAL_BARRIER
        assert gate_obs.hp == 200

        # Ritual Braziers inside arena
        braziers = [
            obs for obs in map_data.obstacles.values()
            if obs.obstacle_type == ObstacleType.BONE_BRAZIER
        ]
        assert len(braziers) >= 2

    def test_boss_gate_breach_mechanic(self):
        """Verifies breach_boss_gate unblocks the boss seal barrier and mutates map state."""
        engine = ProceduralMapEngine(width=48, height=48)
        map_data = engine.generate_map(seed=123, biome_id="SAVAGE_MANGROVE_SWAMP")

        assert map_data.boss_gate is not None
        gx, gy = map_data.boss_gate
        assert map_data.boss_gate_breached is False
        assert map_data.is_walkable(gx, gy) is False

        # Breach the gate
        breached = engine.breach_boss_gate(map_data)
        assert breached is True
        assert map_data.boss_gate_breached is True
        assert map_data.is_walkable(gx, gy) is True

    def test_spatial_partitioning_and_arena_queries(self):
        """Verifies spatial chunking and boss arena bounding helpers."""
        engine = ProceduralMapEngine(width=64, height=64)
        map_data = engine.generate_map(seed=777, biome_id="CRIMSON_BLOOD_FOREST")

        # Chunk coordinate mapping
        assert map_data.get_chunk_coords(0, 0) == (0, 0)
        assert map_data.get_chunk_coords(31, 31) == (1, 1)
        assert map_data.get_chunk_coords(63, 63) == (3, 3)

        # Boss arena query
        br = map_data.boss_room
        assert br is not None
        bcx, bcy = br.center
        assert map_data.is_in_boss_room(bcx, bcy) is True
        assert map_data.is_in_boss_room(map_data.spawn_point[0], map_data.spawn_point[1]) is False

        arena_cells = map_data.get_boss_arena_cells()
        assert len(arena_cells) == br.w * br.h
        assert map_data.get_walkables_count() > 100

    def test_war_fog_ratio_and_cell_queries(self):
        """Verifies WarFogEngine get_explored_ratio, is_cell_visible, and is_cell_explored."""
        fog_engine = WarFogEngine()
        vis = fog_engine.create_player_visibility("p_explore", width=32, height=32, view_radius=6)
        engine = ProceduralMapEngine(width=32, height=32)
        map_data = engine.generate_map(seed=888, biome_id="OUTCAST_MINE_SHAFTS")

        sx, sy = map_data.spawn_point
        fog_engine.update_visibility(vis, map_data, sx, sy)

        assert fog_engine.is_cell_visible(vis, sx, sy) is True
        assert fog_engine.is_cell_explored(vis, sx, sy) is True
        assert fog_engine.get_explored_ratio(vis) > 0.0
        assert fog_engine.get_explored_ratio(vis) <= 1.0

    def test_zero_boss_gate_bypass_and_single_perimeter_entrance(self):
        """Verifies 100% of perimeter cells around Boss Arena are solid walls except exactly 1 boss gate, preventing bypass."""
        for dim in [32, 48, 64, 80]:
            for seed in [101, 202, 303]:
                engine = ProceduralMapEngine(width=dim, height=dim)
                map_data = engine.generate_map(seed=seed, biome_id="CORRUPTED_FIEND_RUINS")
                br = map_data.boss_room
                assert br is not None
                assert map_data.boss_gate is not None
                gx, gy = map_data.boss_gate

                # Count non-wall perimeter cells
                perimeter_cells = []
                for x in range(br.x, br.x + br.w):
                    perimeter_cells.extend([(x, br.y), (x, br.y + br.h - 1)])
                for y in range(br.y + 1, br.y + br.h - 1):
                    perimeter_cells.extend([(br.x, y), (br.x + br.w - 1, y)])

                non_walls = [
                    (px, py) for (px, py) in perimeter_cells
                    if map_data.tiles[py][px].tile_type != TileType.WALL
                ]
                assert len(non_walls) == 1, f"Expected 1 entrance on perimeter, got {len(non_walls)} at seed {seed}, dim {dim}"
                assert non_walls[0] == (gx, gy)
                assert map_data.tiles[gy][gx].tile_type == TileType.BOSS_GATE

                # When gate is sealed, verify boss arena is unreachable from spawn without breach
                unbreached_connected, _ = verify_path_connectivity(
                    map_data, map_data.spawn_point, map_data.boss_point, allow_sealed_boss_gate=False
                )
                assert unbreached_connected is False, f"Boss gate bypassed at seed {seed}, dim {dim}!"

                # Breach the gate
                breach_ok = engine.breach_boss_gate(map_data)
                assert breach_ok is True
                breached_connected, _ = verify_path_connectivity(
                    map_data, map_data.spawn_point, map_data.boss_point, allow_sealed_boss_gate=False
                )
                assert breached_connected is True

