"""
FreeExile M3 Unit Test Suite: Tile-Level Collision & Spawn Validation.
Covers:
1. Direct tile blocking: WALL, CHASM, WATER, BOSS_GATE, VOID.
2. Passable tiles: FLOOR, PATH, DENSE_TERRAIN, POI, ENCOUNTER_*.
3. 2-Axis sliding physics: Diagonal wall collision slides along orthogonal axis.
4. Corner sliding: Sliding along corner tiles without penetration.
5. Player collision radius: Edge penetration prevention at r = 0.35.
6. Out-of-bounds containment: Coordinates < 0 or >= W, H are blocked.
7. Boss Gate locked state blocks movement.
8. Boss Gate unlocking mutates tile to passable and clears collision.
9. Server validate_monster_spawn rejects impassable tiles.
10. Dodge/blink behavior on chasms.
11. Safe Haven purity (Sanctuary & Hideout).
"""

import math
import unittest
from unittest.mock import patch

from server.world.map_data_types import (
    TileType, TileCell, MapGridData, ObstacleInstance, ObstacleType
)
from server.world.zone_engine import ZoneEngine
from server.world.procedural_map_engine import ProceduralMapEngine


def is_position_blocked(grid: MapGridData, wx: float, wy: float, radius: float = 0.35, is_dodge: bool = False) -> bool:
    min_tx = int(math.floor(wx - radius))
    max_tx = int(math.floor(wx + radius))
    min_ty = int(math.floor(wy - radius))
    max_ty = int(math.floor(wy + radius))

    for ty in range(min_ty, max_ty + 1):
        for tx in range(min_tx, max_tx + 1):
            if not grid.is_in_bounds(tx, ty):
                return True
            tile = grid.tiles[ty][tx].tile_type
            if tile == TileType.CHASM and is_dodge:
                continue
            if tile in (TileType.WALL, TileType.CHASM, TileType.WATER, TileType.BOSS_GATE, TileType.VOID) or not tile.is_passable():
                return True
            obs = grid.obstacles.get((tx, ty))
            if obs and not obs.is_destroyed and obs.blocks_movement:
                return True
    return False


def resolve_movement_with_sliding(grid: MapGridData, cur_x: float, cur_y: float, dx: float, dy: float, radius: float = 0.35, is_dodge: bool = False):
    target_x = cur_x + dx
    target_y = cur_y + dy
    if not is_position_blocked(grid, target_x, target_y, radius, is_dodge):
        return target_x, target_y, False

    can_x = dx != 0 and not is_position_blocked(grid, cur_x + dx, cur_y, radius, is_dodge)
    can_y = dy != 0 and not is_position_blocked(grid, cur_x, cur_y + dy, radius, is_dodge)

    if can_x and can_y:
        if abs(dx) >= abs(dy):
            return cur_x + dx, cur_y, True
        return cur_x, cur_y + dy, True
    if can_x:
        return cur_x + dx, cur_y, True
    if can_y:
        return cur_x, cur_y + dy, True
    return cur_x, cur_y, False


