"""
Empirical Challenger Test Suite: Adversarial Verification of Reactive Language Switching in Chat UI.
Tests:
1. Live programmatic switching across 9 languages without page reload (tabs, placeholder, button, badge).
2. Dynamic channel switching and permission placeholder localization.
3. Cooldown badge states localization (locked, waiting, ready).
4. In-place reactive re-render of open rich item tooltips.
5. In-place reactive re-render of historical chat log messages and channel tags.
6. Stress test: 1,000 rapid sequential locale switches (SLA < 500ms, zero listener leak).
7. Invalid, unknown, and falsy locale fallbacks ('xx', null, undefined, '').
8. Listener error isolation and subscription cleanup.
9. Cross-tab storage event reactive synchronization.
"""

from __future__ import annotations

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

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
WEBAPP_DIR = PROJECT_ROOT / "client" / "webapp"
I18N_CATALOG_JS = (WEBAPP_DIR / "js" / "data" / "i18n_catalog.js").as_posix()
I18N_JS = (WEBAPP_DIR / "js" / "data" / "i18n.js").as_posix()
CHAT_CATALOG_JS = (WEBAPP_DIR / "js" / "data" / "chat_i18n_catalog.js").as_posix()
CHAT_UI_JS = (WEBAPP_DIR / "js" / "ui" / "chat_ui.js").as_posix()


def run_node_eval(script_body: str, timeout_sec: float = 15.0) -> Dict[str, Any]:
    """Execute ES module code in Node.js with simulated browser environment."""
    bootstrap = f"""
import fs from 'fs';
global.Image = class {{ constructor() {{ this.src = ''; }} }};
const elements = new Map();
const createMockEl = (tag, id = '') => {{
  let _html = '', _text = '';
  const children = [], classSet = new Set();
  const el = {{
    tagName: tag.toUpperCase(), id, dataset: {{}}, className: '', value: '', disabled: false, placeholder: '',
    get innerHTML() {{ return _html; }}, set innerHTML(v) {{ _html = v; if (!v) children.length = 0; }},
    get textContent() {{ return _text; }}, set textContent(v) {{ _text = v; }},
    classList: {{
      add(...c) {{ c.forEach(x => classSet.add(x)); el.className = Array.from(classSet).join(' '); }},
      remove(...c) {{ c.forEach(x => classSet.delete(x)); el.className = Array.from(classSet).join(' '); }},
      contains(c) {{ return classSet.has(c); }},
      toggle(c, f) {{ if (f ?? !classSet.has(c)) classSet.add(c); else classSet.delete(c); el.className = Array.from(classSet).join(' '); }}
    }},
    children, appendChild(c) {{ children.push(c); return c; }}, querySelector: () => null,
    querySelectorAll: (s) => (s === '.chat-channel-tab') ? Array.from(elements.values()).filter(e => e.classList.contains('chat-channel-tab')) : [],
    addEventListener: () => {{}}, focus: () => {{}}
  }};
  return el;
}};
const getOrCreate = (id, tag = 'div') => {{
  if (!elements.has(id)) elements.set(id, createMockEl(tag, id));
  return elements.get(id);
}};
const win = new EventTarget();
win.localStorage = {{ _s: {{}}, getItem(k) {{ return this._s[k] || null; }}, setItem(k, v) {{ this._s[k] = String(v); }} }};
global.window = win; global.document = {{
  getElementById: (id) => getOrCreate(id), createElement: (t) => createMockEl(t), querySelector: () => null,
  querySelectorAll: (s) => (s === '.chat-channel-tab') ? Array.from(elements.values()).filter(e => e.classList.contains('chat-channel-tab')) : [],
  addEventListener: () => {{}}, readyState: 'complete'
}};
global.localStorage = win.localStorage;
for (let i = 1; i <= 8; i++) {{
  const tab = createMockEl('button', 'tab-' + i);
  tab.classList.add('chat-channel-tab'); tab.dataset.channelId = String(i); elements.set('tab-' + i, tab);
}}
['chat-input', 'chat-send-btn', 'chat-cooldown-badge', 'chat-ticker-channel', 'chat-ticker-text',
 'chat-messages-container', 'modal-item-link-tooltip', 'item-tooltip-name', 'item-tooltip-sub',
 'item-tooltip-icon', 'item-tooltip-crafter', 'item-tooltip-affixes', 'item-tooltip-hmac-badge'].forEach(id => getOrCreate(id));

new Function(fs.readFileSync('{I18N_CATALOG_JS}', 'utf-8'))();
new Function(fs.readFileSync('{I18N_JS}', 'utf-8'))();
await import('file:///{CHAT_CATALOG_JS}');
const chatUI = await import('file:///{CHAT_UI_JS}');
chatUI.initChatUI();
{script_body}
"""
    res = subprocess.run(
        ["node", "--input-type=module", "-e", bootstrap],
        capture_output=True, text=True, encoding="utf-8", cwd=str(PROJECT_ROOT), timeout=timeout_sec
    )
    if res.returncode != 0:
        raise RuntimeError(f"Node execution failed (code {res.returncode}):\n{res.stderr}")
    for line in reversed(res.stdout.strip().split("\n")):
        line = line.strip()
        if (line.startswith("{") and line.endswith("}")) or (line.startswith("[") and line.endswith("]")):
            return json.loads(line)
    return {}


