"""
Unit test suite for FreeExile R&D Asset Pipeline Tools.
Tests LOD Generator, WebP Texture Compressor, and Mipmap Utilities.
"""

from pathlib import Path
import tempfile
from PIL import Image
import pytest

from tools.asset_pipeline.lod_generator import (
    evaluate_lod_generation,
    generate_sprite_lods,
)
from tools.asset_pipeline.mipmap_utils import (
    generate_and_save_mipmaps,
    generate_mipmap_chain,
    save_mipmap_chain,
)
from tools.asset_pipeline.texture_compressor import (
    batch_compress_directory,
    compress_to_webp,
)


@pytest.fixture
def sample_sprite_png(tmp_path: Path) -> Path:
    """Provides a realistic character sprite texture from assets or fallback."""
    canonical = Path(__file__).resolve().parent.parent.parent / "client" / "webapp" / "assets" / "characters" / "char_feral_berserker.png"
    if canonical.exists():
        return canonical

    # Fallback to creating a test texture
    img_path = tmp_path / "test_sprite.png"
    img = Image.new("RGBA", (256, 256), (100, 150, 200, 255))
    img.save(img_path, format="PNG")
    return img_path


class TestLODGenerator:
    def test_generate_sprite_lods(self, sample_sprite_png: Path, tmp_path: Path) -> None:
        out_dir = tmp_path / "lods"
        paths = generate_sprite_lods(sample_sprite_png, out_dir)

        assert "lod0" in paths
        assert "lod1" in paths
        assert "lod2" in paths
        assert "LOD-0" in paths
        assert "LOD-2" in paths

        assert paths["lod0"].exists()
        assert paths["lod1"].exists()
        assert paths["lod2"].exists()

        with Image.open(sample_sprite_png) as orig_img:
            ow, oh = orig_img.size
        with Image.open(paths["lod0"]) as img0:
            assert img0.size == (ow, oh)
        with Image.open(paths["lod1"]) as img1:
            assert img1.size == (ow // 2, oh // 2)
        with Image.open(paths["lod2"]) as img2:
            assert img2.size == (ow // 4, oh // 4)

    def test_lod2_size_budget(self, sample_sprite_png: Path, tmp_path: Path) -> None:
        res = evaluate_lod_generation(sample_sprite_png, tmp_path / "lods_eval")
        assert res.original_size > 0
        assert res.lod2_size_ratio <= 0.40, f"LOD-2 ratio {res.lod2_size_ratio} exceeded 40% budget"

    def test_missing_input_raises_error(self, tmp_path: Path) -> None:
        with pytest.raises(FileNotFoundError):
            generate_sprite_lods(tmp_path / "non_existent.png")


class TestTextureCompressor:
    def test_compress_to_webp(self, sample_sprite_png: Path, tmp_path: Path) -> None:
        out_webp = tmp_path / "compressed.webp"
        res = compress_to_webp(sample_sprite_png, out_webp, quality=80)

        assert res.output_path == out_webp
        assert res.output_path.exists()
        assert res.compressed_size > 0
        assert res.compression_ratio <= 0.50, f"WebP ratio {res.compression_ratio} exceeded 50% budget"
        assert res.saved_bytes > 0
        assert res.reduction_percentage >= 50.0

    def test_batch_compress_directory(self, tmp_path: Path) -> None:
        src_dir = tmp_path / "src_batch"
        src_dir.mkdir()
        for i in range(3):
            img = Image.new("RGBA", (128, 128), (i * 50, 100, 150, 255))
            img.save(src_dir / f"tex_{i}.png")

        out_dir = tmp_path / "out_batch"
        results = batch_compress_directory(src_dir, out_dir, quality=80)
        assert len(results) == 3
        for r in results:
            assert r.output_path.exists()
            assert r.output_path.suffix == ".webp"

    def test_compress_missing_file(self, tmp_path: Path) -> None:
        with pytest.raises(FileNotFoundError):
            compress_to_webp(tmp_path / "missing.png")


class TestMipmapUtils:
    def test_generate_mipmap_chain_dimensions(self) -> None:
        img = Image.new("RGBA", (64, 64), (255, 0, 0, 255))
        chain = generate_mipmap_chain(img, min_dimension=1)

        # 64 -> 32 -> 16 -> 8 -> 4 -> 2 -> 1 (7 levels)
        expected_sizes = [(64, 64), (32, 32), (16, 16), (8, 8), (4, 4), (2, 2), (1, 1)]
        actual_sizes = [m.size for m in chain]
        assert actual_sizes == expected_sizes

    def test_save_mipmap_chain(self, tmp_path: Path) -> None:
        img_path = tmp_path / "mip_test.png"
        Image.new("RGBA", (256, 256), (120, 100, 80, 255)).save(img_path, format="PNG")
        out_dir = tmp_path / "mips"
        paths = save_mipmap_chain(img_path, out_dir, min_dimension=16)
        # 256 -> 128 -> 64 -> 32 -> 16 (5 levels)
        assert len(paths) == 5
        for p in paths:
            assert p.exists()

    def test_generate_and_save_mipmaps_hook(self, tmp_path: Path) -> None:
        img_path = tmp_path / "mip_hook_test.png"
        Image.new("RGBA", (256, 256), (120, 100, 80, 255)).save(img_path, format="PNG")
        paths = generate_and_save_mipmaps(img_path, tmp_path / "hook_mips", min_dimension=32)
        # 256 -> 128 -> 64 -> 32 (4 levels)
        assert len(paths) == 4
        for p in paths:
            assert p.exists()
