"""
Unit Test Suite for FreeExile 30 Map Styles Asset Integrity & Quality.
Verifies:
1. Presence of all 30 style folders under client/webapp/assets/map/styles/.
2. Presence of all 9 required textures per style:
   floor.png, wall.png, path.png, liquid.png, props.png,
   floor_normal.png, wall_normal.png, path_normal.png, liquid_normal.png.
3. Valid non-empty PNG magic header (\\x89PNG\\r\\n\\x1a\\n).
4. Standard dimensions (floor/path/liquid: 64x32 or 128x64 px; wall: 128x128 px).
5. Tangent-space lighting compliance (normal maps Blue channel mean > 128.0).
6. File size efficiency: individual tile files < 60 KB, total asset folder < 10 MB.
7. Adversarial edge cases: corrupt magic, invalid dimensions, inverted normals, oversized files.
"""

from __future__ import annotations

from pathlib import Path
from typing import List, Set, Tuple

import pytest
from PIL import Image, ImageStat

from server.world.map_style_catalog import list_available_styles

PNG_MAGIC: bytes = b"\x89PNG\r\n\x1a\n"

REQUIRED_ASSET_FILES: Tuple[str, ...] = (
    "floor.png",
    "wall.png",
    "path.png",
    "liquid.png",
    "props.png",
    "floor_normal.png",
    "wall_normal.png",
    "path_normal.png",
    "liquid_normal.png",
)

NORMAL_MAP_FILES: Tuple[str, ...] = (
    "floor_normal.png",
    "wall_normal.png",
    "path_normal.png",
    "liquid_normal.png",
)

VALID_TILE_DIMS: Set[Tuple[int, int]] = {(64, 32), (128, 64)}
VALID_WALL_DIMS: Set[Tuple[int, int]] = {(128, 128)}

MAX_TILE_FILE_SIZE_BYTES: int = 60 * 1024  # 60 KB limit
MAX_TOTAL_FOLDER_SIZE_BYTES: int = 10 * 1024 * 1024  # 10 MB limit


def verify_png_magic_header(file_path: Path) -> bool:
    """Verifies that the file exists, is non-empty, and has valid PNG magic bytes."""
    if not file_path.is_file() or file_path.stat().st_size < 8:
        return False
    with open(file_path, "rb") as f:
        header = f.read(8)
    return header == PNG_MAGIC


def compute_blue_channel_mean(file_path: Path) -> float:
    """Computes the mean intensity of the Blue channel across the image."""
    with Image.open(file_path) as img:
        stat = ImageStat.Stat(img)
        if len(stat.mean) >= 3:
            return float(stat.mean[2])
    return 0.0


@pytest.fixture(scope="module")
def project_root() -> Path:
    """Return repository root path."""
    return Path(__file__).resolve().parent.parent.parent


@pytest.fixture(scope="module")
def styles_root(project_root: Path) -> Path:
    """Return canonical styles directory client/webapp/assets/map/styles."""
    return project_root / "client" / "webapp" / "assets" / "map" / "styles"


@pytest.fixture(scope="module")
def canonical_styles() -> List[str]:
    """Retrieve all 30 canonical style IDs."""
    styles = list_available_styles()
    assert len(styles) == 30, f"Expected 30 styles, got {len(styles)}"
    return styles


class TestMapStyleDirectoryStructure:
    """Validates root styles directory and all 30 style subdirectories."""

    def test_styles_root_directory_exists(self, styles_root: Path) -> None:
        """Root styles directory must exist and be a directory."""
        assert styles_root.exists(), f"Styles root does not exist: {styles_root}"
        assert styles_root.is_dir(), f"Styles root is not a directory: {styles_root}"

    def test_all_30_style_subdirectories_exist(
        self, styles_root: Path, canonical_styles: List[str]
    ) -> None:
        """All 30 canonical styles must have a matching lowercase directory."""
        missing = [
            s_id for s_id in canonical_styles
            if not (styles_root / s_id.lower()).is_dir()
        ]
        assert not missing, f"Missing style subdirectories: {missing}"

    def test_no_unrecognized_style_subdirectories(
        self, styles_root: Path, canonical_styles: List[str]
    ) -> None:
        """Only the 30 canonical style subdirectories should be present."""
        expected_dirs = {s_id.lower() for s_id in canonical_styles}
        actual_dirs = {
            p.name for p in styles_root.iterdir() if p.is_dir()
        }
        unexpected = actual_dirs - expected_dirs
        assert not unexpected, f"Unexpected subdirectories in styles folder: {unexpected}"


