"""
Adversarial Stress Testing & Empirical Verification Suite:
Wilderness Map Generator Sinuosity, Safe Radius, Isolated Boss Gate,
Encounter Zones, and Binary Serialization (Milestone M1-1).

Empirically challenges server/world/wilderness_map_generator.py and
server/world/map_binary_serializer.py across 500+ random seeds and all canonical zones.
"""

from __future__ import annotations
import math
import struct
from typing import Dict, List, Set, Tuple
import pytest

from server.world.map_data_types import (
    TileType,
    TileCell,
    MapGridData,
    EncounterZone,
)
from server.world.wilderness_map_generator import (
    WildernessMapGenerator,
    ZONE_DEFAULT_SIZES,
)
from server.world.map_binary_serializer import (
    serialize_map_grid,
    deserialize_map_grid,
    HEADER_MAGIC,
    HEADER_SIZE,
    BIOME_TO_CODE,
)
from server.world.procedural_map_engine import (
    verify_path_connectivity,
    calculate_path_sinuosity,
)


CANONICAL_ZONES: List[str] = list(ZONE_DEFAULT_SIZES.keys()) + ["zone_boundless_sanctuary"]


class TestWildernessMapGeneratorAdversarialStress:
    """Empirical challenge suite testing 500+ maps across all canonical zones."""

    def test_stress_500_plus_random_seeds_all_zones(self) -> None:
        """Evaluates 550 maps across 10 zones (55 seeds each) against all 5 criteria."""
        total_tested = 0
        min_sinuosity = 999.0

        for zone_idx, zone_id in enumerate(CANONICAL_ZONES):
            gen = WildernessMapGenerator.for_zone(zone_id)
            for i in range(55):
                seed = 100000 * (zone_idx + 1) + i * 79 + 31
                total_tested += 1
                map_data = gen.generate(seed=seed)
                sx, sy = map_data.spawn_point

                # 1. Spawn safe radius 8.0: exactly 0 WALL tiles, 100% walkable
                self._verify_spawn_safe_zone(map_data, sx, sy, seed, zone_id)

                # 2. Isolated Boss Arena & Gate: exactly 1 gate, sealed perimeter
                self._verify_boss_arena_isolation(map_data, seed, zone_id)

                # 3. Encounter zones: count is 2 or 3, all encounter cells walkable
                self._verify_encounter_zones(map_data, seed, zone_id)

                # 4. Sinuous main path: connectivity exists and sinuosity >= 1.25
                sin = self._verify_sinuosity(map_data, seed, zone_id)
                if sin < min_sinuosity:
                    min_sinuosity = sin

                # 5. Binary roundtrip: lossless bit-for-bit roundtrip
                self._verify_binary_roundtrip(map_data, seed, zone_id)

        assert total_tested >= 500
        assert min_sinuosity >= 1.25

    def _verify_spawn_safe_zone(self, m: MapGridData, sx: int, sy: int, seed: int, zid: str) -> None:
        """Asserts zero WALL tiles and 100% walkability in radius 8.0."""
        for y in range(max(0, sy - 8), min(m.height, sy + 9)):
            for x in range(max(0, sx - 8), min(m.width, sx + 9)):
                if (x - sx) ** 2 + (y - sy) ** 2 <= 64:
                    cell = m.tiles[y][x]
                    assert cell.tile_type != TileType.WALL, f"WALL in safe radius at ({x},{y}), seed={seed}, zone={zid}"
                    assert cell.walkable is True, f"Blocked cell in safe radius at ({x},{y}), seed={seed}, zone={zid}"

    def _verify_boss_arena_isolation(self, m: MapGridData, seed: int, zid: str) -> None:
        """Asserts exactly 1 BOSS_GATE and solid WALL perimeter sealing."""
        gates = [c for row in m.tiles for c in row if c.tile_type == TileType.BOSS_GATE]
        assert len(gates) == 1, f"Found {len(gates)} boss gates, expected 1, seed={seed}, zone={zid}"
        assert m.boss_gate == (gates[0].x, gates[0].y)

        br = m.boss_room
        assert br is not None
        perimeter = [(x, y) for x in range(br.x, br.x + br.w) for y in (br.y, br.y + br.h - 1)] + \
                    [(x, y) for y in range(br.y + 1, br.y + br.h - 1) for x in (br.x, br.x + br.w - 1)]
        for px, py in perimeter:
            cell = m.tiles[py][px]
            if (px, py) == m.boss_gate:
                assert cell.tile_type == TileType.BOSS_GATE
                assert cell.walkable is False
            else:
                assert cell.tile_type == TileType.WALL, f"Perimeter leak at ({px},{py}), seed={seed}, zone={zid}"
                assert cell.walkable is False

    def _verify_encounter_zones(self, m: MapGridData, seed: int, zid: str) -> None:
        """Asserts 2-3 encounter zones with valid bounds and walkable cells."""
        assert 2 <= len(m.encounter_zones) <= 3, f"Encounter count {len(m.encounter_zones)} not 2-3, seed={seed}"
        for enc in m.encounter_zones:
            assert 0 <= enc.min_x < enc.max_x < m.width
            assert 0 <= enc.min_y < enc.max_y < m.height
            assert enc.tier in (1, 2, 3)

        enc_cells = [c for row in m.tiles for c in row if c.tile_type in (
            TileType.ENCOUNTER_LOW, TileType.ENCOUNTER_MEDIUM, TileType.ENCOUNTER_HIGH
        )]
        assert len(enc_cells) >= 15
        assert all(c.walkable for c in enc_cells)

    def _verify_sinuosity(self, m: MapGridData, seed: int, zid: str) -> float:
        """Asserts path connectivity and sinuosity >= 1.25."""
        ok, path = verify_path_connectivity(m, m.spawn_point, m.boss_gate, allow_sealed_boss_gate=True)
        assert ok is True, f"Main path disconnected for seed={seed}, zone={zid}"
        sin = calculate_path_sinuosity(path, m.spawn_point, m.boss_gate)
        assert sin >= 1.25, f"Sinuosity {sin} < 1.25 for seed={seed}, zone={zid}"
        return sin

    def _verify_binary_roundtrip(self, m: MapGridData, seed: int, zid: str) -> None:
        """Asserts zero bit-level drift across serialization roundtrip."""
        b1 = serialize_map_grid(m)
        m2 = deserialize_map_grid(b1)
        b2 = serialize_map_grid(m2)
        assert b1 == b2, f"Bit drift detected on re-serialization, seed={seed}, zone={zid}"
        assert m2.width == m.width and m2.height == m.height
        assert m2.spawn_point == m.spawn_point and m2.boss_gate == m.boss_gate
        assert len(m2.encounter_zones) == len(m.encounter_zones)
        assert len(m2.poi_points) == len(m.poi_points)

    def test_boss_arena_strict_impenetrability_when_locked(self) -> None:
        """Adversarial check: with locked boss gate, boss room is 100% impenetrable."""
        gen = WildernessMapGenerator.for_zone("zone_tang_kiem_nhai")
        for seed in [11, 22, 33, 44, 55]:
            m = gen.generate(seed=seed)
            altar = m.boss_point
            ok, _ = verify_path_connectivity(m, m.spawn_point, altar, allow_sealed_boss_gate=False)
            assert ok is False, f"Boss arena penetrated while gate locked! seed={seed}"

            # Interior flood fill: cannot leak to exterior
            interior_visited = self._flood_fill(m, altar, blocked_gate=True)
            br = m.boss_room
            assert br is not None
            for ix, iy in interior_visited:
                assert br.x < ix < br.x + br.w - 1, f"Interior flood fill escaped boss arena at x={ix}"
                assert br.y < iy < br.y + br.h - 1, f"Interior flood fill escaped boss arena at y={iy}"

    def _flood_fill(self, m: MapGridData, start: Tuple[int, int], blocked_gate: bool) -> Set[Tuple[int, int]]:
        """Breadth-first search finding all connected walkable tiles."""
        visited: Set[Tuple[int, int]] = set()
        queue = [start]
        visited.add(start)
        while queue:
            cx, cy = queue.pop(0)
            for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
                nx, ny = cx + dx, cy + dy
                if (nx, ny) in visited or not m.is_in_bounds(nx, ny):
                    continue
                cell = m.tiles[ny][nx]
                if cell.tile_type == TileType.WALL:
                    continue
                if blocked_gate and cell.tile_type == TileType.BOSS_GATE:
                    continue
                visited.add((nx, ny))
                queue.append((nx, ny))
        return visited

    def test_all_pois_and_dead_ends_reachable_from_spawn(self) -> None:
        """Ensures player can traverse to all POIs, dead ends, and encounter centers."""
        for zid in CANONICAL_ZONES:
            gen = WildernessMapGenerator.for_zone(zid)
            m = gen.generate(seed=777)
            for px, py, _ in m.poi_points:
                ok, _ = verify_path_connectivity(m, m.spawn_point, (px, py), allow_sealed_boss_gate=True)
                assert ok is True, f"Unreachable POI at ({px},{py}) in {zid}"

            for dx, dy in m.dead_end_points:
                ok, _ = verify_path_connectivity(m, m.spawn_point, (dx, dy), allow_sealed_boss_gate=True)
                assert ok is True, f"Unreachable dead end at ({dx},{dy}) in {zid}"

            for enc in m.encounter_zones:
                center = ((enc.min_x + enc.max_x) // 2, (enc.min_y + enc.max_y) // 2)
                ok, _ = verify_path_connectivity(m, m.spawn_point, center, allow_sealed_boss_gate=True)
                assert ok is True, f"Unreachable encounter zone center at {center} in {zid}"

    def test_extreme_seed_fuzzing(self) -> None:
        """Fuzzes extreme random seeds (negative, zero, large int64)."""
        gen = WildernessMapGenerator.for_zone("zone_tang_kiem_nhai")
        extreme_seeds = [-999999, -1, 0, 1, 2**31 - 1, 2**40 + 7]
        for seed in extreme_seeds:
            m = gen.generate(seed=seed)
            assert m.width == 60 and m.height == 45
            assert len(m.encounter_zones) in (2, 3)
            assert m.boss_gate is not None
            b = serialize_map_grid(m)
            assert len(b) > HEADER_SIZE

    def test_binary_serializer_adversarial_tampering(self) -> None:
        """Verifies binary deserializer rejects malformed, truncated, and corrupt payloads."""
        gen = WildernessMapGenerator.for_zone("zone_tang_kiem_nhai")
        valid_bytes = bytearray(serialize_map_grid(gen.generate(seed=123)))

        # 1. Truncated buffer below HEADER_SIZE
        with pytest.raises(ValueError, match="Buffer too short"):
            deserialize_map_grid(bytes(valid_bytes[:10]))

        # 2. Corrupted magic header
        corrupt_magic = bytearray(valid_bytes)
        corrupt_magic[0] = ord(b"X")
        with pytest.raises(ValueError, match="Invalid map magic"):
            deserialize_map_grid(bytes(corrupt_magic))

        # 3. Truncated tile payload
        with pytest.raises(ValueError, match="Buffer truncated"):
            deserialize_map_grid(bytes(valid_bytes[:-50]))
