"""
Unit Tests for Cross-Language Map Styles Catalog Synchronization.
Verifies 100% parity across server/world/map_style_catalog.py, data/map_styles.db,
and client/webapp/js/data/map_style_catalog.js, including legacy biome backward compatibility.
"""

from __future__ import annotations
import json
import re
import sqlite3
import subprocess
from pathlib import Path
from typing import Any, Dict

import pytest

import server.world.map_style_catalog as py_catalog
from server.world.map_style_service import MapStyleService

HEX_REGEX = re.compile(r"^#[0-9a-fA-F]{6}$")

EXPECTED_LEGACY_MAPPINGS = {
    1: ("BLEACHED_BONE_CANYON", "STY_11_HEM_NUI_XUONG_TRANG"),
    2: ("SAVAGE_MANGROVE_SWAMP", "STY_06_BAI_THA_MA_NGAP_MAN"),
    3: ("CRIMSON_BLOOD_FOREST", "STY_02_HUYET_SAT_LAM"),
    4: ("OUTCAST_MINE_SHAFTS", "STY_16_MO_QUANG_LUU_DAY"),
    5: ("CORRUPTED_FIEND_RUINS", "STY_21_PHE_TICH_MA_THAN"),
}

EXPECTED_THEMES = [
    "HOANG_DA_DA_NGOAI",
    "DAM_LAY_DOC_CHUONG",
    "HEM_NUI_COT_MAC",
    "HAM_MO_TA_QUAI",
    "THAN_DIEN_PHE_TICH",
    "HU_KHONG_DI_BIEN",
]


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


@pytest.fixture(scope="module")
def db_service(project_root: Path) -> MapStyleService:
    db_file = project_root / "data" / "map_styles.db"
    return MapStyleService(db_path=db_file)


@pytest.fixture(scope="module")
def js_catalog_data(project_root: Path) -> Dict[str, Any]:
    """Execute Node.js to import and serialize client/webapp/js/data/map_style_catalog.js."""
    js_file = project_root / "client" / "webapp" / "js" / "data" / "map_style_catalog.js"
    assert js_file.is_file(), f"Client catalog file missing: {js_file}"

    script = """
    import catalog from './client/webapp/js/data/map_style_catalog.js';
    const payload = {
        map_styles: catalog.MAP_STYLES,
        code_to_style: catalog.BIOME_CODE_TO_STYLE_ID,
        legacy_map: catalog.LEGACY_BIOME_MAP
    };
    console.log(JSON.stringify(payload));
    """
    proc = subprocess.run(
        ["node", "--input-type=module", "-e", script],
        cwd=str(project_root),
        capture_output=True,
        text=True,
        encoding="utf-8",
        check=True,
    )
    # Filter out potential Node warning lines (e.g. MODULE_TYPELESS_PACKAGE_JSON)
    stdout_lines = [line.strip() for line in (proc.stdout or "").splitlines() if line.strip().startswith("{")]
    assert len(stdout_lines) > 0, f"No JSON output from Node: {proc.stdout} (stderr: {proc.stderr})"
    return json.loads(stdout_lines[-1])


def test_python_catalog_and_db_style_count_parity(db_service: MapStyleService) -> None:
    """Verify that Python catalog and DB both contain exactly 30 styles."""
    py_styles = py_catalog.list_styles()
    db_styles = db_service.list_styles()
    assert len(py_styles) == 30
    assert len(db_styles) == 30


def test_python_catalog_and_db_style_attributes_parity(db_service: MapStyleService) -> None:
    """Verify 1-to-1 attribute match between Python catalog and SQLite database."""
    db_styles = {s.style_id: s for s in db_service.list_styles()}
    py_styles = py_catalog.MAP_STYLES_BY_ID

    assert set(db_styles.keys()) == set(py_styles.keys())

    for s_id, py_s in py_styles.items():
        db_s = db_styles[s_id]
        assert db_s.biome_code == py_s.biome_code
        assert db_s.name_vi == py_s.name_vi
        assert db_s.name_en == py_s.name_en
        assert db_s.theme_category == py_s.theme_category
        assert db_s.theme_category_name == py_s.theme_category_name
        assert db_s.palette.floor == py_s.palette.floor
        assert db_s.palette.wall == py_s.palette.wall
        assert db_s.palette.path == py_s.palette.path
        assert db_s.palette.liquid == py_s.palette.liquid
        assert db_s.palette.ambient == py_s.palette.ambient
        assert db_s.lighting.ambient_hex == py_s.lighting.ambient_hex
        assert pytest.approx(db_s.lighting.fog_density) == py_s.lighting.fog_density
        assert pytest.approx(db_s.hazard.slow_factor) == py_s.hazard.slow_factor
        assert db_s.hazard.dps == py_s.hazard.dps
        assert db_s.legacy_alias == py_s.legacy_alias


def test_python_catalog_and_db_tiles_and_props_parity(db_service: MapStyleService) -> None:
    """Verify tile mappings and props count and attributes match between Python and DB."""
    for s_id in py_catalog.list_available_styles():
        py_tiles = py_catalog.get_tiles_for_style(s_id)
        db_tiles = db_service.get_tiles_for_style(s_id)
        assert len(py_tiles) == 20
        assert len(db_tiles) == 20
        for pt, dt in zip(py_tiles, db_tiles):
            assert pt.tile_code == dt.tile_code
            assert pt.tile_type_name == dt.tile_type_name
            assert pt.texture_path == dt.texture_path
            assert pt.procedural_fallback_color == dt.procedural_fallback_color

        py_props = py_catalog.get_props_for_style(s_id)
        db_props = db_service.get_props_for_style(s_id)
        assert len(py_props) >= 3
        assert len(py_props) == len(db_props)
        for pp, dp in zip(py_props, db_props):
            assert pp.prop_id == dp.prop_id
            assert pp.placement_rule == dp.placement_rule
            assert pytest.approx(pp.spawn_weight) == dp.spawn_weight