class TestTileCollision(unittest.TestCase):
    def setUp(self):
        self.w, self.h = 10, 10
        self.tiles = [
            [TileCell(x=x, y=y, tile_type=TileType.FLOOR, walkable=True) for x in range(self.w)]
            for y in range(self.h)
        ]
        self.grid = MapGridData(width=self.w, height=self.h, seed=1, biome="BLEACHED_BONE_CANYON", tiles=self.tiles)
        self.engine = ZoneEngine()
        ProceduralMapEngine.clear_zone_maps()

    def test_01_direct_tile_blocking(self):
        for impassable in (TileType.WALL, TileType.CHASM, TileType.WATER, TileType.BOSS_GATE, TileType.VOID):
            self.assertFalse(impassable.is_passable())
            self.grid.tiles[5][5].tile_type = impassable
            self.assertTrue(is_position_blocked(self.grid, 5.5, 5.5))

    def test_02_passable_tiles_allow_movement(self):
        for passable in (TileType.FLOOR, TileType.PATH, TileType.DENSE_TERRAIN, TileType.POI, TileType.ENCOUNTER_LOW):
            self.assertTrue(passable.is_passable())
            self.grid.tiles[5][5].tile_type = passable
            self.assertFalse(is_position_blocked(self.grid, 5.5, 5.5))

    def test_03_two_axis_sliding_diagonal_wall(self):
        for x in range(self.w):
            self.grid.tiles[6][x].tile_type = TileType.WALL
        cur_x, cur_y = 3.0, 5.5
        new_x, new_y, sliding = resolve_movement_with_sliding(self.grid, cur_x, cur_y, 0.4, 0.4)
        self.assertTrue(sliding)
        self.assertAlmostEqual(new_x, 3.4, places=2)
        self.assertAlmostEqual(new_y, 5.5, places=2)

    def test_04_corner_sliding_without_penetration(self):
        self.grid.tiles[5][5].tile_type = TileType.WALL
        new_x, new_y, _ = resolve_movement_with_sliding(self.grid, 4.2, 5.8, 0.5, -0.1)
        self.assertFalse(is_position_blocked(self.grid, new_x, new_y))

    def test_05_player_collision_radius_edge_penetration(self):
        self.grid.tiles[5][0].tile_type = TileType.WALL
        self.assertTrue(is_position_blocked(self.grid, 1.2, 5.5, radius=0.35))
        self.assertFalse(is_position_blocked(self.grid, 1.4, 5.5, radius=0.35))

    def test_06_out_of_bounds_containment(self):
        self.assertTrue(is_position_blocked(self.grid, -0.5, 5.0))
        self.assertTrue(is_position_blocked(self.grid, 10.5, 5.0))
        self.assertTrue(is_position_blocked(self.grid, 5.0, -0.5))
        self.assertTrue(is_position_blocked(self.grid, 5.0, 10.5))

    def test_07_boss_gate_locked_blocks_movement(self):
        self.grid.tiles[7][7].tile_type = TileType.BOSS_GATE
        self.grid.obstacles[(7, 7)] = ObstacleInstance("obs_gate", 7, 7, ObstacleType.BOSS_SEAL_BARRIER, blocks_movement=True)
        self.assertTrue(is_position_blocked(self.grid, 7.5, 7.5))

    def test_08_boss_gate_unlocking_mutates_tile_and_clears_collision(self):
        self.grid.tiles[7][7].tile_type = TileType.BOSS_GATE
        self.grid.obstacles[(7, 7)] = ObstacleInstance("obs_gate", 7, 7, ObstacleType.BOSS_SEAL_BARRIER, blocks_movement=True)
        self.grid.tiles[7][7].tile_type = TileType.FLOOR
        self.grid.obstacles[(7, 7)].is_destroyed = True
        self.grid.obstacles[(7, 7)].blocks_movement = False
        self.assertFalse(is_position_blocked(self.grid, 7.5, 7.5))

    def test_09_server_validate_monster_spawn_rejects_impassable(self):
        with patch.object(ProceduralMapEngine, "get_tile_type", return_value=TileType.WALL):
            allowed, reason = self.engine.validate_monster_spawn("zone_tang_kiem_nhai", tx=5, ty=5)
            self.assertFalse(allowed)
            self.assertIn("không thể đi qua", reason)
        with patch.object(ProceduralMapEngine, "get_tile_type", return_value=TileType.CHASM):
            allowed, _ = self.engine.validate_monster_spawn("zone_tang_kiem_nhai", 5, 5)
            self.assertFalse(allowed)
        with patch.object(ProceduralMapEngine, "get_tile_type", return_value=TileType.WATER):
            allowed, _ = self.engine.validate_monster_spawn("zone_tang_kiem_nhai", tx=5, ty=5)
            self.assertFalse(allowed)
        with patch.object(ProceduralMapEngine, "get_tile_type", return_value=TileType.BOSS_GATE):
            allowed, _ = self.engine.validate_monster_spawn("zone_tang_kiem_nhai", tx=5, ty=5)
            self.assertFalse(allowed)
        with patch.object(ProceduralMapEngine, "get_tile_type", return_value=TileType.FLOOR):
            allowed, reason = self.engine.validate_monster_spawn("zone_tang_kiem_nhai", tx=5, ty=5)
            self.assertTrue(allowed)
            self.assertIn("hợp lệ", reason)

    def test_10_dodge_blink_behavior_on_chasms(self):
        self.grid.tiles[4][4].tile_type = TileType.CHASM
        self.assertTrue(is_position_blocked(self.grid, 4.5, 4.5, is_dodge=False))
        self.assertFalse(is_position_blocked(self.grid, 4.5, 4.5, is_dodge=True))
        self.grid.tiles[4][4].tile_type = TileType.WALL
        self.assertTrue(is_position_blocked(self.grid, 4.5, 4.5, is_dodge=True))

    def test_11_safe_haven_strict_purity(self):
        self.assertFalse(self.engine.validate_monster_spawn("zone_player_hideout", is_dummy=False)[0])
        self.assertFalse(self.engine.validate_monster_spawn("zone_boundless_sanctuary", is_dummy=False)[0])
        self.assertTrue(self.engine.validate_monster_spawn("zone_player_hideout", is_dummy=True)[0])
        self.assertTrue(self.engine.validate_monster_spawn("zone_boundless_sanctuary", is_dummy=True)[0])


if __name__ == "__main__":
    unittest.main()
