"""
E2E Test Suite for FreeExile 30 Biomes Procedural Generation & Server Pipeline.
Validates:
1. Procedural generation across all 30 biomes (seeds 1..30).
2. Compact binary serialization across all 30 biomes with header byte 3 mapping to 1..30.
3. Zone canonical biome resolution and override logic.
4. WebApp /api/map query parameter integration with tools/serve_webapp.py.
5. Adversarial buffer corruption and edge case handling.

Rules: Strict typing, functions <= 50 lines, file <= 350 lines (soft cap).
"""

from __future__ import annotations
import socketserver
import struct
import threading
import urllib.parse
import urllib.request
from typing import Generator, List, Tuple
import pytest

from server.world.map_data_types import TileType, MapGridData
from server.world.map_style_catalog import list_styles, get_map_style, get_style_by_code
from server.world.wilderness_map_generator import WildernessMapGenerator, ZONE_DEFAULT_SIZES
from server.world.map_binary_serializer import serialize_map_grid, deserialize_map_grid
from tools.serve_webapp import FreeExileHTTPRequestHandler


@pytest.fixture(scope="module")
def webapp_server() -> Generator[str, None, None]:
    """Spins up a lightweight local test server on an ephemeral port for E2E HTTP testing."""
    server = socketserver.TCPServer(("127.0.0.1", 0), FreeExileHTTPRequestHandler)
    port = server.server_address[1]
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    yield f"http://127.0.0.1:{port}"
    server.shutdown()
    server.server_close()


class TestProceduralGeneration30Biomes:
    """Verifies procedural map generation for all 30 biomes across seeds 1..30."""

    def test_generation_legacy_biomes_baseline(self) -> None:
        """Baseline verification that legacy biomes (codes 1-5) generate valid grids."""
        legacy_biomes = [
            ("BLEACHED_BONE_CANYON", 1),
            ("SAVAGE_MANGROVE_SWAMP", 2),
            ("CRIMSON_BLOOD_FOREST", 3),
            ("OUTCAST_MINE_SHAFTS", 4),
            ("CORRUPTED_FIEND_RUINS", 5),
        ]
        for b_name, b_code in legacy_biomes:
            gen = WildernessMapGenerator(width=60, height=45, biome_id=b_name)
            grid = gen.generate(seed=b_code)
            assert grid.width == 60 and grid.height == 45
            assert grid.biome == b_name
            assert grid.get_walkables_count() > 0

    def test_generation_all_30_biomes(self) -> None:
        all_styles = list_styles()
        assert len(all_styles) == 30, f"Expected 30 canonical styles, found {len(all_styles)}"

        for style in all_styles:
            seed = style.biome_code
            gen = WildernessMapGenerator(width=60, height=45, biome_id=style.style_id)
            grid = gen.generate(seed=seed)

            assert isinstance(grid, MapGridData)
            assert grid.width == 60 and grid.height == 45
            assert 0 <= grid.spawn_point[0] < grid.width and 0 <= grid.spawn_point[1] < grid.height
            assert grid.boss_gate is not None
            assert len(grid.encounter_zones) >= 2
            assert grid.get_walkables_count() >= int(grid.width * grid.height * 0.15)

    def test_seed_determinism_and_divergence(self) -> None:
        style = get_style_by_code(1)
        assert style is not None
        gen = WildernessMapGenerator(width=60, height=45, biome_id=style.style_id)

        m1a = gen.generate(seed=101)
        m1b = gen.generate(seed=101)
        m2 = gen.generate(seed=202)

        tiles_1a = [c.tile_type for row in m1a.tiles for c in row]
        tiles_1b = [c.tile_type for row in m1b.tiles for c in row]
        tiles_2 = [c.tile_type for row in m2.tiles for c in row]

        assert tiles_1a == tiles_1b, "Identical seeds must produce identical maps"
        assert tiles_1a != tiles_2, "Different seeds must produce divergent terrain"

    def test_spawn_clearing_invariance_all_30_biomes(self) -> None:
        for code in range(1, 31):
            style = get_style_by_code(code)
            assert style is not None
            m = WildernessMapGenerator(width=60, height=45, biome_id=style.style_id).generate(seed=code)
            sx, sy = m.spawn_point
            r_sq = 8.0 * 8.0

            for y in range(max(1, int(sy - 8)), min(m.height - 1, int(sy + 9))):
                for x in range(max(1, int(sx - 8)), min(m.width - 1, int(sx + 9))):
                    if (x - sx) ** 2 + (y - sy) ** 2 <= r_sq:
                        cell = m.tiles[y][x]
                        assert cell.walkable, f"Cell ({x},{y}) in biome {code} spawn radius must be walkable"
                        assert cell.tile_type in (TileType.FLOOR, TileType.PATH), f"Cell ({x},{y}) must be FLOOR/PATH"


