"""
Unit Test Suite for FreeExile WebApp Multi-Channel Chat UI & 2.5D Item Tooltip Integration.
Verifies DOM elements, channel keys, FIFO ring buffer, rich item tag parsing,
anti-spam cooldowns, 9-language localization, and key navigation in WebApp.
"""

from __future__ import annotations

import json
import re
import subprocess
import unittest
from pathlib import Path
from typing import Any, Dict, List

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
WEBAPP_DIR = PROJECT_ROOT / "client" / "webapp"
INDEX_HTML = WEBAPP_DIR / "index.html"
MAIN_JS = WEBAPP_DIR / "js" / "main.js"
CHAT_UI_JS = WEBAPP_DIR / "js" / "ui" / "chat_ui.js"
CHAT_MANAGER_TS = PROJECT_ROOT / "client" / "src" / "chat" / "ChatManager.ts"


class TestWebappChatUI(unittest.TestCase):
    """Unit tests for FreeExile WebApp Chat Dock, Ring Buffer, and Item Tooltips."""

    @classmethod
    def setUpClass(cls) -> None:
        cls.html = INDEX_HTML.read_text(encoding="utf-8")
        cls.main_js = MAIN_JS.read_text(encoding="utf-8")
        cls.chat_ui_js = CHAT_UI_JS.read_text(encoding="utf-8")
        cls.chat_mgr_ts = CHAT_MANAGER_TS.read_text(encoding="utf-8")

    def _eval_chat_ui_node(self, script: str) -> Any:
        """Helper to evaluate JS snippets using chat_ui.js in Node.js."""
        escaped_path = str(CHAT_UI_JS).replace("\\", "/")
        full_script = f"""
        import('file:///{escaped_path}').then(chatModule => {{
            {script}
        }}).catch(err => {{
            console.error(err);
            process.exit(1);
        }});
        """
        res = subprocess.run(
            ["node", "--input-type=module"],
            input=full_script.encode("utf-8"),
            capture_output=True,
            check=True
        )
        out = res.stdout.decode("utf-8").strip()
        return json.loads(out) if out else None

    @staticmethod
    def _mock_dom_js() -> str:
        return """
        const _createEl = (tag) => ({
            tagName: tag, id: '', className: '', innerHTML: '', textContent: '', children: [],
            classList: { _c: new Set(), add(c){this._c.add(c);}, remove(c){this._c.delete(c);}, contains(c){return this._c.has(c);} },
            appendChild(c){ this.children.push(c); return c; }, querySelectorAll(){ return []; }, addEventListener(){}
        });
        const elements = {};
        global.document = { getElementById: (id) => elements[id] || (elements[id] = _createEl('div')), createElement: (t) => _createEl(t) };
        """

    # --- Group 1: DOM Elements in index.html ---

    def test_01_chat_dock_mounted_in_index_html(self) -> None:
        """Chat dock and minimized ticker must exist in index.html."""
        for eid in ["chat-dock", "chat-minimized-bar", "chat-ticker-channel", "chat-ticker-text",
                    "chat-expanded-panel", "chat-messages-container", "chat-input", "chat-send-btn",
                    "chat-cooldown-badge", "chat-btn-collapse"]:
            self.assertIn(f'id="{eid}"', self.html)

    def test_02_all_eight_channel_tabs_in_index_html(self) -> None:
        """All 8 channel tabs must be present with valid data-channel-id in index.html."""
        tabs = re.findall(r'<button\b[^>]*data-channel-id="(\d+)"[^>]*>(.*?)</button>', self.html)
        channel_ids = [int(t[0]) for t in tabs]
        self.assertEqual(sorted(channel_ids), [1, 2, 3, 4, 5, 6, 7, 8])

        expected_labels = {
            1: "Thế Giới", 2: "Khu Vực", 3: "Bang Hội", 4: "Đội Ngũ",
            5: "Mật Thư", 6: "Hệ Thống", 7: "Chiêu Mộ", 8: "Góp Ý"
        }
        for cid, label in tabs:
            clean_label = label.strip()
            self.assertEqual(clean_label, expected_labels[int(cid)])
            self.assertLessEqual(len(clean_label.split()), 2, "Tab label must be <= 2 words")

    def test_03_item_tooltip_modal_in_index_html(self) -> None:
        """Rich 2.5D Cổ Võ item link tooltip modal must be mounted in index.html."""
        for eid in ["modal-item-link-tooltip", "item-tooltip-icon", "item-tooltip-name", "item-tooltip-sub",
                    "item-tooltip-affixes", "item-tooltip-crafter", "item-tooltip-hmac-badge", "btn-close-item-tooltip"]:
            self.assertIn(f'id="{eid}"', self.html)
        self.assertIn('✓ HMAC Xác Thực', self.html)

    def test_04_script_inclusion_in_index_html(self) -> None:
        """chat_ui.js must be imported as a Native ES Module in index.html."""
        self.assertIn('<script type="module" src="js/ui/chat_ui.js"></script>', self.html)

    # --- Group 2: JavaScript Logic & Ring Buffer ---

    def test_05_channel_constants_and_cooldowns(self) -> None:
        """CHANNELS constant in chat_ui.js must define all 8 channels with SLA cooldowns."""
        data = self._eval_chat_ui_node("console.log(JSON.stringify(chatModule.CHANNELS));")
        self.assertEqual(len(data), 8)
        expected_cd = {1: 15000, 2: 3000, 3: 500, 4: 200, 5: 500, 6: 0, 7: 10000, 8: 5000}
        for cid, cd in expected_cd.items():
            self.assertEqual(data[str(cid)]["cooldownMs"], cd)

    def test_06_fifo_ring_buffer_100_capacity(self) -> None:
        """Ring buffer must cap channel history to strictly 100 messages."""
        data = self._eval_chat_ui_node("""
            for (let i = 1; i <= 120; i++) {
                chatModule.addChatMessage({
                    messageId: `msg_${i}`,
                    senderName: `User_${i}`,
                    channel: 1,
                    displayContent: `Message content ${i}`,
                    linkedItems: [],
                    timestampMs: Date.now() + i
                });
            }
            const history = chatModule.getChannelHistory(1);
            console.log(JSON.stringify({
                total: history.length,
                firstId: history[0].messageId,
                lastId: history[history.length - 1].messageId
            }));
        """)
        self.assertEqual(data["total"], 100)
        self.assertEqual(data["firstId"], "msg_21")
        self.assertEqual(data["lastId"], "msg_120")

    def test_07_rich_item_tag_parsing(self) -> None:
        """parseItemTags must parse single and multiple [item:uuid:sig:name:rarity] tags."""
        data = self._eval_chat_ui_node("""
            const content = 'Khoe kiếm [item:uuid-101:sig-aaa:Thanh Phong Kiếm:4] và giáp [item:uuid-102:sig-bbb:Huyền Thiết Giáp:3]!';
            const parsed = chatModule.parseItemTags(content);
            console.log(JSON.stringify(parsed));
        """)
        self.assertEqual(len(data), 2)
        self.assertEqual(data[0]["itemUuid"], "uuid-101")
        self.assertEqual(data[0]["signature"], "sig-aaa")
        self.assertEqual(data[0]["itemName"], "Thanh Phong Kiếm")
        self.assertEqual(data[0]["rarity"], 4)

        self.assertEqual(data[1]["itemUuid"], "uuid-102")
        self.assertEqual(data[1]["signature"], "sig-bbb")
        self.assertEqual(data[1]["itemName"], "Huyền Thiết Giáp")
        self.assertEqual(data[1]["rarity"], 3)

    def test_08_item_tag_formatting(self) -> None:
        """formatItemTag formats tag from arguments and snapshot objects."""
        data = self._eval_chat_ui_node("""
            const tag1 = chatModule.formatItemTag('Huyết Đao', 5, 'uuid-777', 'sig-888');
            const tag2 = chatModule.formatItemTag({
                itemUuid: 'uuid-999',
                signature: 'sig-999',
                itemName: 'Thái Cổ Thần Thương',
                rarity: 5
            });
            console.log(JSON.stringify({ tag1, tag2 }));
        """)
        self.assertEqual(data["tag1"], "[item:uuid-777:sig-888:Huyết Đao:5]")
        self.assertEqual(data["tag2"], "[item:uuid-999:sig-999:Thái Cổ Thần Thương:5]")

    def test_09_html_rendering_and_xss_sanitization(self) -> None:
        """renderMessageHtml must escape raw HTML entities while rendering clickable item tags."""
        data = self._eval_chat_ui_node("""
            const unsafe = '<script>alert(1)</script> [item:u1:s1:Bảo Kiếm:4] & "test"';
            const html = chatModule.renderMessageHtml(unsafe);
            console.log(JSON.stringify(html));
        """)
        self.assertNotIn("<script>", data)
        self.assertIn("&lt;script&gt;", data)
        self.assertIn("chat-item-link", data)
        self.assertIn('data-uuid="u1"', data)
        self.assertIn('data-sig="s1"', data)

    # --- Group 3: Localization & Microcopy ---

    def test_10_multilingual_channel_dictionary(self) -> None:
        """CHANNEL_I18N must provide 8-channel translations across all 9 languages."""
        data = self._eval_chat_ui_node("""
            console.log(JSON.stringify(chatModule.CHANNEL_I18N));
        """)
        locales = ["vi", "en", "zh", "ja", "ko", "th", "de", "ru", "es"]
        required_keys = ["world", "zone", "guild", "party", "whisper", "system", "recruit", "feedback"]
        for loc in locales:
            self.assertIn(loc, data, f"Locale {loc} missing in CHANNEL_I18N")
            for k in required_keys:
                self.assertIn(k, data[loc], f"Channel {k} missing in locale {loc}")
                word_count = len(data[loc][k].strip().split())
                self.assertLessEqual(word_count, 2, f"Channel {k} in {loc} exceeds 2 words: {data[loc][k]}")

    def test_11_chat_buttons_microcopy_and_no_bilingual(self) -> None:
        """All buttons in chat UI must adhere to <= 2 words and zero bilingual parens."""
        btns = re.findall(r'<button\b[^>]*class="[^"]*chat-[^"]*"[^>]*>(.*?)</button>', self.html)
        for b in btns:
            clean = re.sub(r'<[^>]+>', '', b).strip()
            if not clean:
                continue
            words = clean.split()
            self.assertLessEqual(len(words), 2, f"Chat button text exceeds 2 words: {clean}")
            self.assertNotIn(" (", clean, f"Chat button text contains bilingual parens: {clean}")

    # --- Group 4: Main.js Keyboard Wiring & Client Engine ---

    def test_12_main_js_wires_chat_enter_and_escape(self) -> None:
        """main.js must import initChatUI and wire Enter and Escape keys."""
        self.assertIn("import { initChatUI } from './ui/chat_ui.js';", self.main_js)
        self.assertIn("initChatUI();", self.main_js)
        self.assertIn("key === 'ENTER'", self.main_js)
        self.assertIn("modal-item-link-tooltip", self.main_js)
        self.assertIn("chat-expanded-panel", self.main_js)

    def test_13_chat_manager_ts_has_full_snapshot_fields(self) -> None:
        """ChatManager.ts ClientItemSnapshot must include itemLevel, createdAtMs, and affixes."""
        self.assertIn("itemLevel: number;", self.chat_mgr_ts)
        self.assertIn("createdAtMs: number;", self.chat_mgr_ts)
        self.assertIn("affixes: ClientItemAffix[];", self.chat_mgr_ts)
        self.assertIn("setQueryItemSnapshotCallback", self.chat_mgr_ts)
        self.assertIn("onTappedItemLink", self.chat_mgr_ts)

    # --- Group 5: Security, XSS Prevention & HMAC Verification ---

    def test_14_item_name_attribute_breakout_prevention(self) -> None:
        """renderMessageHtml must escape quotes preventing attribute breakout on data-name."""
        data = self._eval_chat_ui_node("""
            const payload = '[item:u1:s1:Item" autofocus onfocus="alert(1)":1] & \\'single\\'';
            const html = chatModule.renderMessageHtml(payload);
            console.log(JSON.stringify({
                html,
                hasBreakout: html.includes('data-name="Item" autofocus onfocus="alert(1)""'),
                hasEscapedQuotes: html.includes('data-name="Item&quot; autofocus onfocus=&quot;alert(1)&quot;"'),
                hasEscapedSingle: html.includes('&#39;single&#39;')
            }));
        """)
        self.assertFalse(data["hasBreakout"], "Double quotes must not break out of data-name attribute")
        self.assertTrue(data["hasEscapedQuotes"], "Double quotes in item tags must be escaped as &quot;")
        self.assertTrue(data["hasEscapedSingle"], "Single quotes must be escaped as &#39;")

    def test_15_sender_name_html_entity_escaping(self) -> None:
        """renderChatLog must sanitize msg.senderName with HTML entity escaping before inserting into innerHTML."""
        data = self._eval_chat_ui_node(f"""
            {self._mock_dom_js()}
            chatModule.addChatMessage({{
                messageId: 'msg_xss_sender',
                senderName: '<img src="x" onerror="alert(1)">',
                channel: 1,
                displayContent: 'Test Content',
                linkedItems: [],
                timestampMs: Date.now()
            }});
            const container = elements['chat-messages-container'];
            const lastRow = container.children[container.children.length - 1];
            console.log(JSON.stringify({{
                renderedHtml: lastRow?.innerHTML || '',
                escapedSender: chatModule.escapeHtml('<img src="x" onerror="alert(1)">')
            }}));
        """)
        self.assertNotIn('<img src="x"', data["renderedHtml"], "Raw HTML in senderName must never reach innerHTML")
        self.assertIn("&lt;img src=&quot;x&quot; onerror=&quot;alert(1)&quot;&gt;", data["renderedHtml"])
        self.assertEqual(data["escapedSender"], "&lt;img src=&quot;x&quot; onerror=&quot;alert(1)&quot;&gt;")

    def test_16_unverified_item_link_displays_warning_badge_and_no_fake_affixes(self) -> None:
        """Unverified/fallback item snapshots must display ⚠ Chưa Xác Thực badge and no fake affixes."""
        data = self._eval_chat_ui_node(f"""
            {self._mock_dom_js()}
            chatModule.openItemTooltip({{
                itemUuid: 'u-unverified-999',
                signature: 'sig-invalid',
                itemName: 'Kiếm Giả Mạo',
                rarity: 5,
                isVerified: false
            }});
            const badge = elements['item-tooltip-hmac-badge'];
            const affixesContainer = elements['item-tooltip-affixes'];
            const affixTexts = (affixesContainer?.children || []).map(c => c.textContent);
            console.log(JSON.stringify({{ badgeText: badge?.textContent, badgeClass: badge?.className, affixTexts }}));
        """)
        self.assertEqual(data["badgeText"], "⚠ Chưa Xác Thực")
        self.assertIn("bg-rose-950", data["badgeClass"])
        self.assertIn("text-rose-400", data["badgeClass"])
        self.assertIn("border-rose-800/60", data["badgeClass"])
        self.assertEqual(len(data["affixTexts"]), 1)
        self.assertEqual(data["affixTexts"][0], "• Không có dữ liệu thuộc tính (Chưa xác thực)")
        self.assertNotIn("Tăng Sát Thương Cổ Võ", "".join(data["affixTexts"]))

    def test_17_verified_item_link_displays_hmac_verified_badge(self) -> None:
        """Authenticated item snapshots must display ✓ HMAC Xác Thực badge and authentic affixes."""
        data = self._eval_chat_ui_node(f"""
            {self._mock_dom_js()}
            chatModule.registerItemSnapshot({{
                itemUuid: 'u-verified-100',
                signature: 'sig-valid',
                itemName: 'Thái Cổ Thần Thương',
                rarity: 5,
                quality: 20,
                itemLevel: 100,
                crafterName: 'Độc Cô Cầu Bại',
                affixes: [
                    {{ statName: 'Sát Thương Cổ Võ', statValue: 65, isImplicit: true }},
                    {{ statName: 'Bạo Kích Ngũ Hành', statValue: 25, isImplicit: false }}
                ]
            }});
            chatModule.openItemTooltip('u-verified-100');
            const badge = elements['item-tooltip-hmac-badge'];
            const affixesContainer = elements['item-tooltip-affixes'];
            const affixTexts = (affixesContainer?.children || []).map(c => c.textContent);
            const crafterText = elements['item-tooltip-crafter']?.textContent;
            console.log(JSON.stringify({{ badgeText: badge?.textContent, badgeClass: badge?.className, affixTexts, crafterText }}));
        """)
        self.assertEqual(data["badgeText"], "✓ HMAC Xác Thực")
        self.assertIn("bg-emerald-950", data["badgeClass"])
        self.assertIn("text-emerald-400", data["badgeClass"])
        self.assertIn("border-emerald-700/60", data["badgeClass"])
        self.assertEqual(data["crafterText"], "Độc Cô Cầu Bại")
        self.assertEqual(len(data["affixTexts"]), 2)
        self.assertIn("✦ Sát Thương Cổ Võ: +65", data["affixTexts"])
        self.assertIn("• Bạo Kích Ngũ Hành: +25", data["affixTexts"])

    def test_18_snapshot_cache_size_bounded_to_500(self) -> None:
        """snapshotCache must be strictly bounded to max 500 entries (FIFO eviction)."""
        data = self._eval_chat_ui_node("""
            for (let i = 1; i <= 600; i++) {
                chatModule.registerItemSnapshot({
                    itemUuid: `uuid_${i}`,
                    signature: `sig_${i}`,
                    itemName: `Item_${i}`,
                    rarity: 1
                });
            }
            console.log(JSON.stringify({
                size: chatModule.snapshotCache.size,
                hasOldest: chatModule.snapshotCache.has('uuid_1'),
                hasEvicted100: chatModule.snapshotCache.has('uuid_100'),
                hasKept101: chatModule.snapshotCache.has('uuid_101'),
                hasNewest: chatModule.snapshotCache.has('uuid_600')
            }));
        """)
        self.assertEqual(data["size"], 500)
        self.assertTrue(not data["hasOldest"] and not data["hasEvicted100"] and data["hasKept101"] and data["hasNewest"])


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