class TestChallengerI18nReactiveChat(unittest.TestCase):
    """Empirical adversarial test suite for live reactive language switching."""

    def test_01_live_programmatic_locale_switching_9_languages(self) -> None:
        """Calling setLocale across 9 languages updates tabs, button, placeholder reactively."""
        data = run_node_eval("""
        const locales = ['en', 'zh', 'ja', 'vi', 'ko', 'th', 'de', 'ru', 'es'];
        const res = {};
        for (const loc of locales) {
          window.FreeExileI18n.setLocale(loc);
          const tabs = Array.from(elements.values()).filter(e => e.classList.contains('chat-channel-tab')).map(t => t.textContent);
          res[loc] = { tabs, sendBtn: getOrCreate('chat-send-btn').textContent,
            placeholder: getOrCreate('chat-input').placeholder, badge: getOrCreate('chat-cooldown-badge').textContent };
        }
        console.log(JSON.stringify(res));
        """)
        self.assertEqual(data["en"]["tabs"][0], "World")
        self.assertEqual(data["en"]["sendBtn"], "Send")
        self.assertIn("Requires Level 20", data["en"]["placeholder"])
        self.assertEqual(data["en"]["badge"], "Locked")

        self.assertEqual(data["zh"]["tabs"][0], "世界")
        self.assertEqual(data["zh"]["sendBtn"], "发送")
        self.assertIn("20级", data["zh"]["placeholder"])

        self.assertEqual(data["ja"]["tabs"][0], "ワールド")
        self.assertEqual(data["ja"]["sendBtn"], "送信")
        self.assertIn("ワールドチャット", data["ja"]["placeholder"])

        self.assertEqual(data["vi"]["tabs"][0], "Thế Giới")
        self.assertEqual(data["vi"]["sendBtn"], "Gửi")
        self.assertEqual(data["vi"]["badge"], "Khóa")

        self.assertEqual(data["ko"]["tabs"][0], "월드")
        self.assertEqual(data["th"]["tabs"][0], "โลก")
        self.assertEqual(data["de"]["tabs"][0], "Welt")
        self.assertEqual(data["ru"]["tabs"][0], "Мир")
        self.assertEqual(data["es"]["tabs"][0], "Mundo")

    def test_02_dynamic_channel_switching_placeholder(self) -> None:
        """Switching channels produces localized permissions and input placeholders."""
        data = run_node_eval("""
        window.FreeExileI18n.setLocale('en'); chatUI.switchChannel(2);
        const zonePlaceholderEn = getOrCreate('chat-input').placeholder;
        window.FreeExileI18n.setLocale('vi');
        const zonePlaceholderVi = getOrCreate('chat-input').placeholder;
        chatUI.switchChannel(6); window.FreeExileI18n.setLocale('en');
        const sysPlaceholderEn = getOrCreate('chat-input').placeholder;
        window.FreeExileI18n.setLocale('zh');
        const sysPlaceholderZh = getOrCreate('chat-input').placeholder;
        console.log(JSON.stringify({ zonePlaceholderEn, zonePlaceholderVi, sysPlaceholderEn, sysPlaceholderZh }));
        """)
        self.assertEqual(data["zonePlaceholderEn"], "Enter message [Zone]...")
        self.assertEqual(data["zonePlaceholderVi"], "Nhập tin nhắn [Khu Vực]...")
        self.assertIn("System channel is reserved", data["sysPlaceholderEn"])
        self.assertIn("系统频道仅供服务器公告使用", data["sysPlaceholderZh"])

    def test_03_cooldown_badge_states_localization(self) -> None:
        """Cooldown badges translate reactively for both locked and countdown states."""
        data = run_node_eval("""
        chatUI.switchChannel(6); window.FreeExileI18n.setLocale('en');
        const lockedEn = getOrCreate('chat-cooldown-badge').textContent;
        window.FreeExileI18n.setLocale('vi');
        const lockedVi = getOrCreate('chat-cooldown-badge').textContent;
        window.FreeExileI18n.setLocale('ja');
        const lockedJa = getOrCreate('chat-cooldown-badge').textContent;
        console.log(JSON.stringify({ lockedEn, lockedVi, lockedJa }));
        """)
        self.assertEqual(data["lockedEn"], "Locked")
        self.assertEqual(data["lockedVi"], "Khóa")
        self.assertEqual(data["lockedJa"], "ロック")

    def test_04_open_item_tooltip_in_place_reactive_update(self) -> None:
        """Open item tooltip dynamically re-renders in-place when locale changes."""
        data = run_node_eval("""
        chatUI.openItemTooltip({ itemUuid: 'uuid_sword_1', itemName: 'Bạch Hổ Thần Đao', rarity: 4, element: 1,
          quality: 20, itemLevel: 85, crafterName: 'Thiên Công',
          affixes: [{ statName: 'Sát Thương', statValue: 100, isImplicit: true }], isVerified: true });
        window.FreeExileI18n.setLocale('vi');
        const viSub = getOrCreate('item-tooltip-sub').textContent, viBadge = getOrCreate('item-tooltip-hmac-badge').textContent;
        window.FreeExileI18n.setLocale('en');
        const enSub = getOrCreate('item-tooltip-sub').textContent, enBadge = getOrCreate('item-tooltip-hmac-badge').textContent;
        window.FreeExileI18n.setLocale('ja');
        const jaSub = getOrCreate('item-tooltip-sub').textContent, jaBadge = getOrCreate('item-tooltip-hmac-badge').textContent;
        console.log(JSON.stringify({ viSub, viBadge, enSub, enBadge, jaSub, jaBadge }));
        """)
        self.assertIn("Thần Phẩm", data["viSub"])
        self.assertEqual(data["viBadge"], "✓ HMAC Xác Thực")
        self.assertIn("Divine", data["enSub"])
        self.assertEqual(data["enBadge"], "✓ HMAC Verified")
        self.assertIn("神品", data["jaSub"])
        self.assertIn("HMAC", data["jaBadge"])

    def test_05_chat_log_messages_reactive_re_render(self) -> None:
        """Historical chat messages re-render channel tags reactively on locale switch."""
        data = run_node_eval("""
        chatUI.addChatMessage({ messageId: 'msg_h1', senderName: 'DaoKhach', channel: 1,
          displayContent: 'Reactive chat check', linkedItems: [], timestampMs: 1700000000000 });
        const container = getOrCreate('chat-messages-container');
        window.FreeExileI18n.setLocale('vi'); const viHtml = container.children.map(c => c.innerHTML).join('');
        window.FreeExileI18n.setLocale('en'); const enHtml = container.children.map(c => c.innerHTML).join('');
        window.FreeExileI18n.setLocale('zh'); const zhHtml = container.children.map(c => c.innerHTML).join('');
        console.log(JSON.stringify({ hasTheGioi: viHtml.includes('[Thế Giới]'),
          hasWorld: enHtml.includes('[World]'), hasShijie: zhHtml.includes('[世界]') }));
        """)
        self.assertTrue(data["hasTheGioi"])
        self.assertTrue(data["hasWorld"])
        self.assertTrue(data["hasShijie"])

    def test_06_rapid_sequential_switching_stress_harness(self) -> None:
        """Stress testing 1,000 rapid sequential locale switches satisfies < 500ms SLA."""
        data = run_node_eval("""
        const initialListeners = window.FreeExileI18n.listeners.length;
        let eventCount = 0;
        window.addEventListener('freeexile:localeChanged', () => { eventCount++; });
        const locales = ['en', 'zh', 'ja', 'vi', 'ko', 'th', 'de', 'ru', 'es'];
        const t0 = performance.now();
        for (let i = 0; i < 1000; i++) window.FreeExileI18n.setLocale(locales[i % locales.length]);
        const elapsedMs = performance.now() - t0, finalListeners = window.FreeExileI18n.listeners.length;
        const finalLocale = window.FreeExileI18n.getLocale();
        const tab1 = Array.from(elements.values()).filter(e => e.classList.contains('chat-channel-tab'))[0].textContent;
        console.log(JSON.stringify({ elapsedMs, initialListeners, finalListeners, eventCount, finalLocale, tab1 }));
        """)
        self.assertLess(data["elapsedMs"], 500.0, "1,000 switches must execute within 500ms")
        self.assertEqual(data["initialListeners"], data["finalListeners"], "Zero listener leak expected")
        self.assertEqual(data["eventCount"], 1000, "Must dispatch 1,000 CustomEvents")
        self.assertEqual(data["finalLocale"], "en")
        self.assertEqual(data["tab1"], "World")

    def test_07_invalid_and_unknown_locale_fallbacks(self) -> None:
        """Unknown and falsy locales fall back gracefully without unhandled exceptions."""
        data = run_node_eval("""
        window.FreeExileI18n.setLocale('en'); window.FreeExileI18n.setLocale('xx');
        const tabsUnknown = Array.from(elements.values()).filter(e => e.classList.contains('chat-channel-tab')).map(t => t.textContent);
        const sendBtnUnknown = getOrCreate('chat-send-btn').textContent, locBeforeFalsy = window.FreeExileI18n.getLocale();
        window.FreeExileI18n.setLocale(null); window.FreeExileI18n.setLocale(undefined); window.FreeExileI18n.setLocale('');
        const locAfterFalsy = window.FreeExileI18n.getLocale();
        window.FreeExileI18n.setLocale('vi');
        const tab1Recovered = Array.from(elements.values()).filter(e => e.classList.contains('chat-channel-tab'))[0].textContent;
        console.log(JSON.stringify({ tab0Unknown: tabsUnknown[0], sendBtnUnknown, locBeforeFalsy, locAfterFalsy, tab1Recovered }));
        """)
        self.assertEqual(data["tab0Unknown"], "World")
        self.assertEqual(data["sendBtnUnknown"], "Send")
        self.assertEqual(data["locBeforeFalsy"], data["locAfterFalsy"])
        self.assertEqual(data["tab1Recovered"], "Thế Giới")

    def test_08_listener_resilience_and_unsubscription(self) -> None:
        """Subscriber throwing an exception does not halt other listeners, and unsubscribe works."""
        data = run_node_eval("""
        let secondRan = false;
        window.FreeExileI18n.subscribe(() => { throw new Error('Faulty listener error'); });
        window.FreeExileI18n.subscribe(() => { secondRan = true; });
        let unsubCounter = 0;
        const unsub = window.FreeExileI18n.subscribe(() => { unsubCounter++; });
        window.FreeExileI18n.setLocale('en'); const countBeforeUnsub = unsubCounter;
        unsub(); window.FreeExileI18n.setLocale('zh'); const countAfterUnsub = unsubCounter;
        console.log(JSON.stringify({ secondRan, countBeforeUnsub, countAfterUnsub }));
        """)
        self.assertTrue(data["secondRan"], "Subsequent listener must execute despite prior throw")
        self.assertEqual(data["countBeforeUnsub"], 1)
        self.assertEqual(data["countAfterUnsub"], 1, "Unsubscribed listener must not be called")

    def test_09_storage_event_cross_tab_sync(self) -> None:
        """Storage event from peer browser tab triggers locale synchronization."""
        data = run_node_eval("""
        window.FreeExileI18n.setLocale('vi');
        if (window.dispatchEvent) {
          const ev = new Event('storage');
          ev.key = 'freeexile_locale'; ev.newValue = 'ja'; window.dispatchEvent(ev);
        }
        const locale = window.FreeExileI18n.getLocale();
        const tab1 = Array.from(elements.values()).filter(e => e.classList.contains('chat-channel-tab'))[0].textContent;
        console.log(JSON.stringify({ locale, tab1 }));
        """)
        self.assertEqual(data["locale"], "ja")
        self.assertEqual(data["tab1"], "ワールド")


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