class TestBinarySerialization30Biomes:
    """Verifies binary serializer matches byte offset 3 to biome_code 1..30."""

    def test_binary_header_legacy_biomes_baseline(self) -> None:
        """Baseline verification that legacy biomes (1-5) serialize with correct byte offset 3."""
        legacy_biomes = [
            ("BLEACHED_BONE_CANYON", 1),
            ("SAVAGE_MANGROVE_SWAMP", 2),
            ("CRIMSON_BLOOD_FOREST", 3),
            ("OUTCAST_MINE_SHAFTS", 4),
            ("CORRUPTED_FIEND_RUINS", 5),
        ]
        for b_name, b_code in legacy_biomes:
            grid = WildernessMapGenerator(width=60, height=45, biome_id=b_name).generate(seed=b_code)
            payload = serialize_map_grid(grid)
            assert payload[:2] == b"FE"
            assert payload[2] == 1
            assert payload[3] == b_code, f"Expected offset 3 to be {b_code}, got {payload[3]}"

    def test_binary_header_byte3_matches_all_30_biome_codes(self) -> None:
        for code in range(1, 31):
            style = get_style_by_code(code)
            assert style is not None
            grid = WildernessMapGenerator(width=60, height=45, biome_id=style.style_id).generate(seed=code)
            payload = serialize_map_grid(grid)

            assert payload[:2] == b"FE", f"Header magic must be b'FE' for biome {code}"
            assert payload[2] == 1, f"Header version must be 1 for biome {code}"
            assert payload[3] == code, f"Header offset 3 must match biome_code {code}, got {payload[3]}"

    def test_binary_payload_length_formula(self) -> None:
        for code in [1, 5, 10, 20, 30]:
            style = get_style_by_code(code)
            assert style is not None
            grid = WildernessMapGenerator(width=60, height=45, biome_id=style.style_id).generate(seed=code)
            payload = serialize_map_grid(grid)

            expected_len = 16 + len(grid.poi_points) * 3 + len(grid.encounter_zones) * 5 + grid.width * grid.height
            assert len(payload) == expected_len, f"Payload length mismatch for biome {code}"

    def test_roundtrip_deserialization_fidelity_30_biomes(self) -> None:
        for code in [1, 7, 14, 21, 28]:
            style = get_style_by_code(code)
            assert style is not None
            grid = WildernessMapGenerator(width=60, height=45, biome_id=style.style_id).generate(seed=code)
            payload = serialize_map_grid(grid)
            d = deserialize_map_grid(payload)

            assert d.width == grid.width and d.height == grid.height
            assert d.spawn_point == grid.spawn_point
            assert d.boss_gate == grid.boss_gate
            for y in range(grid.height):
                for x in range(grid.width):
                    assert d.tiles[y][x].tile_type == grid.tiles[y][x].tile_type


