#!/usr/bin/env python3
"""
Adversarial Stress Test Harness: Multilingual Lexicon & Wiki Diátaxis.
Agent: challenger_streamline_2
Execution:
    python .agents/teamwork/challenger_streamline_2/adversarial_i18n_wiki_test.py
"""

from __future__ import annotations
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List, Set
from urllib.parse import unquote, urlparse

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8")
if hasattr(sys.stderr, "reconfigure"):
    sys.stderr.reconfigure(encoding="utf-8")

PROJECT_ROOT = Path(r"c:\Projects\FreeExile")
CLIENT_DIR = PROJECT_ROOT / "client" / "webapp"
I18N_CATALOG_JS = CLIENT_DIR / "js" / "data" / "i18n_catalog.js"
I18N_JS = CLIENT_DIR / "js" / "data" / "i18n.js"
INDEX_HTML = CLIENT_DIR / "index.html"
WIKI_DIR = PROJECT_ROOT / "wiki"

ALL_9_LOCALES = ["vi", "en", "zh", "ja", "ko", "th", "de", "ru", "es"]
ALL_14_WIKI_SPECS = [
    "AI_AGENT_ORB_DELEGATION_SPECS.md", "CHARACTERS_DESIGN_SPECS.md",
    "CORE_LORE_AND_SYSTEMS.md", "CURRENCIES_AND_SPIRIT_STONES_SPECS.md",
    "ECONOMIC_BALANCE_AND_ANTI_INFLATION_SPECS.md", "ENDGAME_WORLD_MAP_AND_CONTENT_SPECS.md",
    "INVENTORY_STASH_AND_SPATIAL_GRID_SPECS.md", "ITEMS_AND_EQUIPMENT_SPECS.md",
    "MAPS_DUNGEONS_AND_SECRET_CHAMBERS_SPECS.md", "MAP_PROCGEN_WARFOG_ANTIBOT_GUIDE.md",
    "MEGA_SHOP_AND_PAYMENTS_SPECS.md", "MONSTERS_AND_BOSSES_SPECS.md",
    "PLAYER_HIDEOUT_AND_ASTRAL_PORTALS_SPECS.md", "QUESTS_AND_MILESTONES_SPECS.md"
]