def test_js_catalog_style_count_and_keys(js_catalog_data: Dict[str, Any]) -> None:
    """Verify that client JS catalog defines exactly 30 styles and 30 code mappings."""
    js_styles = js_catalog_data["map_styles"]
    js_codes = js_catalog_data["code_to_style"]

    assert len(js_styles) == 30
    assert len(js_codes) == 30
    assert set(js_styles.keys()) == set(py_catalog.MAP_STYLES_BY_ID.keys())


def test_js_catalog_to_python_parity(js_catalog_data: Dict[str, Any]) -> None:
    """Verify client JS styles match Python styles on id, biome_code, palettes, and rules."""
    js_styles = js_catalog_data["map_styles"]

    for s_id, py_s in py_catalog.MAP_STYLES_BY_ID.items():
        assert s_id in js_styles, f"Style {s_id} missing in JS catalog"
        js_s = js_styles[s_id]

        assert js_s["biome_code"] == py_s.biome_code
        assert js_s["name_vi"] == py_s.name_vi
        assert js_s["name_en"] == py_s.name_en
        assert js_s["theme_category"] == py_s.theme_category
        assert js_s["legacy_alias"] == py_s.legacy_alias

        js_pal = js_s["palette"]
        assert js_pal["floor"] == py_s.palette.floor
        assert js_pal["wall"] == py_s.palette.wall
        assert js_pal["path"] == py_s.palette.path
        assert js_pal["liquid"] == py_s.palette.liquid
        assert js_pal["ambient"] == py_s.palette.ambient

        assert pytest.approx(js_s["lighting"]["fog_density"]) == py_s.lighting.fog_density
        assert js_s["hazard"]["dps"] == py_s.hazard.dps


def test_legacy_biomes_backward_compatibility(
    db_service: MapStyleService, js_catalog_data: Dict[str, Any]
) -> None:
    """Verify codes 1-5 map to the 5 canonical biomes across Python, DB, and JS catalogs."""
    js_codes = {int(k): v for k, v in js_catalog_data["code_to_style"].items()}
    js_legacy = js_catalog_data["legacy_map"]

    for code, (legacy_name, expected_style_id) in EXPECTED_LEGACY_MAPPINGS.items():
        # 1. Python catalog verification
        py_style = py_catalog.get_map_style_by_code(code)
        assert py_style is not None
        assert py_style.style_id == expected_style_id
        assert py_style.legacy_alias == legacy_name
        assert py_catalog.LEGACY_BIOME_TO_STYLE_ID[legacy_name] == expected_style_id
        assert py_catalog.get_map_style(legacy_name).style_id == expected_style_id

        # 2. SQLite DB verification
        db_style = db_service.get_style_by_code(code)
        assert db_style is not None
        assert db_style.style_id == expected_style_id
        assert db_style.legacy_alias == legacy_name
        assert db_service.get_style(legacy_name).style_id == expected_style_id

        # 3. Client JS catalog verification
        assert js_codes[code] == expected_style_id
        assert js_legacy[legacy_name] == expected_style_id


def test_thematic_categories_distribution_across_catalogs(
    db_service: MapStyleService, js_catalog_data: Dict[str, Any]
) -> None:
    """Verify 6 themes have exactly 5 styles each in Python, DB, and Client JS."""
    js_styles = js_catalog_data["map_styles"].values()

    for theme in EXPECTED_THEMES:
        py_group = py_catalog.list_styles_by_category(theme)
        assert len(py_group) == 5, f"Python theme {theme} has {len(py_group)} styles, expected 5"

        db_group = [s for s in db_service.list_styles() if s.theme_category == theme]
        assert len(db_group) == 5, f"DB theme {theme} has {len(db_group)} styles, expected 5"

        js_group = [s for s in js_styles if s["theme_category"] == theme]
        assert len(js_group) == 5, f"JS theme {theme} has {len(js_group)} styles, expected 5"


def test_python_catalog_query_helpers() -> None:
    """Verify all lookup helper methods in server/world/map_style_catalog.py."""
    # Canonical ID lookup
    assert py_catalog.get_map_style("STY_01_HOANG_MANG_CO_LO") is not None
    assert py_catalog.get_style("STY_01_HOANG_MANG_CO_LO") is not None

    # Biome code lookup
    assert py_catalog.get_map_style_by_code(6).style_id == "STY_01_HOANG_MANG_CO_LO"
    assert py_catalog.get_style_by_code(6).style_id == "STY_01_HOANG_MANG_CO_LO"
    assert py_catalog.get_map_style_by_code(0) is None
    assert py_catalog.get_map_style_by_code(31) is None

    # List helpers
    all_ids = py_catalog.list_available_styles()
    assert len(all_ids) == 30
    assert len(set(all_ids)) == 30

    ordered_styles = py_catalog.list_styles()
    assert len(ordered_styles) == 30
    assert [s.biome_code for s in ordered_styles] == list(range(1, 31))


def test_hex_colors_validity_across_all_palettes() -> None:
    """Verify that all color strings across all 30 styles conform strictly to #RRGGBB."""
    for s in py_catalog.list_styles():
        for color_name, hex_val in (
            ("floor", s.palette.floor),
            ("wall", s.palette.wall),
            ("path", s.palette.path),
            ("liquid", s.palette.liquid),
            ("ambient", s.palette.ambient),
            ("ambient_hex", s.lighting.ambient_hex),
        ):
            assert HEX_REGEX.match(hex_val), f"Invalid HEX '{hex_val}' in {color_name} of {s.style_id}"