class TestZoneCanonicalBiomeResolution:
    """Verifies zone-to-biome mapping and parameter overrides."""

    def test_canonical_zones_dimensions_and_resolution(self) -> None:
        canonical_zones = [
            "zone_tang_kiem_nhai",
            "zone_ancient_sword_barrow",
            "zone_boundless_sandstorm",
            "zone_boundless_celestial_palace",
        ]
        for zid in canonical_zones:
            gen = WildernessMapGenerator.for_zone(zid)
            expected_dims = ZONE_DEFAULT_SIZES[zid]
            assert gen.width == expected_dims[0] and gen.height == expected_dims[1]
            grid = gen.generate(seed=42)
            assert grid.width == expected_dims[0] and grid.height == expected_dims[1]

    def test_explicit_biome_override_on_zone(self) -> None:
        target_style = get_style_by_code(3) # CRIMSON_BLOOD_FOREST
        assert target_style is not None
        gen = WildernessMapGenerator.for_zone("zone_tang_kiem_nhai", biome_id=target_style.style_id)
        assert gen.biome_id == target_style.style_id
        grid = gen.generate(seed=12)
        assert grid.biome == target_style.style_id
        payload = serialize_map_grid(grid)
        assert payload[3] == 3, f"Overridden biome header byte 3 must be 3, got {payload[3]}"


class TestWebAppApiMapQueryIntegration:
    """Verifies /api/map query parameter integration with serve_webapp.py."""

    def test_api_map_with_zone_and_biome_query_param(self, webapp_server: str) -> None:
        url = f"{webapp_server}/api/map?zone_id=zone_tang_kiem_nhai&seed=77&biome=STY_06_BAI_THA_MA_NGAP_MAN"
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=5) as resp:
            assert resp.status == 200
            assert resp.headers.get("Content-Type") == "application/octet-stream"
            body = resp.read()
            assert body[:2] == b"FE"
            # STY_06_BAI_THA_MA_NGAP_MAN has biome_code 2
            assert body[3] == 2, f"Expected byte 3 to be 2 for STY_06, got {body[3]}"

    def test_api_map_with_integer_biome_query_param(self, webapp_server: str) -> None:
        url = f"{webapp_server}/api/map?zone_id=zone_tang_kiem_nhai&seed=88&biome=18"
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=5) as resp:
            assert resp.status == 200
            body = resp.read()
            assert body[:2] == b"FE"
            assert body[3] == 18, f"Expected byte 3 to be 18 for biome=18, got {body[3]}"

    def test_api_map_default_when_biome_omitted(self, webapp_server: str) -> None:
        url = f"{webapp_server}/api/map?zone_id=zone_tang_kiem_nhai&seed=99"
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=5) as resp:
            assert resp.status == 200
            body = resp.read()
            assert body[:2] == b"FE"
            assert body[3] in range(1, 31), f"Default biome code {body[3]} must be in 1..30"


class TestAdversarialAndEdgeCases:
    """Verifies failure cascading, corrupted buffer handling, and extreme seeds."""

    def test_adversarial_truncated_buffer_raises(self) -> None:
        with pytest.raises(ValueError, match="Buffer too short"):
            deserialize_map_grid(b"FE\x01\x01")

    def test_adversarial_corrupted_magic_header_raises(self) -> None:
        dummy_header = struct.pack("<2sBBHHBBBBBBH", b"XX", 1, 1, 60, 45, 10, 10, 50, 50, 0, 0, 0)
        with pytest.raises(ValueError, match="Invalid map magic header"):
            deserialize_map_grid(dummy_header)

    def test_extreme_seeds_generation(self) -> None:
        style = get_style_by_code(1)
        assert style is not None
        gen = WildernessMapGenerator(width=60, height=45, biome_id=style.style_id)
        for extreme_seed in [0, -9999, 2147483647]:
            grid = gen.generate(seed=extreme_seed)
            assert grid.width == 60 and grid.height == 45
            assert grid.get_walkables_count() > 0