class AdversarialI18nWikiHarness:
    """Core empirical testing engine."""

    def __init__(self) -> None:
        self.catalog: Dict[str, Dict[str, str]] = {}

    def load_catalog_via_node(self) -> Dict[str, Dict[str, str]]:
        esc_path = str(I18N_CATALOG_JS).replace("\\", "/")
        js = f"const fs=require('fs'),w={{}};new Function('window',fs.readFileSync('{esc_path}','utf8'))(w);console.log(JSON.stringify(w.I18N_CATALOG));"
        res = subprocess.run(["node", "-e", js], capture_output=True, encoding="utf-8", check=True, cwd=str(PROJECT_ROOT))
        self.catalog = json.loads(res.stdout)
        return self.catalog

    def test_key_parity_and_integrity(self) -> Dict[str, Any]:
        """Test 143 key parity, duplicate declaration, and empty string check."""
        report: Dict[str, Any] = {"passed": True, "details": {}}
        vi_keys = set(self.catalog.get("vi", {}).keys())
        if len(vi_keys) != 143:
            report["passed"] = False
            report["details"]["vi_key_count"] = f"Expected 143, got {len(vi_keys)}"

        for loc in ALL_9_LOCALES:
            loc_keys = set(self.catalog.get(loc, {}).keys())
            missing, extra = vi_keys - loc_keys, loc_keys - vi_keys
            empty = [k for k, v in self.catalog.get(loc, {}).items() if not str(v).strip()]
            loc_ok = (len(loc_keys) == 143) and not missing and not extra and not empty
            if not loc_ok:
                report["passed"] = False
            report["details"][loc] = {"key_count": len(loc_keys), "missing": list(missing), "empty": empty, "ok": loc_ok}

        raw = I18N_CATALOG_JS.read_text(encoding="utf-8")
        token_report: Dict[str, List[str]] = {}
        for i, loc in enumerate(ALL_9_LOCALES):
            nxt = ALL_9_LOCALES[i + 1] if i + 1 < len(ALL_9_LOCALES) else None
            pat = rf"\b{loc}\s*:\s*\{{(.*?)\n\s*{nxt}\s*:\s*\{{" if nxt else rf"\b{loc}\s*:\s*\{{(.*?)\n\s*\}}\s*;"
            m = re.search(pat, raw, re.DOTALL)
            if m:
                s = re.sub(r'"(?:\\.|[^"\\])*"', '""', re.sub(r"'(?:\\.|[^'\\])*'", "''", m.group(1)))
                keys = re.findall(r"([a-zA-Z0-9_]+)\s*:", s)
                seen: Set[str] = set()
                dupes = [k for k in keys if k in seen or seen.add(k)]
                if dupes:
                    report["passed"] = False
                    token_report[loc] = dupes
        report["details"]["source_duplicates"] = token_report
        return report

    def test_rapid_language_switching_1000_cycles(self) -> Dict[str, Any]:
        """Execute 1,000 rapid language switching cycles in Node.js VM with mock DOM."""
        js = """
        const fs = require('fs'), vm = require('vm');
        const cat = fs.readFileSync('client/webapp/js/data/i18n_catalog.js', 'utf8');
        const i18n = fs.readFileSync('client/webapp/js/data/i18n.js', 'utf8');
        const html = fs.readFileSync('client/webapp/index.html', 'utf8');
        const htmlKeys = [...html.matchAll(/data-i18n=["']([a-zA-Z0-9_-]+)["']/g)].map(m => m[1]);
        class MockEl {
            constructor(id, attrs = {}) { this.id = id; this.attributes = attrs; this.textContent = ''; this.innerText = ''; this.dataset = {}; }
            getAttribute(n) { return this.attributes[n] || null; }
            addEventListener() {}
            removeEventListener() {}
            querySelector() { return new MockEl('sub'); }
        }
        const els = htmlKeys.map(k => new MockEl('el-' + k, { 'data-i18n': k }));
        ['txt-char-name', 'txt-modal-meridian-title', 'txt-modal-vault-title', 'skill-dodge', 'lang-select'].forEach(id => els.push(new MockEl(id)));
        const doc = {
            readyState: 'complete',
            getElementById: id => els.find(e => e.id === id) || null,
            querySelectorAll: sel => sel.includes('[data-i18n]') ? els.filter(e => e.getAttribute('data-i18n')) : [],
            addEventListener: () => {}
        };
        const sb = { document: doc, localStorage: { store: {}, getItem: k => sb.localStorage.store[k]||null, setItem: (k,v)=>{sb.localStorage.store[k]=String(v);} },
            Image: class { constructor() { this.src = ''; } }, CustomEvent: class { constructor(t,o) { this.type = t; this.detail = o?o.detail:{}; } },
            console, setTimeout, joystickActive: false };
        sb.window = sb; sb.window.document = doc; sb.window.localStorage = sb.localStorage; sb.window.Image = sb.Image;
        sb.window.dispatchEvent = () => {}; sb.window.addEventListener = () => {};
        vm.createContext(sb); vm.runInContext(cat, sb); vm.runInContext(i18n, sb);
        const engine = sb.FreeExileI18n, locales = ['vi','en','zh','ja','ko','th','de','ru','es'];
        const startHeap = process.memoryUsage().heapUsed, startTime = Date.now();
        let fails = 0, corrupted = 0;
        for (let i = 0; i < 1000; i++) {
            const loc = locales[i % locales.length];
            engine.setLocale(loc);
            if (engine.getLocale() !== loc) fails++;
            engine.updateDOM();
            for (const el of els) {
                if (el.textContent === 'undefined' || el.textContent === 'null') corrupted++;
                if (el.innerText === 'undefined' || el.innerText === 'null') corrupted++;
            }
            if (!engine.t('dodge') || !engine.getPlayerName() || !engine.t('interaction_f_prompt', { name: 'NPC', service: 'S' })) fails++;
        }
        const delta = ((process.memoryUsage().heapUsed - startHeap) / (1024*1024)).toFixed(2);
        console.log(JSON.stringify({ cycles: 1000, failures: fails, corrupted, elapsedMs: Date.now() - startTime, heapDeltaMb: delta, finalLocale: engine.getLocale() }));
        """
        res = subprocess.run(["node", "-e", js], capture_output=True, encoding="utf-8", check=True, cwd=str(PROJECT_ROOT))
        d = json.loads(res.stdout)
        d["passed"] = (d["failures"] == 0 and d["corrupted"] == 0)
        return d

    def test_description_strings_spec_format(self) -> Dict[str, Any]:
        """Verify all 23 description keys in all 9 languages meet 1-sentence technical spec format."""
        desc_keys = [k for k in self.catalog.get("vi", {}).keys() if k.endswith("_desc")]
        rep: Dict[str, Any] = {"passed": True, "count": len(desc_keys), "max_len": 0, "violations": [], "th_no_period": 0, "decimal_locs": []}
        if len(desc_keys) != 23:
            rep["passed"] = False
            rep["violations"].append(f"Expected 23 descs, got {len(desc_keys)}")

        for loc in ALL_9_LOCALES:
            for k in desc_keys:
                v = self.catalog.get(loc, {}).get(k, "")
                if not v:
                    rep["passed"] = False
                    rep["violations"].append(f"[{loc}] '{k}' empty")
                    continue
                rep["max_len"] = max(rep["max_len"], len(v))
                if len(v) > 100 or "\n" in v or "\r" in v:
                    rep["passed"] = False
                    rep["violations"].append(f"[{loc}] '{k}' length/newline violation: {len(v)}")
                s = v.rstrip(".。")
                if ". " in s or "。" in s or "!" in s or "?" in s:
                    rep["passed"] = False
                    rep["violations"].append(f"[{loc}] '{k}' multi-sentence: '{v}'")
                if loc in ["vi", "en", "ko", "de", "ru", "es"] and not v.endswith("."):
                    rep["passed"] = False
                    rep["violations"].append(f"[{loc}] '{k}' missing .")
                elif loc in ["zh", "ja"] and not v.endswith("。"):
                    rep["passed"] = False
                    rep["violations"].append(f"[{loc}] '{k}' missing 。")
                elif loc == "th" and not v.endswith("."):
                    rep["th_no_period"] += 1
                if "0.25" in v and loc not in rep["decimal_locs"]:
                    rep["decimal_locs"].append(loc)
        return rep

    def test_interpolation_and_unicode_escaping(self) -> Dict[str, Any]:
        """Adversarial stress-test on parameter injection, escaping, and fallback."""
        js = """
        const fs = require('fs'), vm = require('vm');
        const cat = fs.readFileSync('client/webapp/js/data/i18n_catalog.js', 'utf8');
        const i18n = fs.readFileSync('client/webapp/js/data/i18n.js', 'utf8');
        const sb = { document: { getElementById: ()=>null, querySelectorAll: ()=>[], addEventListener: ()=>{} },
            localStorage: { getItem: ()=>null, setItem: ()=>{} }, Image: class { constructor() { this.src=''; } }, CustomEvent: class {} };
        sb.window = sb; sb.window.dispatchEvent = ()=>{}; sb.window.addEventListener = ()=>{};
        sb.window.document = sb.document; sb.window.localStorage = sb.localStorage; sb.window.Image = sb.Image;
        vm.createContext(sb); vm.runInContext(cat, sb); vm.runInContext(i18n, sb);
        const eng = sb.FreeExileI18n, res = [];
        eng.setLocale('vi');
        res.push({ test: 'vi_interp', pass: eng.t('interaction_f_prompt', { name: 'Thợ Rèn', service: 'Lò Rèn' }) === '[F] Thợ Rèn - Lò Rèn' });
        eng.setLocale('en');
        res.push({ test: 'en_interp', pass: eng.t('interaction_f_prompt', { name: 'Smith', service: 'Forge' }) === '[F] Smith - Forge' });
        res.push({ test: 'count_interp', pass: eng.t('portal_map_device_active', { count: 6 }).includes('6/6') });
        res.push({ test: 'regex_params', pass: eng.t('interaction_f_prompt', { name: '$1-Danger', service: 'Test$&' }).includes('$1-Danger') });
        res.push({ test: 'empty_params', pass: typeof eng.t('interaction_f_prompt', {}) === 'string' });
        res.push({ test: 'null_key', pass: eng.t(null, {}, 'fb') === 'fb' });
        res.push({ test: 'unknown_key_fb', pass: eng.t('nonexistent_k', {}, 'safe') === 'safe' });
        res.push({ test: 'raw_key_fb', pass: eng.t('nonexistent_k') === 'nonexistent_k' });
        console.log(JSON.stringify({ passed: res.every(r => r.pass), checks: res }));
        """
        r = subprocess.run(["node", "-e", js], capture_output=True, encoding="utf-8", check=True, cwd=str(PROJECT_ROOT))
        return json.loads(r.stdout)

    def test_wiki_articles_and_link_resolution(self) -> Dict[str, Any]:
        """Verify all 14 wiki articles exist in VI, EN, ZH and all markdown/file links resolve."""
        rep: Dict[str, Any] = {"passed": True, "articles": 0, "links": 0, "missing": [], "broken": []}
        for lang in ["vi", "en", "zh"]:
            ld = WIKI_DIR / lang
            if not ld.is_dir():
                rep["passed"] = False
                rep["missing"].append(f"wiki/{lang}")
                continue
            for spec in ALL_14_WIKI_SPECS:
                rep["articles"] += 1
                if not (ld / spec).is_file():
                    rep["passed"] = False
                    rep["missing"].append(f"wiki/{lang}/{spec}")

        for wf in WIKI_DIR.rglob("*.md"):
            txt = wf.read_text(encoding="utf-8")
            targets = [u.strip() for _, u in re.findall(r"\[([^\]]+)\]\(([^)]+)\)", txt)] + re.findall(r'<a\s+[^>]*?href=["\']([^"\']+)["\']', txt)
            for t in targets:
                if not t or t.startswith(("http:", "https:", "mailto:")):
                    continue
                rep["links"] += 1
                if t.startswith("file:///"):
                    p = unquote(urlparse(t).path)
                    tp = Path(p[1:] if p.startswith("/") and len(p) > 2 and p[2] == ":" else p)
                else:
                    f = t.split("#", 1)[0]
                    tp = wf if not f else (wf.parent / f).resolve()
                if not tp.exists():
                    rep["passed"] = False
                    rep["broken"].append({"src": str(wf.relative_to(PROJECT_ROOT)), "target": t})
        return rep

    def run_all(self) -> Dict[str, Any]:
        self.load_catalog_via_node()
        t1 = self.test_key_parity_and_integrity()
        t2 = self.test_rapid_language_switching_1000_cycles()
        t3 = self.test_description_strings_spec_format()
        t4 = self.test_interpolation_and_unicode_escaping()
        t5 = self.test_wiki_articles_and_link_resolution()
        all_ok = t1["passed"] and t2["passed"] and t3["passed"] and t4["passed"] and t5["passed"]
        return {"overall_pass": all_ok, "t1": t1, "t2": t2, "t3": t3, "t4": t4, "t5": t5}


if __name__ == "__main__":
    h = AdversarialI18nWikiHarness()
    res = h.run_all()
    print("=" * 80)
    print("FREEEXILE ADVERSARIAL CHALLENGER: I18N & WIKI STRESS REPORT")
    print("=" * 80)
    print(f"Overall Passed: {res['overall_pass']}")
    print(f"T1 (143 Key Parity & No Dupes): {res['t1']['passed']}")
    print(f"T2 (1,000 Rapid Cycles): {res['t2']['passed']} ({res['t2']['elapsedMs']}ms, {res['t2']['heapDeltaMb']}MB heap delta)")
    print(f"T3 (23 Description Specs <=100 chars, 0 newlines): {res['t3']['passed']} (max_len={res['t3']['max_len']})")
    print(f"T4 (Interpolation & Unicode Escaping): {res['t4']['passed']}")
    print(f"T5 (14 Wiki Articles & Link Resolution): {res['t5']['passed']} ({res['t5']['links']} links checked)")
    print("=" * 80)
    sys.exit(0 if res["overall_pass"] else 1)
