"""
Adversarial Stress, Edge-Case & Vulnerability Test Suite for Milestone M2.
Tests:
1. Ring buffer overflow (> 1000 messages added rapidly across 8 channels).
2. Malformed item tags, edge-case rarities, unclosed tags, and ReDoS resistance.
3. XSS and HTML injection vulnerabilities (attribute breakouts, senderName, script tags).
4. Cooldown bypass attempts, rapid channel switching, and Unicode zero-width characters.
5. Headless / SSR safety checks.
"""

from __future__ import annotations

import json
import subprocess
import time
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"
CHAT_UI_JS = WEBAPP_DIR / "js" / "ui" / "chat_ui.js"
CHAT_MGR_JS = PROJECT_ROOT / "client" / "dist" / "chat" / "ChatManager.js"


def run_node(code: str, timeout_sec: float = 15.0) -> Dict[str, Any]:
    """Helper to run JS code in Node.js and return parsed JSON."""
    wrapped = f"""
    (async () => {{
        try {{
            {code}
            process.exit(0);
        }} catch (err) {{
            console.error(err);
            process.exit(1);
        }}
    }})();
    """
    res = subprocess.run(
        ["node", "--input-type=module", "-e", wrapped],
        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 TestRingBufferAdversarialStress(unittest.TestCase):
    """Stress tests the 100-message FIFO ring buffer under rapid flood conditions."""

    def test_01_chat_ui_ring_buffer_overflow_2000_msgs(self) -> None:
        """2,000 rapid messages per channel must never exceed 100 capacity and maintain FIFO."""
        js = """
        const m = await import('./client/webapp/js/ui/chat_ui.js');
        const initialMem = process.memoryUsage().heapUsed;

        for (let ch = 1; ch <= 8; ch++) {
            for (let i = 1; i <= 2000; i++) {
                m.addChatMessage({
                    messageId: `msg_${ch}_${i}`,
                    senderName: `Player_${i}`,
                    channel: ch,
                    displayContent: `Rapid flood message ${i} on channel ${ch}`,
                    linkedItems: [],
                    timestampMs: 1000000 + i
                });
            }
        }

        const stats = {};
        for (let ch = 1; ch <= 8; ch++) {
            const history = m.getChannelHistory(ch);
            stats[ch] = {
                length: history.length,
                oldestId: history[0]?.messageId,
                newestId: history[history.length - 1]?.messageId
            };
        }
        const finalMem = process.memoryUsage().heapUsed;
        console.log(JSON.stringify({ stats, memDeltaMb: (finalMem - initialMem) / 1024 / 1024 }));
        """
        data = run_node(js)
        stats = data["stats"]
        self.assertEqual(len(stats), 8)
        for ch_str, st in stats.items():
            ch = int(ch_str)
            self.assertEqual(st["length"], 100, f"Channel {ch} exceeded 100 messages")
            self.assertEqual(st["oldestId"], f"msg_{ch}_1901", f"Channel {ch} violated FIFO eviction")
            self.assertEqual(st["newestId"], f"msg_{ch}_2000", f"Channel {ch} missed newest message")
        self.assertLess(data["memDeltaMb"], 50.0, "Memory delta excessive during flood")

    def test_02_chat_manager_ts_ring_buffer_overflow(self) -> None:
        """ClientChatManager engine must cap at 100 messages FIFO under 2,000 rapid messages."""
        js = """
        const { ClientChatManager, ChatChannel } = await import('./client/dist/chat/ChatManager.js');
        const mgr = new ClientChatManager();

        for (let ch = 1; ch <= 8; ch++) {
            for (let i = 1; i <= 2000; i++) {
                mgr.receiveMessage({
                    messageId: `msg_${ch}_${i}`,
                    senderName: `User_${i}`,
                    channel: ch,
                    displayContent: `Flood content ${i}`,
                    linkedItems: [],
                    timestampMs: 1000 + i
                });
            }
        }

        const stats = {};
        for (let ch = 1; ch <= 8; ch++) {
            const list = mgr.getMessagesForChannel(ch);
            stats[ch] = {
                length: list.length,
                oldestId: list[0]?.messageId,
                newestId: list[list.length - 1]?.messageId
            };
        }
        console.log(JSON.stringify(stats));
        """
        data = run_node(js)
        for ch_str, st in data.items():
            self.assertEqual(st["length"], 100)
            self.assertEqual(st["oldestId"], f"msg_{ch_str}_1901")
            self.assertEqual(st["newestId"], f"msg_{ch_str}_2000")


class TestMalformedItemTagsAndReDoS(unittest.TestCase):
    """Challenges item tag parser and renderer against malformed injections and ReDoS."""

    def test_03_malformed_item_tags_resilience(self) -> None:
        """Malformed item tags must not crash parser and must not extract corrupt structures."""
        js = """
        const m = await import('./client/webapp/js/ui/chat_ui.js');
        const { ClientChatManager } = await import('./client/dist/chat/ChatManager.js');

        const inputs = [
            '[item:malformed]',
            '[item:::]',
            '[item:::::]',
            '[item:u1]',
            '[item:u1:s1:name:-5]',
            '[item:u1:s1:name:abc]',
            '[item:u1:s1:name:0]',
            '[item:u1:s1:name:999999]',
            '[item:u1:s1:name:1.23]',
            '[item:!@#$%:^&*():Sword:1]',
            'Unclosed [item:u1:s1:Sword:1 and tail',
            'Empty [item:]'
        ];

        const results = [];
        for (const input of inputs) {
            const parsedUI = m.parseItemTags(input);
            const parsedTS = ClientChatManager.parseItemTags(input);
            const rendered = m.renderMessageHtml(input);
            results.push({
                input,
                parsedCountUI: parsedUI.length,
                parsedCountTS: parsedTS.length,
                renderedSafe: typeof rendered === 'string'
            });
        }
        console.log(JSON.stringify(results));
        """
        data = run_node(js)
        for res in data:
            self.assertTrue(res["renderedSafe"])
            if "malformed" in res["input"] or res["input"] == "[item:::]":
                self.assertEqual(res["parsedCountUI"], 0, f"Should not parse invalid tag: {res['input']}")

    def test_04_regex_redos_adversarial_resistance(self) -> None:
        """Regex parser must evaluate 100,000-character adversarial inputs in < 20ms without ReDoS."""
        js = """
        const m = await import('./client/webapp/js/ui/chat_ui.js');
        const adversarialStrings = [
            '[item:' + 'a'.repeat(100000),
            '[item:uuid:' + 'b'.repeat(100000),
            '[item:uuid:sig:' + 'c'.repeat(100000),
            '[item:uuid:sig:name:' + '9'.repeat(100000),
            '[item:'.repeat(20000)
        ];

        const timings = [];
        for (const str of adversarialStrings) {
            const t0 = performance.now();
            m.parseItemTags(str);
            m.renderMessageHtml(str);
            const t1 = performance.now();
            timings.push(t1 - t0);
        }
        console.log(JSON.stringify(timings));
        """
        timings = run_node(js)
        for t in timings:
            self.assertLess(t, 25.0, f"Regex evaluation took too long: {t:.2f}ms (potential ReDoS)")


class TestXssAndHtmlInjectionVulnerabilities(unittest.TestCase):
    """Adversarially tests for cross-site scripting (XSS) and attribute breakouts."""

    def test_05_content_script_tag_sanitization(self) -> None:
        """Standard HTML tags in message text must be escaped by renderMessageHtml."""
        js = """
        const m = await import('./client/webapp/js/ui/chat_ui.js');
        const raw = '<script>alert(\"xss\")</script><b>bold</b><img src=x onerror=alert(1)>';
        const rendered = m.renderMessageHtml(raw);
        console.log(JSON.stringify({
            hasRawScript: rendered.includes('<script>'),
            hasRawImg: rendered.includes('<img'),
            hasEscapedScript: rendered.includes('&lt;script&gt;'),
            hasEscapedImg: rendered.includes('&lt;img')
        }));
        """
        data = run_node(js)
        self.assertFalse(data["hasRawScript"])
        self.assertFalse(data["hasRawImg"])
        self.assertTrue(data["hasEscapedScript"])
        self.assertTrue(data["hasEscapedImg"])

    def test_06_item_name_attribute_breakout_vulnerability(self) -> None:
        """EMPIRICAL AUDIT: Detects whether item tag name attribute breakout executes XSS."""
        js = """
        const m = await import('./client/webapp/js/ui/chat_ui.js');
        const payload = '[item:u1:s1:\\\" autofocus onfocus=\\\"alert(1):1]';
        const rendered = m.renderMessageHtml(payload);
        const isVulnerable = rendered.includes('data-name=\"\" autofocus onfocus=\"alert(1)\"');
        console.log(JSON.stringify({ rendered, isVulnerable }));
        """
        data = run_node(js)
        # We record whether the vulnerability exists
        if data["isVulnerable"]:
            # Attribute breakout is empirically detected
            self.assertTrue(data["isVulnerable"], "Attribute breakout XSS vulnerability detected in renderMessageHtml")

    def test_07_sender_name_raw_html_injection_audit(self) -> None:
        """EMPIRICAL AUDIT: Detects whether senderName is concatenated raw into row.innerHTML."""
        js = """
        const fs = await import('fs');
        const code = fs.readFileSync('./client/webapp/js/ui/chat_ui.js', 'utf-8');
        // Check line 244: row.innerHTML = ... ${msg.senderName} ...
        const rawSenderInterpolation = code.includes('${msg.senderName}:');
        console.log(JSON.stringify({ rawSenderInterpolation }));
        """
        data = run_node(js)
        self.assertTrue(data["rawSenderInterpolation"], "msg.senderName is interpolated unescaped into innerHTML")


class TestCooldownAndChannelSwitching(unittest.TestCase):
    """Verifies that cooldowns cannot be bypassed by switching channels or rapid calling."""

    def test_08_cooldown_bypass_attempt_via_channel_switching(self) -> None:
        """Switching channels and returning must NOT bypass active cooldown timer."""
        js = """
        const createMockElement = (id) => ({
            id, dataset: { channelId: '1' },
            classList: { _c: new Set(), add(c){this._c.add(c);}, remove(c){this._c.delete(c);}, contains(c){return this._c.has(c);} },
            disabled: false, innerText: '', textContent: '',
            addEventListener: () => {}, querySelectorAll: () => [], querySelector: () => null, appendChild: () => {}
        });
        global.document = {
            getElementById: (id) => createMockElement(id),
            querySelectorAll: () => [createMockElement('tab')],
            addEventListener: () => {}, createElement: (t) => createMockElement(t), readyState: 'complete'
        };

        const m = await import('./client/webapp/js/ui/chat_ui.js');
        const { ClientChatManager, ChatChannel } = await import('./client/dist/chat/ChatManager.js');

        // Trigger send on World channel (15s)
        m.recordMessageSent(1);
        const remInitial = m.getCooldownRemaining(1);

        // Rapid channel switching
        m.switchChannel(2);
        m.switchChannel(3);
        m.switchChannel(7);
        m.switchChannel(1);
        const remAfterSwitch = m.getCooldownRemaining(1);

        const mgr = new ClientChatManager();
        mgr.recordMessageSent(ChatChannel.World);
        const mgrBefore = mgr.checkCooldown(ChatChannel.World);
        mgr.setActiveChannel(ChatChannel.Zone);
        mgr.setActiveChannel(ChatChannel.World);
        const mgrAfter = mgr.checkCooldown(ChatChannel.World);

        console.log(JSON.stringify({
            remInitial,
            remAfterSwitch,
            mgrBeforeAllowed: mgrBefore.allowed,
            mgrAfterAllowed: mgrAfter.allowed
        }));
        """
        data = run_node(js)
        self.assertGreater(data["remInitial"], 14000)
        self.assertGreater(data["remAfterSwitch"], 14000)
        self.assertFalse(data["mgrBeforeAllowed"])
        self.assertFalse(data["mgrAfterAllowed"], "Cooldown must not be bypassed by activeChannel change")

    def test_09_unicode_zero_width_tag_handling(self) -> None:
        """Zero-width characters and homoglyphs inside item names must parse safely."""
        js = """
        const m = await import('./client/webapp/js/ui/chat_ui.js');
        const tag = '[item:u-99:s-99:V\\u200B\\u200C\\u200Dõ\\uFEFFThuật:5]';
        const parsed = m.parseItemTags(tag);
        const rendered = m.renderMessageHtml(tag);
        console.log(JSON.stringify({
            count: parsed.length,
            parsedName: parsed[0]?.itemName,
            renderedHasButton: rendered.includes('chat-item-link')
        }));
        """
        data = run_node(js)
        self.assertEqual(data["count"], 1)
        self.assertTrue(data["renderedHasButton"])


class TestPlaywrightBrowserExploit(unittest.TestCase):
    """Executes real browser exploit payloads in Chromium via Playwright."""

    def test_10_playwright_sender_name_and_item_breakout_xss(self) -> None:
        """EMPIRICAL AUDIT: Real browser executes XSS if senderName or item tag attribute is unescaped."""
        from playwright.sync_api import sync_playwright

        index_uri = (WEBAPP_DIR / "index.html").resolve().as_uri()
        with sync_playwright() as p:
            browser = p.chromium.launch(
                channel="msedge",
                headless=True,
                args=["--allow-file-access-from-files"]
            )
            page = browser.new_page()


            page.goto(index_uri)
            page.wait_for_timeout(300)

            # Test Exploit 1: senderName injected with onerror
            page.evaluate("""() => {
                window.__xss_sender = false;
                window.chatUI.addChatMessage({
                    messageId: 'poc_sender_xss',
                    senderName: '<img src=invalid_poc onerror="window.__xss_sender=true">',
                    channel: 1,
                    displayContent: 'Normal text',
                    linkedItems: [],
                    timestampMs: Date.now()
                });
            }""")
            page.wait_for_timeout(300)
            exploit1_fired = page.evaluate("() => window.__xss_sender")

            # Test Exploit 2: Item tag attribute breakout
            page.evaluate("""() => {
                window.__xss_item = false;
                window.chatUI.addChatMessage({
                    messageId: 'poc_item_xss',
                    senderName: 'Attacker',
                    channel: 1,
                    displayContent: 'Weapon [item:u1:s1:" autofocus onfocus="window.__xss_item=true:1]',
                    linkedItems: [],
                    timestampMs: Date.now()
                });
            }""")
            page.wait_for_timeout(300)
            exploit2_fired = page.evaluate("() => window.__xss_item")

            container_html = page.evaluate("() => document.getElementById('chat-messages-container').innerHTML")
            browser.close()


        # Both vulnerabilities must be blocked and harmlessly escaped
        self.assertFalse(exploit1_fired, "CRITICAL: Real Chromium executed arbitrary JS via senderName!")
        self.assertFalse(exploit2_fired, "CRITICAL: Real Chromium executed arbitrary JS via item tag attribute breakout onfocus!")
        self.assertIn("&lt;img", container_html, "Sanitized senderName must contain escaped entity &lt;img")
        self.assertNotIn("<img src=invalid_poc", container_html, "Raw <img> tag must never appear in innerHTML")


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

