"""
Unit tests for FreeExile Unified WebApp Localization & Multi-Language Synchronization Engine.
Verifies complete 9-language coverage, dynamic canvas overhead nameplate localization,
interaction prompt translation, and reactive hot-swapping between Vietnamese and English.
"""

import unittest
import os
import re
import json
import subprocess
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
WEBAPP_DIR = PROJECT_ROOT / "client" / "webapp"
I18N_JS = WEBAPP_DIR / "js" / "data" / "i18n.js"
I18N_CATALOG_JS = WEBAPP_DIR / "js" / "data" / "i18n_catalog.js"
CHAT_I18N_CATALOG_JS = WEBAPP_DIR / "js" / "data" / "chat_i18n_catalog.js"
ENTITY_RENDERER_JS = WEBAPP_DIR / "js" / "engine" / "entity_renderer.js"
DIALOGUE_NPC_JS = WEBAPP_DIR / "js" / "ui" / "dialogue_npc.js"
TOOLTIPS_JS = WEBAPP_DIR / "js" / "ui" / "tooltips.js"
INDEX_HTML = WEBAPP_DIR / "index.html"


class TestWebAppLocalizationEngine(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """Load and evaluate I18N_CATALOG and CHAT_I18N_CATALOG via node for deep semantic assertions."""
        escaped_path = str(I18N_CATALOG_JS).replace("\\", "/")
        node_script = f"""
        const fs = require('fs');
        const code = fs.readFileSync('{escaped_path}', 'utf-8');
        const fakeWindow = {{}};
        new Function('window', code)(fakeWindow);
        console.log(JSON.stringify(fakeWindow.I18N_CATALOG));
        """
        res = subprocess.run(["node", "-e", node_script], capture_output=True, encoding="utf-8", check=True)
        cls.catalog = json.loads(res.stdout)

        if CHAT_I18N_CATALOG_JS.exists():
            chat_url = CHAT_I18N_CATALOG_JS.resolve().as_uri()
            chat_node_script = f"""
            import('{chat_url}').then(m => {{
                console.log(JSON.stringify(m.CHAT_I18N_CATALOG));
            }});
            """
            res_chat = subprocess.run(["node", "--input-type=module", "-e", chat_node_script], capture_output=True, encoding="utf-8", check=True)
            cls.chat_catalog = json.loads(res_chat.stdout)
        else:
            cls.chat_catalog = {}

    def test_i18n_files_exist_and_imported_in_html(self):
        """i18n_catalog.js and i18n.js must exist and be included in index.html in correct order."""
        self.assertTrue(I18N_CATALOG_JS.exists(), "i18n_catalog.js must exist in js/data/")
        self.assertTrue(I18N_JS.exists(), "i18n.js must exist in js/data/")

        with open(INDEX_HTML, "r", encoding="utf-8") as f:
            html = f.read()

        self.assertIn("js/data/i18n_catalog.js", html, "index.html must import i18n_catalog.js")
        self.assertIn("js/data/i18n.js", html, "index.html must import i18n.js")

        catalog_idx = html.index("js/data/i18n_catalog.js")
        i18n_idx = html.index("js/data/i18n.js")
        self.assertLess(catalog_idx, i18n_idx, "i18n_catalog.js must be imported before i18n.js")

    def test_all_nine_languages_in_catalog(self):
        """I18N_CATALOG must define dictionaries for all 9 required global languages."""
        with open(I18N_CATALOG_JS, "r", encoding="utf-8") as f:
            catalog_code = f.read()

        locales = ["vi", "en", "zh", "ja", "ko", "th", "de", "ru", "es"]
        for loc in locales:
            self.assertRegex(catalog_code, rf"['\"]?{loc}['\"]?\s*:\s*\{{", f"Locale '{loc}' must exist in I18N_CATALOG")

    def test_npc_and_character_translations_parity(self):
        """Essential NPC names, titles, services, and character names must exist in EN and VI."""
        with open(I18N_CATALOG_JS, "r", encoding="utf-8") as f:
            catalog_code = f.read()

        required_keys = [
            "char_name",
            "npc_van_gioi_thuong_nhan_name",
            "npc_van_gioi_thuong_nhan_title",
            "npc_van_gioi_thuong_nhan_service",
            "npc_thiet_tuong_au_da_tu_name",
            "npc_thiet_tuong_au_da_tu_title",
            "npc_thiet_tuong_au_da_tu_service",
            "npc_chan_vo_truyen_thua_name",
            "npc_chan_vo_truyen_thua_title",
            "npc_chan_vo_truyen_thua_service",
            "npc_doc_co_tan_kiem_name",
            "npc_doc_co_tan_kiem_title",
            "npc_doc_co_tan_kiem_service",
            "npc_sa_mac_du_than_name",
            "npc_sa_mac_du_than_title",
            "npc_sa_mac_du_than_service",
        ]

        for key in required_keys:
            self.assertIn(key, catalog_code, f"Translation key '{key}' must exist in I18N_CATALOG")

    def test_entity_renderer_no_hardcoded_vietnamese_player_name(self):
        """Canvas entity renderer must NOT hardcode 'Kẻ Bị Lưu Đày' but query I18N instead."""
        with open(ENTITY_RENDERER_JS, "r", encoding="utf-8") as f:
            code = f.read()

        # Check that ctx.fillText("Kẻ Bị Lưu Đày" is eliminated
        self.assertNotIn('ctx.fillText("Kẻ Bị Lưu Đày"', code, "entity_renderer.js must not hardcode 'Kẻ Bị Lưu Đày'")
        # Must call I18N
        self.assertTrue("I18N" in code, "entity_renderer.js must query I18N for entity nameplates")

    def test_entity_renderer_localized_npc_and_monster_nameplates(self):
        """NPC floating nameplates and monster names must be localized via I18N."""
        with open(ENTITY_RENDERER_JS, "r", encoding="utf-8") as f:
            code = f.read()

        self.assertIn("getNpcName", code, "entity_renderer.js must use I18N.getNpcName")
        self.assertIn("getNpcTitle", code, "entity_renderer.js must use I18N.getNpcTitle")

    def test_dialogue_npc_localized_interaction_and_prompts(self):
        """Nearby NPC interaction titles must dynamically adapt to selected language."""
        with open(DIALOGUE_NPC_JS, "r", encoding="utf-8") as f:
            code = f.read()

        # Must not have static un-localized template string for interaction
        self.assertTrue("I18N" in code, "dialogue_npc.js must integrate with I18N")

    def test_tooltips_support_localization_keys(self):
        """Tooltips engine must look up localized title and description keys."""
        with open(TOOLTIPS_JS, "r", encoding="utf-8") as f:
            code = f.read()

        self.assertTrue(
            "tooltipTitleKey" in code or "I18N" in code or "data-i18n" in code,
            "tooltips.js must support localized tooltip keys"
        )

    def test_dialogue_npc_multilingual_rendering(self):
        """Dialogue modal must resolve node text, choice text, and prompt buttons via I18N."""
        with open(DIALOGUE_NPC_JS, "r", encoding="utf-8") as f:
            code = f.read()

        self.assertIn("dialogueContent", code)
        # Check that node text is translated via I18N.t
        self.assertTrue(
            "I18N.t" in code and "dialogue_" in code,
            "dialogue_npc.js must query I18N.t for dialogue body text with a key"
        )
        # Check that choice text is translated via I18N.t
        self.assertTrue(
            "dialogue_choice_" in code or "ch.textKey" in code or "I18N.t(" in code,
            "dialogue_npc.js must query I18N.t for choice texts"
        )
        # Check that select prompt is localized
        self.assertIn("dialogue_select", code, "dialogue_npc.js must localize 'Chọn ↵' button with 'dialogue_select'")

    def test_modal_headers_and_tabs_i18n_attributes(self):
        """Important modal titles and sub-tabs in index.html must have data-i18n attributes."""
        with open(INDEX_HTML, "r", encoding="utf-8") as f:
            html = f.read()
        templates_dir = WEBAPP_DIR / "templates"
        if templates_dir.is_dir():
            for tpl in templates_dir.glob("*.html"):
                html += "\n" + tpl.read_text(encoding="utf-8")

        expected_i18n_keys = [
            'data-i18n="modal_meridian_title"',
            'data-i18n="modal_vault_title"',
            'data-i18n="vault_tab_market"',
            'data-i18n="vault_tab_guild"',
            'data-i18n="vault_tab_stats"',
            'data-i18n="shop_tab_coins"',
        ]
        for key in expected_i18n_keys:
            self.assertIn(key, html, f"index.html or templates must include {key} for automatic localization")

    def test_dialogue_and_ui_keys_in_catalog(self):
        """Catalog must contain essential keys for dialogue choices, notices, and modal tabs in EN and VI."""
        with open(I18N_CATALOG_JS, "r", encoding="utf-8") as f:
            catalog = f.read()

        keys_to_verify = [
            "dialogue_select",
            "vault_tab_market",
            "vault_tab_guild",
            "vault_tab_stats",
            "shop_tab_coins",
            "notice_forge_active",
            "notice_quest_doc_co"
        ]
        for k in keys_to_verify:
            self.assertIn(k, catalog, f"Key '{k}' must exist in I18N_CATALOG")

    def test_full_nine_language_key_parity_and_count(self):
        """100% key parity across all 9 languages (all 143 keys present in vi, en, zh, ja, ko, th, de, ru, es)."""
        locales = ["vi", "en", "zh", "ja", "ko", "th", "de", "ru", "es"]
        master_keys = set(self.catalog["vi"].keys())
        self.assertEqual(len(master_keys), 143, f"Expected 143 keys in master catalog, found {len(master_keys)}")

        for loc in locales:
            self.assertIn(loc, self.catalog, f"Locale '{loc}' missing from I18N_CATALOG")
            loc_keys = set(self.catalog[loc].keys())
            missing = master_keys - loc_keys
            self.assertEqual(len(missing), 0, f"Locale '{loc}' is missing {len(missing)} keys: {missing}")
            extra = loc_keys - master_keys
            self.assertEqual(len(extra), 0, f"Locale '{loc}' has unexpected extra keys: {extra}")
            self.assertEqual(len(loc_keys), 143)
            for k, val in self.catalog[loc].items():
                self.assertTrue(isinstance(val, str) and len(val.strip()) > 0, f"Key '{k}' in [{loc}] must be non-empty string")

    def test_microcopy_brevity_one_to_two_words_all_nine_languages(self):
        """1-2 word microcopy assertion on action labels, tabs, and buttons across all 9 languages."""
        short_prefixes = ("agent_stance_",)
        explicit_short = {
            "dodge", "buyout", "agent_btn_revoke", "dialogue_select", "dialogue_btn_close", "dialogue_btn_leave"
        }
        emoji_cleaner = re.compile(r"[\U00010000-\U0010ffff\u2600-\u27ff\ufe00-\ufe0f]")

        for loc, dict_loc in self.catalog.items():
            for key, val in dict_loc.items():
                is_short = (
                    any(key.startswith(p) for p in short_prefixes) or
                    key in explicit_short
                )
                if is_short and not key.endswith("_desc") and not key.endswith("_subtitle"):
                    clean = emoji_cleaner.sub("", val).strip()
                    if loc in ["vi", "en", "de", "ru", "es", "ko"]:
                        words = clean.split()
                        self.assertLessEqual(len(words), 2, f"UI Label in [{loc}] '{key}' exceeds 2 words: '{val}'")
                    elif loc == "zh":
                        chars = len([c for c in clean if not c.isspace()])
                        self.assertLessEqual(chars, 4, f"UI Label in [{loc}] '{key}' exceeds 4 characters: '{val}'")
                    elif loc == "ja":
                        chars = len([c for c in clean if not c.isspace()])
                        self.assertLessEqual(chars, 6, f"UI Label in [{loc}] '{key}' exceeds 6 characters: '{val}'")
                    elif loc == "th":
                        self.assertLessEqual(len(clean), 12, f"UI Label in [{loc}] '{key}' exceeds 12 characters: '{val}'")

    def test_zero_bilingual_or_parenthetical_formats_in_button_labels(self):
        """Zero '(' or ')' bilingual formats across action labels, button titles, and interactive controls."""
        banned_prefixes = ("vault_tab_", "shop_tab_", "char_name", "buyout", "dodge", "skill_", "agent_")
        for loc, dict_loc in self.catalog.items():
            for key, val in dict_loc.items():
                if any(key.startswith(p) for p in banned_prefixes) and not key.endswith("_desc"):
                    self.assertNotIn("(", val, f"Bilingual paren '(' forbidden in [{loc}] '{key}': '{val}'")
                    self.assertNotIn(")", val, f"Bilingual paren ')' forbidden in [{loc}] '{key}': '{val}'")

    def test_all_tooltip_descriptions_are_single_sentence_technical_specs(self):
        """All tooltip descriptions are single-sentence technical specs (no newlines, <= 100 characters)."""
        desc_keys = [k for k in self.catalog["vi"].keys() if k.endswith("_desc")]
        self.assertGreaterEqual(len(desc_keys), 20, "Must have at least 20 tooltip/skill description keys")

        for loc, dict_loc in self.catalog.items():
            for key in desc_keys:
                val = dict_loc[key]
                self.assertNotIn("\n", val, f"Description [{loc}] '{key}' contains illegal newline: '{val}'")
                self.assertLessEqual(len(val), 100, f"Description [{loc}] '{key}' exceeds 100 chars ({len(val)}): '{val}'")
                stripped = val.rstrip(".。")
                self.assertNotIn(". ", stripped, f"Description [{loc}] '{key}' must be single sentence: '{val}'")
                self.assertNotIn("。", stripped, f"Description [{loc}] '{key}' must be single sentence: '{val}'")

    def test_html_referenced_keys_one_hundred_percent_coverage(self):
        """100% coverage of keys referenced in client/webapp/index.html and HUD tooltips."""
        html_content = INDEX_HTML.read_text(encoding="utf-8")
        referenced_keys = set()
        for m in re.finditer(r'data-(?:i18n|tooltip-title-key|tooltip-desc-key)=["\']([a-zA-Z0-9_-]+)["\']', html_content):
            referenced_keys.add(m.group(1))

        restored_hud_keys = [
            "tooltip_divine_vault_title", "tooltip_divine_vault_desc",
            "tooltip_character_title", "tooltip_character_desc",
            "tooltip_bag_title", "tooltip_bag_desc"
        ]
        for rhk in restored_hud_keys:
            self.assertIn(rhk, referenced_keys, f"HUD key '{rhk}' must be referenced in index.html")

        for loc in ["vi", "en", "zh", "ja", "ko", "th", "de", "ru", "es"]:
            catalog_keys = set(self.catalog[loc].keys())
            if hasattr(self, "chat_catalog") and loc in self.chat_catalog:
                catalog_keys.update(self.chat_catalog[loc].keys())
            missing = referenced_keys - catalog_keys
            self.assertEqual(len(missing), 0, f"HTML references keys missing in [{loc}]: {missing}")

    def test_code_hygiene_line_limits(self):
        """Verify i18n files respect FreeExile code hygiene constraints."""
        with open(I18N_JS, "r", encoding="utf-8") as f:
            i18n_lines = len(f.readlines())
        self.assertLessEqual(i18n_lines, 350, f"i18n.js must not exceed 350 lines (currently {i18n_lines})")

        with open(I18N_CATALOG_JS, "r", encoding="utf-8") as f:
            catalog_lines = len(f.readlines())
        self.assertLessEqual(catalog_lines, 700, f"i18n_catalog.js must not exceed 700 lines (currently {catalog_lines})")


if __name__ == "__main__":
    unittest.main()