class TestMapStyleFileCompletenessAndMagic:
    """Validates presence and PNG magic headers for all required style assets."""

    @pytest.mark.parametrize("style_id", list_available_styles())
    def test_style_contains_all_9_required_files(
        self, styles_root: Path, style_id: str
    ) -> None:
        """Each style folder must contain the 9 mandatory asset files."""
        style_dir = styles_root / style_id.lower()
        assert style_dir.is_dir(), f"Style folder not found: {style_dir}"
        missing_files = [
            fname for fname in REQUIRED_ASSET_FILES
            if not (style_dir / fname).is_file()
        ]
        assert not missing_files, f"{style_id} missing required files: {missing_files}"

    @pytest.mark.parametrize("style_id", list_available_styles())
    def test_style_files_have_valid_png_magic_and_non_empty(
        self, styles_root: Path, style_id: str
    ) -> None:
        """Every required file must be non-empty and start with PNG magic bytes."""
        style_dir = styles_root / style_id.lower()
        for fname in REQUIRED_ASSET_FILES:
            fpath = style_dir / fname
            assert fpath.is_file(), f"File does not exist: {fpath}"
            assert fpath.stat().st_size > 0, f"File is empty: {fpath}"
            assert verify_png_magic_header(fpath), f"Invalid PNG magic in {fpath}"

    @pytest.mark.parametrize("style_id", list_available_styles())
    def test_style_files_are_decodable_by_pil(
        self, styles_root: Path, style_id: str
    ) -> None:
        """PIL must successfully decode every required file as a PNG image."""
        style_dir = styles_root / style_id.lower()
        for fname in REQUIRED_ASSET_FILES:
            fpath = style_dir / fname
            with Image.open(fpath) as img:
                assert img.format == "PNG", f"{fpath} format is {img.format}, expected PNG"
                assert img.width > 0 and img.height > 0, f"Invalid dimensions for {fpath}"


class TestMapStyleDimensions:
    """Validates isometric and wall dimension contracts across all 30 styles."""

    @pytest.mark.parametrize("style_id", list_available_styles())
    def test_isometric_tile_dimensions(
        self, styles_root: Path, style_id: str
    ) -> None:
        """Floor, path, and liquid tiles must be either 64x32 or 128x64 px."""
        style_dir = styles_root / style_id.lower()
        tile_files = (
            "floor.png", "path.png", "liquid.png",
            "floor_normal.png", "path_normal.png", "liquid_normal.png",
        )
        for fname in tile_files:
            fpath = style_dir / fname
            with Image.open(fpath) as img:
                dims = (img.width, img.height)
                assert dims in VALID_TILE_DIMS, (
                    f"{style_id}/{fname} dimensions {dims} not in {VALID_TILE_DIMS}"
                )

    @pytest.mark.parametrize("style_id", list_available_styles())
    def test_wall_tile_dimensions(
        self, styles_root: Path, style_id: str
    ) -> None:
        """Wall albedo and normal maps must be 128x128 px for vertical extrusion."""
        style_dir = styles_root / style_id.lower()
        for fname in ("wall.png", "wall_normal.png"):
            fpath = style_dir / fname
            with Image.open(fpath) as img:
                dims = (img.width, img.height)
                assert dims in VALID_WALL_DIMS, (
                    f"{style_id}/{fname} dimensions {dims} not in {VALID_WALL_DIMS}"
                )

    @pytest.mark.parametrize("style_id", list_available_styles())
    def test_props_sheet_dimensions(
        self, styles_root: Path, style_id: str
    ) -> None:
        """Props sheet must be a non-empty image with width >= height."""
        style_dir = styles_root / style_id.lower()
        props_path = style_dir / "props.png"
        with Image.open(props_path) as img:
            assert img.width >= 64, f"{style_id}/props.png width {img.width} < 64"
            assert img.height >= 32, f"{style_id}/props.png height {img.height} < 32"


class TestMapStyleNormalMapsTangentSpace:
    """Validates tangent-space normal maps Blue channel dominance (> 128.0)."""

    @pytest.mark.parametrize("style_id", list_available_styles())
    def test_normal_maps_blue_channel_dominance(
        self, styles_root: Path, style_id: str
    ) -> None:
        """Normal maps must have Blue channel mean > 128 for lighting shaders."""
        style_dir = styles_root / style_id.lower()
        for fname in NORMAL_MAP_FILES:
            fpath = style_dir / fname
            b_mean = compute_blue_channel_mean(fpath)
            assert b_mean > 128.0, (
                f"{style_id}/{fname} B-channel mean {b_mean:.2f} <= 128.0"
            )


class TestMapStyleFileSizeAndBudget:
    """Validates file size budgets for mobile efficiency and fast loading."""

    @pytest.mark.parametrize("style_id", list_available_styles())
    def test_individual_tile_files_under_60kb(
        self, styles_root: Path, style_id: str
    ) -> None:
        """Individual tile textures and normal maps must be strictly < 60 KB."""
        style_dir = styles_root / style_id.lower()
        for fname in REQUIRED_ASSET_FILES:
            fpath = style_dir / fname
            size_bytes = fpath.stat().st_size
            assert size_bytes < MAX_TILE_FILE_SIZE_BYTES, (
                f"{style_id}/{fname} size {size_bytes / 1024:.1f} KB >= 60 KB budget"
            )

    def test_total_assets_folder_size_under_10mb(self, styles_root: Path) -> None:
        """Total size of all assets under map/styles must be strictly < 10 MB."""
        total_size = sum(
            f.stat().st_size for f in styles_root.rglob("*") if f.is_file()
        )
        assert total_size < MAX_TOTAL_FOLDER_SIZE_BYTES, (
            f"Total styles size {total_size / (1024 * 1024):.2f} MB >= 10 MB budget"
        )


class TestMapStyleAssetAdversarialAndEdgeCases:
    """Adversarial stress tests for integrity validators and error handling."""

    def test_corrupt_png_magic_header_detected(self, tmp_path: Path) -> None:
        """Corrupted header bytes must fail verification."""
        fake_file = tmp_path / "corrupt.png"
        fake_file.write_bytes(b"NOT_A_PNG_FILE_HEADER")
        assert not verify_png_magic_header(fake_file)

    def test_truncated_header_file_detected(self, tmp_path: Path) -> None:
        """Files with fewer than 8 bytes must fail verification."""
        fake_file = tmp_path / "truncated.png"
        fake_file.write_bytes(b"\x89PNG")
        assert not verify_png_magic_header(fake_file)

    def test_nonexistent_file_detected(self, tmp_path: Path) -> None:
        """Missing file must safely return False without crashing."""
        ghost = tmp_path / "ghost.png"
        assert not verify_png_magic_header(ghost)

    def test_low_blue_channel_normal_map_detected(self, tmp_path: Path) -> None:
        """Normal map with Blue channel mean <= 128 must be flagged."""
        bad_norm = tmp_path / "bad_normal.png"
        img = Image.new("RGB", (64, 32), (200, 200, 30))
        img.save(bad_norm, format="PNG")
        b_mean = compute_blue_channel_mean(bad_norm)
        assert b_mean <= 128.0

    def test_invalid_tile_dimension_flagged(self) -> None:
        """Arbitrary dimension (e.g. 100x100) must not match tile or wall dimensions."""
        dim = (100, 100)
        assert dim not in VALID_TILE_DIMS
        assert dim not in VALID_WALL_DIMS
